---
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<FileSystemFileHandle>();
  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 <textarea value={content} onChange={(e) => setContent(e.target.value)} />;
}
```

### SQLite WASM + OPFS
```ts
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
const sqlite3 = await sqlite3InitModule();

const db = new sqlite3.oo1.OpfsDb('app.db');
db.exec('CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)');
db.exec('INSERT INTO users VALUES (1, "Alice")');
const r = db.exec('SELECT * FROM users', { rowMode: 'object' });
```

→ Local DB. Browser 만 (no server).

### 큰 file (Photo editor)
```ts
const [handle] = await window.showOpenFilePicker({
  types: [{ accept: { 'image/*': ['.png', '.jpg'] } }],
});
const file = await handle.getFile();   // 100MB
const arr = await file.arrayBuffer();   // memory load — careful
```

→ 큰 file 가 stream:
```ts
const stream = file.stream();
// process chunks
```

### Browser support
```
File System Access API:
- Chrome 86+
- Edge 86+
- Opera 72+
- Safari: showSaveFilePicker, showOpenFilePicker (16.4+, partial)
- Firefox: 안 (no plan)

OPFS:
- 모든 modern (Safari 16, FF 111, Chrome 102)
```

→ Fallback = `<input type="file">` + `URL.createObjectURL`.

### Fallback
```ts
async function openFile(): Promise<File> {
  if ('showOpenFilePicker' in window) {
    const [h] = await window.showOpenFilePicker();
    return h.getFile();
  }
  
  // Fallback
  const input = document.createElement('input');
  input.type = 'file';
  input.click();
  return new Promise((res) => input.onchange = () => res(input.files![0]));
}

async function saveFile(blob: Blob, name: string) {
  if ('showSaveFilePicker' in window) {
    const h = await window.showSaveFilePicker({ suggestedName: name });
    const w = await h.createWritable();
    await w.write(blob);
    await w.close();
    return;
  }
  
  // Fallback
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = name;
  a.click();
  URL.revokeObjectURL(a.href);
}
```

### Use case
- VS Code Web (Microsoft)
- Photoshop Web (Adobe)
- Figma (web)
- Google Docs (limited)
- IDE / editor (StackBlitz, CodeSandbox)
- DAW / audio editor (BandLab)

### Security
```
- HTTPS only
- User gesture 필수 (click 안 = call 안 됨)
- Domain-scoped (handle 가 다른 site 에서 사용 X)
- System dir block (e.g. /System on macOS)
```

### Permission persistence
```
Chrome: 같은 origin, 같은 file = persisted (refresh OK).
Browser close = revoked.

→ IndexedDB 가 handle 저장 → re-prompt.
```

## 🤔 의사결정 기준
| 작업 | 추천 |
|---|---|
| Native-like editor | File System Access API |
| 큰 local DB | OPFS + SQLite |
| 일반 download | Fallback (input + a.click) |
| Photo edit | File handle + stream |
| Cross-browser (FF) | Fallback |
| Cloud sync | Server upload |
| Drag & drop | DataTransferItem |

## ❌ 안티패턴
- **User gesture 없이 호출**: error.
- **Permission 매번 prompt**: persist.
- **큰 file 가 arrayBuffer**: OOM.
- **Safari / FF 가정**: fallback 없음.
- **OPFS 가 user-visible 가정**: sandboxed.
- **Sync access in main thread**: blocked.
- **Origin 변경 + handle**: invalid.

## 🤖 LLM 활용 힌트
- File System Access API 가 Chrome / Edge 만 — fallback.
- OPFS = 큰 sandboxed storage (SQLite WASM 친화).
- Handle = persistent reference (IndexedDB).
- Streaming = 큰 file.

## 🔗 관련 문서
- [[Frontend_Streams_API]]
- [[Web_PWA_Service_Worker]]
- [[DB_SQLite_Patterns]]
