IndexedDB — 브라우저 안에 있는 내 전용 창고
세 가지 저장소를 크기로 비교
| 비유 | 실제 | |
|---|---|---|
| 쿠키 | 지갑 속 메모지 | 4KB, 서버로 매번 따라감 |
| localStorage | 책상 서랍 | 5~10MB, 글자만, 꺼낼 때 화면이 잠깐 멈춤 |
| IndexedDB | 창고 | 수백 MB~GB, 파일·이미지·오디오 통째로, 안 멈춤 |
핵심 차이는 용량보다 "파일을 그대로 넣을 수 있다" 는 점입니다. 오디오 파일이나 영상 조각을 문자열로 바꾸지 않고 Blob/File 원본 그대로 보관할 수 있습니다. 그리고 모든 작업이 비동기라서 큰 데이터를 꺼내는 동안에도 화면이 멈추지 않습니다.
용어 4개만 알면 끝
| 용어 | 창고에서는 | 한 줄 설명 |
|---|---|---|
| Database | 창고 건물 | 사이트(오리진)마다 하나씩. 이름 + 버전 번호를 가짐 |
| ObjectStore | 선반 | 같은 종류 물건끼리 모아두는 곳. RDB의 테이블 |
| Index | 선반에 붙인 색인표 | "수정일순으로 찾기" 같은 지름길 |
| Transaction | 출입 절차 | 물건 넣고 빼는 모든 행동은 이 절차 안에서만. 중간에 실패하면 통째로 되돌림 |
코드는 실무에선 이 정도
원래 API는 옛날 방식(이벤트 콜백)이라 손이 많이 갑니다. idb라는 1KB짜리 얇은 도우미를 쓰면 이렇게 짧아집니다.
import { openDB } from 'idb';
// 1. 창고 열기 (없으면 짓기)
const db = await openDB('내창고', 1, {
upgrade(db) {
db.createObjectStore('projects', { keyPath: 'id' }); // 선반 만들기
},
});
// 2. 넣기
await db.put('projects', { id: 'a1', name: '더빙 작업', updatedAt: Date.now() });
// 3. 꺼내기
const one = await db.get('projects', 'a1');
const all = await db.getAll('projects');
// 4. 지우기
await db.delete('projects', 'a1');
localStorage 쓰던 감각과 거의 같습니다. 다른 건 await가 붙는다는 것뿐입니다.
딱 하나만 조심하면 되는 함정
출입 절차(트랜잭션) 도중에 딴짓하면 문이 닫힙니다.
// ❌ 서버 다녀오는 사이에 트랜잭션이 끝나버림
const tx = db.transaction('projects', 'readwrite');
const data = await fetch('/api/x'); // ← 여기서 문 닫힘
tx.store.put(data); // 에러
// ✅ 필요한 걸 다 챙긴 다음, 창고 출입은 짧게
const data = await fetch('/api/x');
await db.put('projects', data);
원칙: 창고 문은 짧게 열고 짧게 닫는다. 네트워크 요청은 절대 그 사이에 끼우지 않습니다.
더 깊이: 창고 리모델링 — 버전과 마이그레이션
선반 구조를 바꾸고 싶으면 버전 번호를 올리면 됩니다. upgrade 콜백이 "몇 층까지 지어져 있었는지"(oldVersion)를 알려주므로, 거기서부터 순서대로 공사하면 옛 사용자도 데이터 손실 없이 따라옵니다.
const db = await openDB('내창고', 3, {
upgrade(db, oldVersion) {
if (oldVersion < 1) db.createObjectStore('projects', { keyPath: 'id' });
if (oldVersion < 2) db.createObjectStore('drafts', { keyPath: 'id' });
if (oldVersion < 3) {
// 기존 선반에 색인표 붙이기
const tx = db.transaction; // upgrade 중엔 versionchange 트랜잭션 사용
}
},
});
localStorage에서 IndexedDB로 이사하는 안전한 순서도 같은 원리입니다.
- 앱 시작 시 IndexedDB를 먼저 열고
- localStorage에 옛 데이터가 있으면 → IndexedDB로 복사
- 복사가 성공한 것을 확인한 뒤에만 localStorage 쪽을 삭제 (실패하면 다음 실행 때 재시도)
- 이후 읽기/쓰기는 전부 IndexedDB로
"옮기고 나서 지운다"는 순서만 지키면 중간에 새로고침이 나도 데이터가 사라지지 않습니다.
더 깊이: 창고가 철거당하지 않게 하기
브라우저 저장소는 기본적으로 "best-effort" — 디스크가 부족하면 브라우저가 오래 안 쓴 사이트의 창고부터 비웁니다. 막는 방법이 있습니다.
// 지금 얼마나 쓰고 있는지
const { usage, quota } = await navigator.storage.estimate();
// "이 창고는 지우지 말아주세요" 요청
const granted = await navigator.storage.persist();
persist()가 true를 돌려주면 그 오리진의 데이터는 용량 압박에도 자동 삭제 대상에서 빠집니다. 다만 브라우저가 거절할 수 있으므로(북마크·설치·사용 빈도 등을 봄), 업로드 이어하기처럼 중요한 데이터라면 UX까지 같이 설계해야 합니다 — "임시 저장됨" 표시, 서버 업로드가 진실의 원천이고 로컬은 어디까지나 사본이라는 원칙.
브라우저별 함정 하나: Safari는 사용자가 7일간 사이트와 상호작용하지 않으면 스크립트 저장소를 지울 수 있습니다(ITP). "로컬에만 있는 유일본"을 만들지 않는 것이 가장 확실한 방어입니다.
더 깊이: 넣지 말아야 할 것
IndexedDB는 암호화되지 않고, 같은 오리진의 모든 JS가 읽을 수 있습니다. 토큰·개인정보 같은 민감 데이터는 넣지 않습니다. XSS가 뚫리면 창고째 털립니다. 반대로 Web Worker와 Service Worker에서도 접근할 수 있어서, 백그라운드 업로드·오프라인 동기화 같은 작업과는 궁합이 아주 좋습니다.
Studio / Dubbing 쪽에 붙인다면
- 업로드 중 새로고침 방어 — 파일 조각을 창고에 넣어두고, 끊기면 이어서 올리기
- 편집 초안 자동 저장 — 타임라인·자막 수정을 창고에 계속 쌓아두기
- 디코딩 결과 캐싱 — 같은 오디오를 두 번 디코딩하지 않기
- 오프라인 작업 큐 — 인터넷 끊긴 동안의 수정 사항을 모아뒀다가 복구되면 전송
한 줄 요약 — IndexedDB는 "브라우저 안에 딸린 창고". 서랍(localStorage)에 안 들어가는 큰 물건을 둘 때 쓰되, 창고 문은 짧게 여닫고, 중요한 물건은 서버에 원본을 둡니다.
스스로 점검할 질문
- localStorage로 만들어둔 기능을 IndexedDB로 옮기려면, 기존 사용자 데이터를 잃지 않고 어떤 순서로 마이그레이션해야 하나요? (위 "이사 순서" 4단계를 코드로 옮길 수 있으면 통과)
- IndexedDB에 저장한 파일이 저장 공간 부족으로 지워지는 걸 막으려면 어떤 코드(
persist,estimate)와 UX가 필요한가요?