브라우저에서 파일을 동기식으로 다루는 법

Mar 20, 2026

5 min
💬

브라우저에서 동기식 파일 I/O를 하려면 저장은 OPFS, 실행 자리는 Web Worker, 읽고 쓰기는 FileSystemSyncAccessHandle이 담당한다. 세 개념이 각각 어떤 문제를 풀고 왜 항상 같이 등장하는지를 Pyodide 이슈 사례로 따라가 보았다.

1. 개요

브라우저에서 동기식 파일 I/O를 다루려면 저장 위치, 실행 컨텍스트, I/O 방식이라는 세 문제를 함께 풀어야 한다. OPFS(Origin Private File System)는 브라우저 내부 전용 저장 공간을 제공하고, Web Worker는 블로킹 작업을 메인 스레드 밖에서 실행하게 한다. FileSystemSyncAccessHandle은 OPFS 파일을 동기식으로 읽고 쓰게 해준다. Pyodide의 브라우저 파일 I/O 이슈에서도 이 세 개념이 함께 등장한다.

이 글은 네 가지 질문을 따라간다.

  • OPFS(Origin Private File System)가 왜 필요한지, 기존 방식과 어떻게 다른지
  • Web Worker가 파일 I/O와 어떤 관계인지
  • FileSystemSyncAccessHandle이 어떤 문제를 해결하는지
  • 세 개념이 왜 항상 같이 등장하는지

2. 왜 OPFS(Origin Private File System) 가 필요한가

브라우저의 파일 처리는 오랫동안 <input type="file">이나 FileReader 기반의 제한적 API에 의존했다. File API는 사용자가 file input이나 drag and drop으로 건네준 파일의 복사본을 읽고 처리하는 쪽에 가깝고, 원본 파일에 다시 쓰는 길은 없다.

이후 File System Access API가 등장해 사용자 동의 기반으로 로컬 파일을 읽고 쓸 수 있게 됐다. 이때 픽커로 여는 사용자 파일과는 별도로, 앱이 내부용으로 쓰는 origin 전용 저장소인 OPFS도 같은 스펙에 함께 들어왔다. 둘은 대상이 다르다. File System Access API가 다루는 건 사용자에게 보이는 파일 시스템이고, OPFS는 사용자에게 보이지 않는 앱 전용 공간이다.

OPFS(Origin Private File System)는 각 origin(웹사이트 주소) 단위로 격리된 브라우저 내부 전용 파일 시스템으로, 브라우저가 관리하는 앱 전용 저장소에 가깝다. 사용자에게 직접 노출되지 않는 저장소라 사용자에게 보이는 파일 시스템보다 권한과 보안 흐름이 단순하다. 여기에 뒤이어 추가된 Worker 안의 동기 접근 핸들이 OPFS 파일에만 붙으면서, WASM 기반 앱이 기대하는 동기 파일 I/O 모델을 브라우저에서 맞출 수 있게 됐다.

  • 용량 제한이 있다: IndexedDB처럼 브라우저 스토리지 쿼터를 따르며, navigator.storage.estimate()로 사용량을 확인할 수 있다.
  • 사이트 데이터를 지우면 삭제된다.
  • 사용자가 파일 경로를 찾을 수 없다.
  • 동기식 API를 제공한다: File System API의 다른 메서드는 전부 async지만, OPFS 파일은 FileSystemSyncAccessHandle로 동기 접근이 가능하다. 단, Dedicated Web Worker 안에서만 쓸 수 있다.

NOTE

OPFS는 origin 단위로 격리된 브라우저 내부 전용 파일 시스템이다. 사용자에게 보이는 파일 시스템과 달리 보안 검사와 권한 요청 없이 접근할 수 있어 빠르고, Dedicated Web Worker 환경에서는 동기식 파일 접근도 가능하다.

3. 세 가지 개념

OPFS(Origin Private File System), Web Worker, FileSystemSyncAccessHandle은 각자 다른 관점의 개념이다.

3.1. OPFS(Origin Private File System) — 어디에 저장하는가

OPFS는 브라우저가 관리하는 샌드박스 파일 시스템이다. origin마다 독립된 저장 공간을 가지며 파일 탐색기에는 보이지 않는다. 접근 방법은 두 가지로, 메인 스레드와 Worker 모두 가능한 async API와 Worker 안에서만 가능한 FileSystemSyncAccessHandle이 있다.

JavaScript
// async API로 OPFS 접근 (메인 스레드에서도 가능)
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('data.bin', { create: true });
 
// 쓰기
const writable = await fileHandle.createWritable();
await writable.write('hello');
await writable.close();
 
// 읽기
const file = await fileHandle.getFile();
const text = await file.text();

TIP

OPFS는 "어디에 저장하느냐"의 문제다. origin 단위로 격리된 브라우저 내부 파일 시스템이며, 접근은 async API(메인/Worker)와 FileSystemSyncAccessHandle(Worker 전용) 두 가지다.

3.2. Web Worker — 누가 실행하는가

