🔒 보안 컨텍스트(Secure context): 이 기능은 오직 보안 컨텍스트(HTTPS)에서만 사용할 수 있어요. 일부 또는 모든 지원 브라우저에서요.
📝 참고: 이 기능은 Web Workers에서도 사용할 수 있어요.
File System API는 — File System Access API를 통해 제공되는 확장 기능과 함께 디바이스의 파일 시스템에 있는 파일들에 접근할 수 있게 해주는데요 — 파일을 읽고, 쓰고, 관리하는 기능들을 제공해요.
이 API와 File and Directory Entries API, 그리고 File API 간의 비교는 다른 파일 관련 API들과의 관계를 참고하세요.
여러분, File System API를 처음 접하시면 "어? 브라우저에서 진짜 내 컴퓨터 파일에 접근한다고?" 하고 놀라실 수 있어요. 맞아요! 예전에는 웹 브라우저에서 로컬 파일 시스템에 직접 접근하는 건 보안상 거의 불가능했거든요.
실무에서 이 API를 활용하면 다음과 같은 걸 만들 수 있어요:
⚠️ 주의할 점: "Secure context"라고 되어 있죠? 이건 반드시 HTTPS 환경에서만 동작한다는 뜻이에요. 로컬 개발할 때는 localhost가 보안 컨텍스트로 인정되니까 걱정 안 하셔도 되지만, 실제 배포할 때는 HTTPS가 필수입니다!
그리고 문서에서 언급된 세 가지 API가 헷갈리실 수 있는데요:
<input type="file">로 선택한 파일을 읽는 기본적인 API이 API는 사용자의 로컬 기기나 사용자가 접근할 수 있는 네트워크 파일 시스템에 있는 파일들과 상호작용할 수 있게 해줘요. 이 API의 핵심 기능에는 파일 읽기, 파일 쓰기 또는 저장하기, 그리고 디렉토리(폴더) 구조에 접근하는 것이 포함되어 있습니다.
파일과 디렉토리에 대한 대부분의 상호작용은 '핸들(handles)'을 통해 이루어집니다. 부모 클래스인 FileSystemHandle은 파일과 디렉토리를 각각 다루기 위한 두 개의 자식 클래스인 FileSystemFileHandle과 FileSystemDirectoryHandle을 정의하는 데 도움을 주죠.
💡 강사의 팁: 여기서 '핸들(Handle)'이라는 개념이 생소할 수 있는데요, 간단히 말해 운영체제의 파일이나 폴더를 자바스크립트 코드에서 제어할 수 있도록 브라우저가 쥐어주는 '리모컨'이나 '포인터'라고 생각하시면 이해하기 쉽습니다!
이 핸들들은 사용자 시스템의 특정 파일이나 디렉토리를 나타냅니다. 먼저 window.showOpenFilePicker()나 window.showDirectoryPicker() 같은 메서드를 사용해서 사용자에게 파일이나 디렉토리 선택기(picker)를 보여줌으로써 이 핸들에 대한 접근 권한을 얻을 수 있어요. 이 메서드들이 호출되면 파일 선택창이 나타나고 사용자가 파일이나 디렉토리를 선택하게 됩니다. 이 과정이 성공적으로 끝나면 비로소 핸들이 반환되는 것이죠.
또한 다음과 같은 방법을 통해서도 파일 핸들에 접근할 수 있어요:
💡 강사의 팁: 드래그 앤 드롭 방식을 지원하는 파일 업로더를 만들 때
DataTransferItem.getAsFileSystemHandle()가 정말 유용하게 쓰입니다.
각 핸들은 고유한 기능을 제공하며, 어떤 핸들을 사용하느냐에 따라 몇 가지 차이점이 있습니다 (자세한 내용은 인터페이스(Interfaces) 섹션을 확인해 주세요). 핸들을 얻고 나면 파일 데이터나 선택한 디렉토리의 정보(하위 요소 포함)에 접근할 수 있게 됩니다. 이 API는 그동안 웹 환경에 부족했던 아주 강력한 잠재적 기능들을 열어주었어요. 하지만, API를 설계할 때 보안이 가장 중요한 문제였기 때문에 사용자가 명시적으로 허용하지 않는 한 파일/디렉토리 데이터에 대한 접근은 엄격히 차단됩니다. (단, 아래에서 설명할 오리진 프라이빗 파일 시스템(Origin private file system)의 경우는 사용자에게 보이지 않는 독립된 공간이므로 예외입니다.)
참고: 이 API의 기능들을 사용할 때 발생할 수 있는 다양한 예외(Exceptions)들은 스펙에 정의된 대로 관련 페이지들에 나열되어 있어요. 하지만 이 API와 기본 운영체제 간의 상호작용 때문에 에러 상황이 꽤 복잡해집니다. 유용한 관련 정보를 포함하여 스펙에 에러 매핑을 나열하자는 제안이 현재 논의 중인 상태입니다.
참고:
FileSystemHandle을 기반으로 하는 객체들은IndexedDB데이터베이스 인스턴스로 직렬화(serialize)되거나,postMessage()를 통해 전송될 수도 있습니다.
오리진 프라이빗 파일 시스템(OPFS)은 File System API의 일부로 제공되는 스토리지 엔드포인트예요. 페이지의 출처(origin)에 비공개로 유지되며, 일반 파일 시스템처럼 사용자 눈에 보이지 않습니다. 성능에 매우 최적화되어 있고 파일 내용에 대한 제자리(in-place) 쓰기 권한을 제공하는 특별한 종류의 파일에 접근할 수 있게 해줍니다.
다음에 OPFS를 활용할 수 있는 몇 가지 훌륭한 사용 사례를 소개해 드릴게요:
사용 방법에 대한 자세한 지침은 오리진 프라이빗 파일 시스템(Origin private file system) 문서를 읽어보세요.
FileSystemWritableFileStream 인터페이스를 사용합니다. 저장하고자 하는 데이터가 Blob, String 객체, 문자열 리터럴, 또는 buffer 형식이라면, 스트림을 열고 데이터를 파일에 저장할 수 있어요. 기존 파일에 덮어쓸 수도 있고 완전히 새로운 파일에 저장할 수도 있습니다.FileSystemSyncAccessHandle의 경우, write() 메서드를 사용하여 파일에 변경 사항을 기록합니다. 만약 특정 시점에 변경 사항을 디스크에 즉각적으로 커밋(저장)해야 한다면 선택적으로 flush() 메서드를 호출할 수도 있어요 (그렇지 않으면 기본 운영 체제가 적절하다고 판단할 때 알아서 처리하도록 놔둘 수 있는데, 대부분의 경우에는 이렇게 맡겨두는 것이 괜찮습니다).FileSystemChangeRecord 🧪(실험적 기능)
: FileSystemObserver가 관찰한 단일 변경 사항에 대한 세부 정보를 포함합니다.
FileSystemHandle
: 파일이나 디렉토리 항목을 나타내는 객체입니다. 여러 개의 핸들이 같은 항목을 나타낼 수도 있어요. 대부분의 경우 FileSystemHandle을 직접 다루기보다는 이것의 자식 인터페이스인 FileSystemFileHandle과 FileSystemDirectoryHandle을 사용하게 됩니다.
FileSystemFileHandle
: 파일 시스템 내의 단일 파일 항목에 대한 핸들을 제공합니다.
FileSystemDirectoryHandle
: 파일 시스템 내의 디렉토리(폴더)에 대한 핸들을 제공합니다.
FileSystemObserver 🧪(실험적 기능)
: 선택한 파일이나 디렉토리에 어떤 변경이 일어나는지 관찰(observe)할 수 있는 메커니즘을 제공합니다.
FileSystemSyncAccessHandle
: 파일 시스템 항목에 대한 동기식 핸들을 제공하며, 디스크에 있는 단일 파일에 대해 제자리(in-place) 작업을 수행합니다. 파일 읽기 및 쓰기를 동기식으로 처리하면 비동기 작업의 오버헤드가 큰 환경(예: WebAssembly)에서 핵심 메서드들의 성능을 크게 끌어올릴 수 있어요. 이 클래스는 오리진 프라이빗 파일 시스템(Origin private file system) 내의 파일들을 다루기 위해 전용 웹 워커(Web Workers) 내부에서만 접근이 가능합니다.
FileSystemWritableFileStream
: 디스크에 있는 단일 파일에 대해 작업을 수행하며, 개발자가 쓰기 편하도록 추가적인 편의 메서드를 갖춘 WritableStream 객체입니다.
Window.showDirectoryPicker()
: 사용자가 디렉토리를 선택할 수 있는 디렉토리 선택기 화면을 띄웁니다.
Window.showOpenFilePicker()
: 사용자가 하나 또는 여러 개의 파일을 선택할 수 있는 파일 선택기를 띄웁니다.
Window.showSaveFilePicker()
: 사용자가 파일을 저장할 수 있도록 파일 저장 선택기를 띄웁니다.
DataTransferItem.getAsFileSystemHandle()
: 드래그 앤 드롭한 항목이 파일인 경우 FileSystemFileHandle로 이행(fulfill)되거나, 디렉토리인 경우 FileSystemDirectoryHandle로 이행되는 Promise를 반환합니다.
StorageManager.getDirectory()
: 오리진 프라이빗 파일 시스템(Origin private file system)에 저장된 디렉토리와 그 내용물에 접근할 수 있게 해주는 FileSystemDirectoryHandle 객체의 참조를 얻는 데 사용됩니다. FileSystemDirectoryHandle 객체로 이행되는 Promise를 반환합니다.
아래 코드는 사용자가 파일 선택기에서 파일을 선택할 수 있게 해주는 예제입니다.
async function getFile() {
// 파일 선택기를 열고 배열 비구조화 할당을 통해 첫 번째 핸들을 가져옵니다
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
return file;
}
다음 비동기(asynchronous) 함수는 파일 선택기를 띄우고, 파일이 선택되면 getFile() 메서드를 사용해서 파일의 내용을 가져옵니다.
💡 강사의 팁: 프론트엔드 포트폴리오 사이트를 구축하실 때 이런 파일 선택기 로직을 커스텀 훅(Hook)으로 만들어두면 재사용성도 높고 코드도 훨씬 깔끔해집니다. 아래 코드의
pickerOpts객체처럼 옵션을 주어 특정 확장자(이미지 등)만 필터링하는 방법은 실무에서도 빈번하게 쓰이니 눈여겨보세요!
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
async function getTheFile() {
// 파일 선택기를 열고 첫 번째 핸들을 가져옵니다
const [fileHandle] = await window.showOpenFilePicker(pickerOpts);
// 파일의 내용을 가져옵니다
const fileData = await fileHandle.getFile();
}
다음 예제는 지정된 이름을 가진 디렉토리 핸들을 반환합니다. 만약 해당 디렉토리가 없다면 새로 만들어줍니다.
const dirName = "directoryToGetName";
// 'currentDirHandle'이라는 디렉토리 핸들을 가지고 있다고 가정합니다
const subDir = await currentDirHandle.getDirectoryHandle(dirName, {
create: true,
});
다음 비동기 함수는 특정 디렉토리 핸들을 기준으로, 선택한 파일의 상대 경로를 찾기 위해 resolve() 메서드를 사용합니다.
async function returnPathDirectories(directoryHandle) {
// 파일 선택기를 보여주고 파일 핸들을 얻습니다:
const [handle] = await self.showOpenFilePicker();
if (!handle) {
// 사용자가 취소했거나 파일을 여는 데 실패한 경우.
return;
}
// 핸들이 우리의 디렉토리 핸들 내부에 존재하는지 확인합니다
const relativePaths = await directoryHandle.resolve(handle);
if (relativePaths === null) {
// 디렉토리 핸들 내부에 없는 경우
} else {
// relativePaths는 상대 경로를 나타내는 이름들의 배열입니다
for (const name of relativePaths) {
// 각 항목을 콘솔에 출력합니다
console.log(name);
}
}
}
다음 비동기 함수는 파일 저장 선택기(save file picker)를 엽니다. 사용자가 파일을 선택하면 FileSystemFileHandle을 반환하죠. 그런 다음 FileSystemFileHandle.createWritable() 메서드를 사용해서 쓰기 가능한 스트림(writable stream)을 생성합니다.
그 후, 사용자가 정의한 Blob 데이터를 스트림에 쓰고 나서 정상적으로 스트림을 닫아줍니다.
async function saveFile() {
// 새로운 핸들을 생성합니다
const newHandle = await window.showSaveFilePicker();
// 데이터를 쓰기 위한 FileSystemWritableFileStream을 생성합니다
const writableStream = await newHandle.createWritable();
// 파일에 데이터를 씁니다
await writableStream.write(imgBlob);
// 파일을 닫고 내용을 디스크에 기록합니다.
await writableStream.close();
}
다음 코드는 write() 메서드에 전달할 수 있는 다양한 옵션 예제들을 보여줍니다.
// 단순히 데이터만 전달합니다 (옵션 없음)
writableStream.write(data);
// 결정된 위치에서부터 스트림에 데이터를 씁니다
writableStream.write({ type: "write", position, data });
// 현재 파일 커서의 오프셋을 지정된 위치로 업데이트합니다
writableStream.write({ type: "seek", position });
// 파일의 크기를 지정한 바이트 길이로 조정합니다
writableStream.write({ type: "truncate", size });
이 예제는 오리진 프라이빗 파일 시스템(Origin private file system)에 있는 파일을 동기식으로 읽고 쓰는 방법을 보여줍니다.
다음의 비동기 이벤트 핸들러 함수는 웹 워커(Web Worker) 내부에 작성되어 있어요. 메인 스레드로부터 메시지를 받으면 다음과 같은 작업들을 순서대로 수행합니다:
ArrayBuffer를 생성합니다.onmessage = async (e) => {
// 메인 스크립트에서 워커로 보낸 메시지를 가져옵니다
const message = e.data;
// OPFS에 있는 draft 파일에 대한 핸들을 가져옵니다
const root = await navigator.storage.getDirectory();
const draftHandle = await root.getFileHandle("draft.txt", { create: true });
// 동기식 접근 핸들(sync access handle)을 가져옵니다
const accessHandle = await draftHandle.createSyncAccessHandle();
// 파일의 크기를 가져옵니다.
const fileSize = accessHandle.getSize();
// 파일 내용을 버퍼로 읽어옵니다.
const buffer = new DataView(new ArrayBuffer(fileSize));
const readBuffer = accessHandle.read(buffer, { at: 0 });
// 인코딩된 메시지를 파일의 끝 부분에 씁니다.
const encoder = new TextEncoder();
const encodedMessage = encoder.encode(message);
const writeBuffer = accessHandle.write(encodedMessage, { at: readBuffer });
// 변경 사항을 디스크에 영구 저장합니다.
accessHandle.flush();
// 작업이 끝났다면 FileSystemSyncAccessHandle을 반드시 닫아주세요.
accessHandle.close();
};
참고: 스펙의 초기 버전에서는
close(),flush(),getSize(),truncate()메서드들이 비동기(asynchronous) 방식으로 지정되어 있어서 사용하기가 꽤 불편했습니다. 지금은 이 부분이 수정되어 동기식으로 사용할 수 있지만, 일부 구형 브라우저 환경에서는 여전히 비동기 버전을 지원하고 있으니 호환성 체크를 꼭 해주세요.
안녕하세요 예비 프론트엔드 개발자 여러분! 오늘 함께 파헤쳐볼 MDN 공식 문서는 바로 'Origin private file system (출처 비공개 파일 시스템, 이하 OPFS)'입니다.
문서가 영어로 되어 있어서 조금 막막하셨죠? 걱정 마세요! 제가 이해하기 쉽게, 우리 평소 대화하는 것처럼 풀어서 설명해 드릴게요. 게다가 실무에서 어떻게 쓰이는지 제 경험을 담은 꿀팁과 부연 설명도 듬뿍 담아봤습니다. 자, 그럼 시작해 볼까요?
✅ 기준(Baseline): 널리 사용 가능함 (Widely available)
이 기능은 이미 잘 정착되어 수많은 기기와 브라우저 버전에서 원활하게 작동해요. 2023년 3월부터 거의 모든 주요 브라우저에서 사용할 수 있게 되었습니다.
🔒 안전한 컨텍스트 (Secure context)
이 기능은 데이터를 다루는 만큼 보안이 중요해요. 그래서 안전한 컨텍스트 (HTTPS 환경)에서만 사용할 수 있습니다. 로컬에서 개발하실 때는localhost를 쓰시면 작동하니 너무 걱정 마세요!
⚙️ 참고 (Note)
이 기능은 웹 워커 (Web Workers) 내부에서도 사용할 수 있습니다. (이게 왜 엄청난 장점인지는 뒤에서 자세히 설명해 드릴게요!)
출처 비공개 파일 시스템(OPFS)은 File System API (파일 시스템 API)의 일부로 제공되는 저장소 엔드포인트입니다. 이름에서 알 수 있듯, 페이지의 '출처(Origin)'에 종속되어 있어서 외부나 사용자에게는 일반 파일 시스템처럼 노출되지 않는 프라이빗한 공간이죠.
OPFS는 특별한 종류의 파일에 접근할 수 있게 해 주는데요, 이 파일들은 성능이 극도로 최적화되어 있고 파일 내용에 대한 '제자리 쓰기(in-place write, 임시 파일 없이 원본에 바로 덮어쓰기)' 권한을 제공한답니다.
(Working with files using the File System Access API)
본격적으로 OPFS를 알아보기 전에, 기존 방식은 어땠는지 살펴볼까요? File System Access API는 파일 시스템 API를 확장해서 '피커(picker, 선택창)' 메서드를 통해 파일에 접근할 수 있게 해 줍니다. 예를 들면 이렇죠:
Window.showOpenFilePicker()를 호출하면 사용자에게 파일을 선택할 수 있는 창이 뜹니다. 사용자가 파일을 고르면 FileSystemFileHandle 객체가 반환돼요.FileSystemFileHandle.getFile()을 호출합니다. 내용을 수정할 때는 FileSystemFileHandle.createWritable() 과 FileSystemWritableFileStream.write()를 사용하고요.FileSystemHandle.requestPermission({mode: 'readwrite'})를 쓰죠.이 방식도 물론 잘 작동합니다. 하지만 꽤 번거로운 제약들이 있어요. 사용자가 직접 볼 수 있는 실제 파일 시스템을 건드리는 작업이다 보니, 악의적인 코드가 사용자 컴퓨터에 심어지는 걸 막기 위해 엄청나게 많은 보안 검사(예: 크롬의 세이프 브라우징(Safe browsing))가 뒤따릅니다. 게다가 원본 파일에 바로 쓰는 게 아니라 임시 파일을 만들어서 작업해요. 모든 보안 검사를 무사히 통과해야만 비로소 원본 파일이 수정되는 구조죠.
결과적으로 이런 작업들은 꽤 느립니다. 단순한 텍스트 몇 줄 바꾸는 거라면 체감하기 힘들겠지만, SQLite 데이터베이스 수정처럼 거대하고 잦은 대규모 파일 업데이트를 진행할 때는 성능 저하가 뼈저리게 느껴질 거예요.
💡 강사님의 실무 팁!
"사용자에게 매번 '이 파일 수정해도 됩니까?' 하고 묻는 알림창이 뜨면 UX(사용자 경험) 측면에서 매우 귀찮고 흐름이 끊기게 됩니다. 보안을 위해 꼭 필요한 절차지만, 앱 내부적으로 임시 캐시 파일이나 백그라운드 데이터를 저장해야 할 때는 전혀 어울리지 않는 방식이죠. 이럴 때 바로 OPFS가 구원투수로 등장하는 겁니다!"
(How does the OPFS solve such problems?)
OPFS는 사용자 눈에 띄지 않으면서 해당 출처(Origin, 예: 우리 웹사이트 도메인)에만 프라이빗하게 존재하는 아주 로우레벨(low-level)의 바이트 단위 파일 접근을 제공합니다.
사용자의 진짜 폴더를 건드리는 게 아니기 때문에, 앞서 말한 복잡한 보안 검사나 권한 승인 절차를 전부 건너뛸 수 있어요! 당연히 기존 File System Access API보다 훨씬 빠릅니다. 게다가 웹 워커(Web workers) 내부에서만 실행할 수 있는 '동기식(synchronous)' 호출 기능도 제공해서, 메인 스레드(우리가 보는 웹페이지 화면의 동작을 담당)가 멈추는 일 없이 쾌적하게 데이터를 다룰 수 있답니다.
OPFS와 사용자가 볼 수 있는 일반 파일 시스템의 차이를 요약해 드릴게요:
navigator.storage.estimate()를 통해 확인할 수 있어요.💡 강사님의 부연 설명!
"프론트엔드에서 데이터를 저장할 때 흔히localStorage나IndexedDB를 떠올리시죠? 텍스트 데이터는 거기로 충분하지만, 고화질 이미지 리사이징 결과물이나 오디오 파형 데이터, 방대한 앱 내부 캐시 같은 수백 MB 단위의 바이너리(바이트 단위) 데이터를 다뤄야 할 때는IndexedDB도 버벅거릴 수 있습니다. 이럴 때 파일 입출력에 특화된 OPFS를 사용하면 웹 앱의 성능을 네이티브 앱 수준으로 끌어올릴 수 있습니다."
(How do you access the OPFS?)
OPFS의 세계로 들어가는 첫걸음은 아주 간단합니다. navigator.storage.getDirectory() 메서드를 호출하기만 하면 돼요. 이 코드를 실행하면 OPFS의 최상위 루트 디렉터리를 나타내는 FileSystemDirectoryHandle 객체의 참조값을 돌려받게 됩니다.
(Manipulating the OPFS from the main thread)
일반적인 자바스크립트 환경(메인 스레드)에서 OPFS에 접근할 때는 비동기 처리 방식인 Promise 기반의 API를 사용해야 합니다.
루트를 나타내는 FileSystemDirectoryHandle 객체에서 getFileHandle()을 호출하면 파일 핸들을, getDirectoryHandle()을 호출하면 하위 디렉터리 핸들을 얻을 수 있습니다.
⚙️ 참고 (Note)
위 메서드들을 호출할 때 옵션으로{ create: true }를 넘겨주면, 만약 해당 이름의 파일이나 폴더가 없을 때 알아서 새로 생성해 줍니다. 정말 편리하죠?
// Create a hierarchy of files and folders
const fileHandle = await opfsRoot.getFileHandle("my first file", {
create: true,
});
const directoryHandle = await opfsRoot.getDirectoryHandle("my first folder", {
create: true,
});
const nestedFileHandle = await directoryHandle.getFileHandle(
"my first nested file",
{ create: true },
);
const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(
"my first nested folder",
{ create: true },
);
// Access existing files and folders via their names
const existingFileHandle = await opfsRoot.getFileHandle("my first file");
const existingDirectoryHandle =
await opfsRoot.getDirectoryHandle("my first folder");
FileSystemDirectoryHandle.getFileHandle()을 호출해서 FileSystemFileHandle 객체를 얻습니다.FileSystemFileHandle.getFile()을 호출하면 진짜 우리가 아는 File 객체가 반환됩니다. 이 File 객체는 Blob의 특수한 형태라서 일반적인 Blob을 다루듯 마음대로 조작할 수 있어요. 예를 들어 텍스트 내용을 바로 뽑아내고 싶다면 Blob.text()를 쓰면 됩니다.getFileHandle()로 FileSystemFileHandle을 가져옵니다.FileSystemFileHandle.createWritable()을 호출해서 FileSystemWritableFileStream 객체를 반환받습니다. (이건 WritableStream의 한 종류예요!)FileSystemWritableFileStream.write()를 호출해 스트림에 원하는 내용을 씁니다.WritableStream.close()를 호출해서 스트림을 닫아주세요! (파일을 다 썼으면 꼭 닫아주는 게 개발자의 기본 소양입니다 ㅎㅎ)지우고 싶은 아이템이 들어있는 부모 디렉터리에서 FileSystemDirectoryHandle.removeEntry()를 호출하고, 지울 파일의 이름을 넘겨주면 됩니다:
directoryHandle.removeEntry("my first nested file");
또는, 삭제하고 싶은 대상 자체(파일 핸들이나 디렉터리 핸들)에 대고 직접 FileSystemHandle.remove()를 호출해도 됩니다. 폴더 안에 있는 모든 파일과 하위 폴더까지 싹 다 날려버리려면 { recursive: true } 옵션을 넣어주세요.
await fileHandle.remove();
await directoryHandle.remove({ recursive: true });
만약 OPFS 전체를 한 번에 초기화(초기화)하고 싶다면 아래 코드를 한 줄이면 됩니다:
await (await navigator.storage.getDirectory()).remove({ recursive: true });
FileSystemDirectoryHandle은 비동기 이터레이터 (asynchronous iterator)라는 특성을 가지고 있어요. 말이 조금 어렵죠? 쉽게 말해 for await...of 반복문을 써서 폴더 안의 내용물을 하나씩 꺼내볼 수 있다는 뜻입니다. entries(), values(), keys() 같은 표준 메서드도 쓸 수 있고요.
예를 들어 볼까요?
for await (let [name, handle] of directoryHandle) {
}
for await (let [name, handle] of directoryHandle.entries()) {
}
for await (let handle of directoryHandle.values()) {
}
for await (let name of directoryHandle.keys()) {
}
(Manipulating the OPFS from a web worker)
💡 강사님의 핵심 포인트!
"여기가 진짜 중요합니다! 메인 스레드에서 무거운 파일 쓰기 작업을 동기식(순차적으로 코드가 끝날 때까지 멈춰있는 방식)으로 처리하면 어떻게 될까요? 사용자가 버튼을 눌러도 웹페이지가 얼어버려서 아무 반응도 하지 않게 됩니다. 하지만 웹 워커(Web Workers)는 메인 스레드와 별개의 공간(백그라운드)에서 돌아가기 때문에 브라우저 화면을 멈추게 하지 않죠. 그래서 브라우저 개발자들은 가장 빠르고 효율적인 '동기식 API'를 오직 웹 워커 안에서만 쓸 수 있게 허락해 두었습니다."
웹 워커에서는 메인 스레드를 블로킹할 걱정이 없기 때문에 동기식 파일 접근 API를 마음껏 쓸 수 있습니다. 동기식 API는 비동기식(Promise 방식)이 가진 약간의 오버헤드를 피할 수 있어서 성능 면에서 압도적으로 더 빠릅니다.
일반적인 FileSystemFileHandle에서 FileSystemFileHandle.createSyncAccessHandle()을 호출하면 파일에 동기식으로 접근할 수 있는 핸들을 얻을 수 있습니다.
⚙️ 참고 (Note)
이름에 "Sync(동기)"라는 말이 들어있긴 하지만,createSyncAccessHandle()메서드를 호출해서 가져오는 행위 자체는 여전히 비동기(await필요)라는 점을 주의하세요!
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("my-high-speed-file.txt", {
create: true,
});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
반환된 FileSystemSyncAccessHandle 객체에는 놀랍도록 빠른 동기식 메서드들이 준비되어 있습니다:
getSize(): 파일의 크기를 바이트 단위로 알려줍니다.write(): 버퍼에 담긴 내용을 파일에 기록합니다. 원하는 오프셋(위치)을 지정할 수도 있고, 최종적으로 성공하게 된 바이트 수를 반환합니다. 이 반환값을 확인해서 파일이 쓰이다가 말았는지(에러 체크)를 안전하게 다룰 수 있어요.read(): 파일의 내용을 버퍼로 읽어 들입니다. 이것도 원하는 위치부터 읽을 수 있어요.truncate(): 파일을 주어진 크기로 자르거나 늘립니다. (리사이징)flush(): write()로 수정한 내용들이 확실하게 물리적인 파일 시스템에 적용(반영)되도록 보장합니다.close(): 다 썼으면 접근 핸들을 닫아줍니다. (꼭 해주세요!)이 모든 메서드를 활용해 엄청난 속도로 파일을 다루는 예제 코드를 살펴봅시다:
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("fast", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode("Some text");
// Write the content at the beginning of the file.
accessHandle.write(content, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();
// Encode more content to write to the file.
const moreContent = textEncoder.encode("More content");
// Write the content at the end of the file.
accessHandle.write(moreContent, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();
// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));
// Read the entire file into the data view.
accessHandle.read(dataView, { at: 0 });
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));
// Read starting at offset 9 into the data view.
accessHandle.read(dataView, { at: 9 });
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));
// Truncate the file after 4 bytes.
accessHandle.truncate(4);
프론트엔드 개발자라면 꼭 확인하고 넘어가야 할 브라우저 지원 현황입니다. getDirectory 기준 지원 시작 버전은 다음과 같습니다. 앞서 살펴봤듯 최신 브라우저에서는 완벽하게 지원됩니다!
| 브라우저 (Desktop) | 버전 지원 시작 | 브라우저 (Mobile) | 버전 지원 시작 |
|---|---|---|---|
| Chrome | 86+ ✅ | Chrome Android | 109+ ✅ |
| Edge | 86+ ✅ | Firefox for Android | 111+ ✅ |
| Firefox | 111+ ✅ | Opera Android | 74+ ✅ |
| Opera | 72+ ✅ | Safari on iOS | 15.2+ ✅ |
| Safari | 15.2+ ✅ | Samsung Internet | 21.0+ ✅ |
| WebView Android | 109+ ✅ |
조금 더 깊이 알아보고 싶다면 아래 링크도 참고해 보세요! 구글 개발자 블로그에서 정리한 훌륭한 아티클입니다.
이 페이지는 MDN 기여자들에 의해 2025년 7월 14일에 마지막으로 수정되었습니다.
어떠셨나요? 생각보다 어렵지 않죠? 브라우저 내에서 엄청나게 빠르고 무거운 데이터를 다루는 웹 애플리케이션(예: 웹 브라우저용 포토샵, 동영상 편집기, 3D 게임 등)을 만들 때 OPFS는 여러분의 강력한 무기가 될 것입니다. 배운 내용을 콘솔에 한 줄씩 쳐보면서 직접 체험해 보시길 추천드려요. 화이팅입니다! 🚀