Sangcho.log
dev

OpenCut 탐구 6 — IndexedDB, OPFS, 그리고 에디터를 떠받치는 외부 서비스 지도

9 min read|

시리즈 마지막 편입니다. 지금까지 폴더 구조, 저장/불러오기, 매니저 아키텍처, Command 패턴, Web Audio 엔진을 봤습니다. 이번에는 시선을 프로젝트 바깥으로 돌립니다. OpenCut이 직접 만들지 않고 기대고 있는 것들 — 브라우저 저장소 두 가지와 외부 서비스들입니다.

2편에서 "메타데이터는 IndexedDB에, 파일은 OPFS에"라는 문장이 스쳐 지나갔는데, 이 분업이 왜 필요한지가 이번 편의 첫 질문입니다.

IndexedDB — 포스트잇이 아니라 서류 파일첩

쿠키나 LocalStorage가 "포스트잇" 수준의 메모라면, IndexedDB는 책상 위의 **본격적인 서류 파일첩(Database)**입니다.

  • 용량이 넉넉함 — LocalStorage는 보통 5MB 내외지만, IndexedDB는 디스크 여유에 따라 수백 MB~GB 단위까지
  • 비동기 동작 — 읽고 쓰는 동안 브라우저가 멈추지 않음
  • 구조화된 데이터 — 문자열만이 아니라 객체, 배열, Blob을 그대로 저장
  • 인덱스 지원 — 책의 색인처럼 특정 필드 기준 고속 검색

IndexedDB가 진가를 발휘하는 상황은 "데이터가 많고 구조가 복잡할 때"입니다. 오프라인 우선 앱의 로컬 데이터, 대량 API 응답 캐싱, 그리고 OpenCut처럼 프로젝트 → 씬 → 트랙 → 엘리먼트로 이어지는 계층 데이터의 보관이죠. 다만 API가 콜백 기반이라 다루기 까다로워 보통 idb 같은 래퍼를 쓰고, 사용자가 개발자 도구로 열어볼 수 있으니 민감 정보는 넣지 않는 것이 원칙입니다. (기본 개념은 예전에 별도 글로 정리했습니다.)

OPFS — 브라우저 안의 고성능 하드디스크

**OPFS(Origin Private File System)**는 브라우저가 제공하는 오리진 전용 가상 파일 시스템입니다. 웹은 보안상 로컬 파일 접근이 엄격히 막혀 있었는데, OPFS는 "사용자의 실제 폴더" 대신 해당 사이트만 접근 가능한 숨겨진 구역을 내주는 방식으로 성능과 프라이버시를 동시에 잡았습니다.

기존 저장소의 어떤 한계 때문에 등장했을까요?

  • 속도 — IndexedDB는 대용량 데이터에서 인덱싱 오버헤드가 붙습니다
  • 파일 구조의 부재 — 운영체제처럼 폴더/파일 단위로 다루는 게 직관적이지 않습니다
  • 저수준 성능 요구 — SQLite를 WASM으로 돌리는 시대에는 DB 파일을 초고속으로 읽고 쓸 수단이 필요합니다

OPFS의 강점은 in-place update — 파일의 특정 부분만 수정하고 고속 스트리밍이 가능하다는 점입니다. 특히 Web Worker에서 쓸 수 있는 FileSystemSyncAccessHandle은 동기 API라서 성능이 극대화됩니다. 오리진 격리 덕분에 파일 접근 권한 팝업도 필요 없고요(브라우저 데이터를 지우면 함께 삭제됩니다).

// OPFS의 루트 디렉토리에 접근
const opfsRoot = await navigator.storage.getDirectory();

// 파일 생성 및 핸들 가져오기
const fileHandle = await opfsRoot.getFileHandle('video_frame_1.bin', { create: true });

// 데이터 쓰기
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();

대표적인 활용처는 SQLite WASM의 기본 저장소, 대용량 영상 프레임 캐싱(캡컷 웹버전 같은 편집기), 로컬 AI 모델 가중치처럼 GB 단위 에셋 보관입니다. 한 줄로 요약하면 **"웹 브라우저 안에서 돌아가는 고성능 하드디스크"**입니다.

두 저장소의 분업 — 도서관 모델

OpenCut의 선택은 이렇습니다.

데이터 저장소 이유
프로젝트 구조, 씬/트랙, 미디어 메타데이터 IndexedDB 구조화된 조회·인덱싱이 필요
영상/오디오 파일 본체 OPFS 대용량 바이너리의 고속 읽기/쓰기

도서관으로 치면 IndexedDB가 검색 카드 목록, OPFS가 서가입니다. "어떤 책이 어디 있는지"는 카드로 찾고, 책 자체는 서가에서 꺼내는 거죠. 파일을 IndexedDB에 Blob으로 넣을 수도 있지만, 수백 MB 영상을 반복 재생·디코딩하는 에디터라면 파일 시스템 시맨틱과 스트리밍 성능을 가진 OPFS가 맞는 도구입니다. 대신 2편에서 봤듯, 두 저장소를 함께 갱신하는 경로의 원자성은 개발자의 몫으로 남습니다.

