Storage말고 IndexedDB는 어때요?
2025-05-21새로운 회사에서 레거시 코드와 버그를 해결하던 중, 다음과 같은 상황을 맞닥뜨렸습니다.
1. 문제 상황
시나리오:
- 외부 결제 모듈 사용 때문에 외부 페이지를 접속했다가 결제가 끝나면 다시 앱으로 복귀합니다
- 이미지로 구성된 기프트 카드는 결제가 완료된 후, 결제 완료 정보와 함께 카드 정보를 서버로 전달해야 합니다
- 해당 기프트 카드 정보는 그동안 Recoil로 관리되고 있어서, 페이지 이동 시 카드 정보가 소멸된 채 결제가 완료되었습니다
처음 이 버그를 발견했을 때는 "뭐야? Recoil 대신 Storage로 관리하면 되는 거 아니야?"라고 생각했습니다.
하지만 로직을 구성하던 중 또 다른 문제를 발견했습니다. 해당 기프트 카드의 이미지가 Base64 형태였던 것이죠.
2. Base64란?
Base64는 바이너리 데이터를 텍스트로 인코딩하는 방식 중 하나입니다.
Base64 구성
64개의 ASCII 문자만을 사용해 데이터를 표현하는 인코딩 방식입니다:
- A-Z (26개)
- a-z (26개)
- 0-9 (10개)
- +, / (2개)
총 64개의 문자로 구성되어 있어 이름이 Base64입니다.
프로젝트 상황
우리 회사 앱은 Next.js와 Flutter로 구성된 웹뷰 형태이며, 해당 카드의 이미지는 Flutter에서 Base64로 가공하여 웹으로 전달하는 방식이었습니다.
문제점: Base64는 텍스트로 인코딩되기 때문에 그 길이가 무지막지하게 깁니다. 따라서 LocalStorage나 SessionStorage에 그대로 저장할 수 없는 문제가 있었죠.
그래서 예전에 이론만 봤었던 IndexedDB를 활용해보기로 했습니다.
3. IndexedDB란?
IndexedDB는 브라우저에서 사용 가능한 로컬 DB API입니다.
주요 특징:
- NoSQL 계열로 key-value 형식으로 데이터를 저장할 수 있습니다
- 수 MB~수 GB의 데이터를 저장 가능합니다
- 비동기로 작동합니다
- 트랜잭션 기반으로 작동하여 안전합니다
- 대부분의 브라우저에서 지원합니다
LocalStorage vs IndexedDB
| 특성 | LocalStorage | IndexedDB |
|---|---|---|
| 저장 용량 | ~5-10MB | 수백 MB ~ 수 GB |
| 데이터 타입 | 문자열만 | 객체, Blob 등 다양 |
| 동작 방식 | 동기 | 비동기 |
| 트랜잭션 | 없음 | 있음 |
4. IndexedDB 사용하기
DB 생성
IndexedDB를 사용하려면 먼저 DB부터 만들어야 합니다.
const request = indexedDB.open("MyDB", 1);
- 첫 번째 매개변수: DB명
- 두 번째 매개변수: DB의 버전
중요: IndexedDB에서는 버전이 가장 중요합니다. Object Store(테이블)를 만들거나 수정하면 반드시 버전을 업그레이드해야 합니다.
Object Store 생성
DB를 만들었으니 이제 Object Store(테이블)를 만들어야 합니다.
request.onupgradeneeded = function (event) {
const db = request.result;
if (!db.objectStoreNames.contains("images")) {
db.createObjectStore("images", { keyPath: "id" });
}
};
request.onerror = () => {
console.error("IndexedDB 열기 실패");
};
request.onsuccess = () => {
console.log("IndexedDB 연결 성공");
};
db.objectStoreNames.contains('images'): 해당 테이블이 있는지 확인합니다createObjectStore: 테이블을 생성합니다keyPath: 고유 키입니다 (MySQL의 PRIMARY KEY 같은 개념)
5. CRUD 작업
Create - 데이터 추가
function addImage(id: string, base64: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.add({ id, base64 });
tx.oncomplete = () => {
console.log("이미지 저장 완료");
};
};
}
순서:
- DB에 접속합니다
db.transaction('images', 'readwrite')로 images 테이블에 대해 읽기/쓰기 권한을 가진 트랜잭션을 생성합니다- 트랜잭션에서 images 테이블을 가져옵니다
- 데이터를 Create하고 트랜잭션 완료를 확인합니다
Read - 데이터 조회
function getImage(id: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readonly");
const store = tx.objectStore("images");
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const result = getRequest.result;
if (result) {
console.log("이미지 데이터:", result.base64);
} else {
console.log("해당 ID의 데이터 없음");
}
};
};
}
Read도 동일하게 DB 연결 → 트랜잭션 생성 → 테이블 접근 순으로 동작합니다.
DB를 생성할 때 지정했던 keyPath로 데이터를 접근할 수 있습니다.
Update - 데이터 수정
function updateImage(id: string, newBase64: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.put({ id, base64: newBase64 });
tx.oncomplete = () => {
console.log("이미지 업데이트 완료");
};
};
}
주의: Update는 기존 정보를 덮어쓰기 때문에 데이터 관리에 주의를 기울여야 합니다.
Delete - 데이터 삭제
function deleteImage(id: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.delete(id);
tx.oncomplete = () => {
console.log("이미지 삭제 완료");
};
};
}
정리
IndexedDB는 다음과 같은 상황에서 유용합니다:
사용하기 좋은 경우:
- 대용량 데이터를 저장해야 할 때
- Base64 이미지나 Blob 데이터를 저장할 때
- 복잡한 객체를 저장해야 할 때
- 오프라인 데이터 캐싱이 필요할 때
LocalStorage가 더 나은 경우:
- 간단한 문자열 데이터만 저장할 때
- 동기적으로 빠르게 접근해야 할 때
- 5MB 이하의 작은 데이터일 때
IndexedDB는 실제 DB처럼 트랜잭션도 존재하며 비동기로 작동하여 실무에서 유용하게 쓰일 경우가 많이 있습니다. 여러분도 기회가 된다면 IndexedDB를 한번 사용해보는 건 어떨까요?