--- id: web-file-system-access title: File System Access API — browser 가 file 직접 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [web, file-system, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Frontend"] } applied_in: [] aliases: [File System Access API, showOpenFilePicker, showSaveFilePicker, FileSystemHandle, OPFS] --- # File System Access API > Browser 가 user 의 file 을 read/write. **`showOpenFilePicker` / `showSaveFilePicker`**. Native app 비슷한 UX. Chrome / Edge (Safari / FF 부분). ## 📖 핵심 개념 - File handle: 영구 reference (permission 유지). - OPFS: Origin Private File System (sandboxed). - 사용자 명시 grant. - Streaming 가능. ## 💻 코드 패턴 ### File 열기 ```ts const [handle] = await window.showOpenFilePicker({ types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }], }); const file = await handle.getFile(); const text = await file.text(); ``` ### File 저장 ```ts const handle = await window.showSaveFilePicker({ suggestedName: 'export.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }], }); const writable = await handle.createWritable(); await writable.write(JSON.stringify(data)); await writable.close(); ``` → User 가 location / name 선택. ### Directory 열기 ```ts const dirHandle = await window.showDirectoryPicker(); for await (const [name, handle] of dirHandle.entries()) { if (handle.kind === 'file') { const file = await handle.getFile(); console.log(name, file.size); } else { // recursive } } ``` ### File 변경 모니터링 (poll) ```ts let lastModified = 0; setInterval(async () => { const file = await handle.getFile(); if (file.lastModified !== lastModified) { lastModified = file.lastModified; const newContent = await file.text(); onChange(newContent); } }, 1000); ``` → True file watch X — poll. ### Persistent handle (IndexedDB 저장) ```ts // Save import { openDB } from 'idb'; const db = await openDB('fileHandles', 1); await db.put('handles', handle, 'lastFile'); // Restore (next session) const handle = await db.get('handles', 'lastFile'); const perm = await handle.queryPermission({ mode: 'readwrite' }); if (perm !== 'granted') { await handle.requestPermission({ mode: 'readwrite' }); } ``` → User 가 같은 file 다시 open 안 함. ### OPFS (Origin Private File System) ```ts const opfs = await navigator.storage.getDirectory(); // Write const fileHandle = await opfs.getFileHandle('app.db', { create: true }); const writable = await fileHandle.createWritable(); await writable.write(new Uint8Array([...])); await writable.close(); // Read const file = await fileHandle.getFile(); const buf = await file.arrayBuffer(); ``` → Sandboxed (사용자 X). 큰 storage. SQLite WASM 가 사용. ### Sync access (worker only) ```ts // In Web Worker const opfs = await navigator.storage.getDirectory(); const handle = await opfs.getFileHandle('db.sqlite'); const sync = await handle.createSyncAccessHandle(); // Sync read / write const buf = new Uint8Array(1024); sync.read(buf, { at: 0 }); sync.write(new Uint8Array([1,2,3]), { at: 0 }); sync.flush(); sync.close(); ``` → Async overhead 없음. SQLite 식. ### Streaming write (큰 file) ```ts const writable = await handle.createWritable(); const ws = writable.getWriter(); for (const chunk of bigData) { await ws.ready; await ws.write(chunk); } await ws.close(); ``` → Memory efficient. ### File picker option ```ts await window.showOpenFilePicker({ multiple: true, types: [ { description: 'Images', accept: { 'image/*': ['.png', '.jpg'] } }, { description: 'PDF', accept: { 'application/pdf': ['.pdf'] } }, ], excludeAcceptAllOption: true, startIn: 'documents', // 'desktop' / 'downloads' / 'pictures' / 'music' / 'videos' }); ``` ### Permission ```ts async function ensureWrite(handle) { const opts = { mode: 'readwrite' }; if ((await handle.queryPermission(opts)) === 'granted') return true; if ((await handle.requestPermission(opts)) === 'granted') return true; return false; } ``` → User 가 매번 prompt. Persistent 가능. ### Drag & drop + handle ```ts window.addEventListener('drop', async (e) => { e.preventDefault(); for (const item of e.dataTransfer!.items) { const handle = await item.getAsFileSystemHandle(); if (handle.kind === 'file') { const file = await handle.getFile(); } else { // directory } } }); ``` ### Editor 가 file save ```tsx function Editor() { const [handle, setHandle] = useState(); const [content, setContent] = useState(''); const open = async () => { const [h] = await window.showOpenFilePicker(); setHandle(h); setContent(await (await h.getFile()).text()); }; const save = async () => { let h = handle; if (!h) h = await window.showSaveFilePicker(); const w = await h.createWritable(); await w.write(content); await w.close(); setHandle(h); }; // Cmd+S useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.metaKey && e.key === 's') { e.preventDefault(); save(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [save]); return