외부 서비스 전체 지도

브라우저 저장소 밖으로 나가면, OpenCut이 사용하는 외부 서비스와 패키지는 이렇게 정리됩니다.

카테고리 외부 서비스/패키지 주요 파일
인증 Better Auth lib/auth/{server,client}.ts, app/api/auth/[...all]/route.ts
DB Drizzle + postgres lib/db/{index,schema}.ts, drizzle.config.ts, migrations/
Redis Upstash lib/rate-limit.ts, lib/auth/server.ts
효과음 Freesound API app/api/sounds/search/route.ts
CMS/RSS Marble + unified/rehype + feed lib/blog/query.ts, app/rss.xml/route.ts
아이콘 Iconify (자체 Provider 레이어) lib/iconify-api.ts, lib/stickers/providers/*
폰트 Google Fonts + 자체 atlas lib/fonts/google-fonts.ts, scripts/generate-font-sprites.ts
비디오/오디오 mediabunny lib/media/*, services/renderer/scene-exporter.ts, services/video-cache/service.ts
파형 wavesurfer.js components/editor/panels/timeline/audio-waveform.tsx
ASR @huggingface/transformers (Whisper ONNX) services/transcription/{worker,service}.ts, lib/transcription/caption.ts
봇 차단 Vercel BotID app/layout.tsx
분석 Databuddy (CDN) app/layout.tsx
(env만) Cloudflare R2, Modal packages/env/src/{web,tools}.ts (실제 호출처 미존재)

이 표에서 읽히는 설계 방향이 있습니다.

  • 서버는 얇습니다. 서버가 하는 일은 인증(Better Auth + Drizzle + postgres), rate limit(Upstash), 효과음 검색 프록시(Freesound), 블로그/RSS 정도. 편집의 본체는 전부 클라이언트에 있습니다.
  • 무거운 연산도 브라우저 몫입니다. 음성→자막 변환조차 서버 API가 아니라 Whisper ONNX 모델을 @huggingface/transformersWeb Worker에서 돌립니다. 미디어 디코딩/인코딩도 WebCodecs 기반 mediabunny로 로컬 처리하고요.
  • 아직 안 쓰는 것도 보입니다. Cloudflare R2와 Modal은 env 정의만 있고 실제 호출처가 없습니다. 클라우드 저장·서버 렌더링 같은 미래 기능의 자리로 읽힙니다.

더 깊이: 브라우저 저장소는 영원하지 않다 — 영속성과 퇴거(eviction)

로컬 우선 아키텍처의 아킬레스건은 브라우저가 저장소를 지울 수 있다는 사실입니다. IndexedDB와 OPFS는 기본적으로 "best-effort" 모드라서, 디스크가 부족해지면 브라우저가 오래 방문하지 않은 오리진의 데이터부터 퇴거시킬 수 있습니다.

방어 수단이 두 가지 있습니다.

// 1. 남은 할당량 확인
const { usage, quota } = await navigator.storage.estimate();

// 2. 영속 저장소 승격 요청 — 승인되면 퇴거 대상에서 제외
const persisted = await navigator.storage.persist();

persist()가 항상 승인되는 것은 아니고(브라우저가 사이트 이용 빈도 등을 보고 판단), 승인되더라도 사용자가 직접 "사이트 데이터 삭제"를 누르면 지워집니다. 그래서 로컬 우선 에디터를 만든다면 **"로컬은 빠른 작업 공간, 진짜 백업은 내보내기 또는 클라우드 동기화"**라는 계약을 사용자에게 분명히 하는 것까지가 설계입니다. OpenCut의 env에 잠들어 있는 R2가 바로 그 다음 단계의 자리일 겁니다.


한 줄 요약 — OpenCut은 "카드 목록은 IndexedDB, 서가는 OPFS"라는 저장소 분업 위에 서 있고, 서버는 인증과 검색 프록시 수준으로 얇게 유지한 채 자막 생성 같은 무거운 일까지 브라우저 안에서 해결하는 로컬 우선 에디터다.

스스로 점검할 질문

  1. 미디어 파일을 IndexedDB의 Blob이 아니라 OPFS에 두는 이유를 성능 관점에서 설명할 수 있나요?
  2. 여러분의 앱이 브라우저 저장소에 중요한 데이터를 둔다면, 퇴거(eviction)에 대한 대비가 있나요?
  3. "서버를 얇게, 연산을 클라이언트로"라는 OpenCut의 선택이 유리한 제품과 불리한 제품은 각각 무엇일까요?

참고 링크