자바스크립트는 기본적으로 싱글 스레드다. 모든 코드가 메인 스레드에서 실행되고 이 스레드가 UI 렌더링과 이벤트 처리도 담당하다 보니, 무거운 연산이나 블로킹 I/O를 메인 스레드에서 돌리면 화면이 멈춘다. Web Worker는 메인 스레드와 분리된 별도 스레드를 만들고, DOM에 직접 접근하지 않는 대신 postMessage() 기반 메시지로 통신한다.

JavaScript
// 메인 스레드
const worker = new Worker('worker.js');
worker.postMessage({ type: 'start' });
worker.onmessage = (e) => console.log('결과:', e.data);
 
// worker.js
self.onmessage = async (e) => {
  // 여기서 블로킹 작업 수행
  self.postMessage({ result: '완료' });
};

TIP

Web Worker는 "누가 실행하느냐"의 문제다. 메인 스레드 블로킹을 막기 위해 OPFS 동기 접근은 Worker 안에서만 허용된다.

3.3. FileSystemSyncAccessHandle — 어떻게 읽고 쓰는가

FileSystemSyncAccessHandle은 OPFS 파일 접근 방법 중 하나로, 이름 그대로 **동기식(Sync)**으로 읽고 쓴다. read, write, getSize, flush, truncate, close 같은 메서드가 전부 동기식으로 동작한다. 초기 스펙에서는 close, flush, getSize, truncate가 비동기로 잘못 명세돼 구버전 일부 브라우저가 그렇게 구현했지만, 현재 지원 브라우저는 모두 동기식이다. 단, 이 핸들은 OPFS 파일에만 생성할 수 있고 Dedicated Web Worker 안에서만 접근할 수 있다.

JavaScript
// Worker 안에서
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('fast.bin', { create: true });
 
// 핸들 생성은 async (한 번만)
const accessHandle = await fileHandle.createSyncAccessHandle();
 
// 이 아래는 전부 동기 — await 없음
const encoder = new TextEncoder();
const buf = encoder.encode('Some data');
 
accessHandle.write(buf, { at: 0 });  // 동기 쓰기
accessHandle.flush();                // 디스크에 반영
const size = accessHandle.getSize(); // 동기 크기 조회
const view = new DataView(new ArrayBuffer(size));
accessHandle.read(view, { at: 0 });  // 동기 읽기
accessHandle.close();

read()/write()는 블로킹이라 메인 스레드를 멈출 수 있어, 스펙상 Dedicated Web Worker 안에서만 사용할 수 있다. createSyncAccessHandle() 메서드 자체는 Promise를 반환하는 비동기 호출이지만, 반환된 핸들의 파일 작업 메서드는 동기식이다. 기본 모드(readwrite)에서는 파일당 핸들을 하나만 열 수 있는 독점 잠금이라, 다 쓴 핸들은 close()로 닫아야 한다.

WARNING

FileSystemSyncAccessHandle은 스펙상 Dedicated Web Worker 안에서만 쓸 수 있다. 메인 스레드에서 createSyncAccessHandle()을 호출하면 에러가 난다.

4. 세 개념이 맞물리는 구조

각자 다른 문제를 풀지만 "브라우저에서 동기식 파일 I/O 구현"이라는 목표에는 셋이 동시에 필요하다.

해결해야 할 문제담당 개념
권한 요청 없이 빠르게 파일을 영속 저장OPFS
동기식 read / write 구현FileSystemSyncAccessHandle
블로킹 호출이 UI 를 멈추지 않게Web Worker

하나라도 빠지면 안 된다.

  • OPFS 대신 File System Access API → 사용자 로컬 파일 접근은 되지만, 보안 검사 때문에 느리고 권한 요청이 필요하다. (목적이 다름: File System Access API=사용자 파일, OPFS=브라우저 내부 전용)
  • FileSystemSyncAccessHandle 없이 async만 → 동기식 f.read() 패턴을 만들 수 없다.
  • Worker 없이 메인 스레드 → 블로킹 호출이 UI를 멈춘다.

Sync I/O 실행 흐름 다이어그램

TIP

표에서 보듯 셋은 겹치는 역할이 없다. 하나만 빠져도 나머지 둘로는 브라우저 동기식 파일 I/O를 완성할 수 없다.

5. OPFS 접근 API 비교

API동기 여부OPFS 전용읽기/쓰기사용 가능 컨텍스트
getFile()async읽기만메인 스레드 / Worker
createWritable()async쓰기만메인 스레드 / Worker
createSyncAccessHandle()생성은 async, 핸들 조작은 sync✅ OPFS만읽기 + 쓰기Dedicated Worker 전용

6. 마무리

처음 Pyodide의 브라우저 파일 I/O 맥락을 읽을 땐 세 개념이 뭉쳐 하나처럼 느껴졌지만, 관점별로 분리하면 명확해진다. OPFS는 어디에 저장할지를, Web Worker는 누가 실행할지를, FileSystemSyncAccessHandle은 어떻게 읽고 쓸지를 각각 담당한다.

TIP

Pyodide 코드가 이 셋을 모두 쓰는 이유가 여기에 있다. 셋 중 하나라도 빠지면 브라우저에서 동기식 파일 I/O는 성립하지 않는다.

7. 참고문헌