
도입 배경
사내 프로덕트 구현 도중 특정 데이터를 임시 저장해야하는 기능에 대한 논의가 있었다.
Local Storage나Session Storage보다 가용한 용량이 크고, 비동기 API를 제공하는 Indexed DB를 해당 기능 구현에 적용하기로 하였다.
이에, Indexed DB에 대해 학습하게 된 부분에 대해 정리를 해보고자 한다.
브라우저에 내장된 비관계형(NoSQL) 데이터베이스.
localStorage , Session Storage 보다 훨씬 강력한 기능 제공.
테이블이 아닌, 오브젝트 스토어(Object Store)를 사용해 데이터를 저장.
JSON 형식의 객체를 그대로 저장 가능
localStorage 의 최대 용량은 약 5MB, Indexed DB는 수백MB ~ 수 GB까지 저장 가능.
브라우저마다 편차가 있을 수 있으나, 일반적으로 50MB 이상 지원
데이터 무결성 유지를 위해 트랜잭션(Transaction) 지원.
여러 작업을 한 번에 트랜잭션 단위로 묶어 실행 가능
특정 속성을 기준으로 빠른 검색이 가능하도록 인덱스(Index)를 생성 가능.
대량의 데이터를 검색할 때 성능 최적화 가능
IndexedDB는 오브젝트 스토어(Object Store)를 사용해 데이터를 저장한다.
IndexedDB는 버전(Version)을 가진다.
onupgradeneeded 이벤트 발생 → 스키마 변경 수행let request = indexedDB.open("myDatabase", 1);
request.onupgradeneeded = function (event) {
let db = event.target.result;
let objectStore = db.createObjectStore("customers", {
keyPath: "id",
autoIncrement: true,
});
objectStore.createIndex("name", "name", { unique: false });
};
request.onsuccess = function (event) {
let db = event.target.result;
console.log("IndexedDB가 성공적으로 열렸습니다.");
};
request.onerror = function (event) {
console.error("데이터베이스 오류:", event.target.errorCode);
};
let transaction = db.transaction(["customers"], "readwrite");
let objectStore = transaction.objectStore("customers");
let customer = { name: "홍길동", email: "hong@example.com" };
let addRequest = objectStore.add(customer);
addRequest.onsuccess = function () {
console.log("고객 정보가 추가되었습니다.");
};
let transaction = db.transaction(["customers"], "readonly");
let objectStore = transaction.objectStore("customers");
let getRequest = objectStore.get(1); // id=1인 데이터 조회
getRequest.onsuccess = function (event) {
if (event.target.result) {
console.log("조회된 데이터:", event.target.result);
} else {
console.log("데이터가 존재하지 않습니다.");
}
};
let transaction = db.transaction(["customers"], "readwrite");
let objectStore = transaction.objectStore("customers");
let updateRequest = objectStore.get(1);
updateRequest.onsuccess = function (event) {
let data = event.target.result;
data.email = "new_email@example.com";
objectStore.put(data);
};
let transaction = db.transaction(["customers"], "readwrite");
let objectStore = transaction.objectStore("customers");
let deleteRequest = objectStore.delete(1);
deleteRequest.onsuccess = function () {
console.log("데이터 삭제 완료");
};
순수 Indexed DB를 사용할 수도 있으나, npm 에서 제공하고 있는 Indexed DB 관련 라이브러리들이 많다.
그 중에서도 idb , Dexie 등 오랜 시간 많은 유저들이 사용해 온 패키지들이 있는데, 해당 라이브러리를 사용하면 Indexed DB를 사용함에 있어 코드를 훨씬 간소화 할 수 있다.
또, 버전 관리 면에서 용이하다.
초기 버전 (v1)
처음 앱을 만들 때, 사용자 이름만 저장한다고 가정하자.
const req = indexedDB.open("userDB", 1); // 버전 1
req.onupgradeneeded = (e) => {
const db = e.target.result;
db.createObjectStore("users", { keyPath: "id" });
// v1: users 스토어에 id, name만 저장
};
기능 확장: 이메일 필드 추가 (v2)
나중에 요구사항이 생겨, 이메일도 저장해야 한다.
(하지만 Indexed DB는 기존 store 구조를 실행 중에 변경할 수 없다. → 버전을 올려야 한다.)
const req = indexedDB.open("userDB", 2); // 버전 2로 올림
req.onupgradeneeded = (e) => {
const db = e.target.result;
const store = e.target.transaction.objectStore("users");
store.createIndex("by_email", "email"); // 새 인덱스 추가
};
기능 확장: 나이 필드 추가 (v3)
이후 릴리즈에 나이 필드를 넣고, 나이 필드에 따라 검색도 하고 싶다는 요구사항이 들어왔다.
const req = indexedDB.open("userDB", 3); // 버전 3으로 업
req.onupgradeneeded = (e) => {
const db = e.target.result;
const store = e.target.transaction.objectStore("users");
store.createIndex("by_age", "age"); // 새로운 인덱스
};
버전 번호는 말하자면 DB 구조 변경의 ‘스위치’ 역할이다.
버전이 올라갈 때만 onupgradeneeded 가 실행되고, 여기서 새로운 스키마를 정의한다.
사용자가 앱을 오래 안 쓰다가 (e.g. 버전 1 DB를 가지고 있다가) → 최신 앱(버전 3)을 실행하면
→ 브라우저는 자동으로 v1 → v3 업그레이드를 거치고, 위에서 작성한 모든 onupgradeneeded 코드가 실행된다.
Dexie)를 이용하는 경우라이브러리는 이러한 반복적이고 번거로운 부분들을 자동화/간소화 해준다.
const db = new Dexie("userDB");
db.version(1).stores({
users: "++id, name"
});
db.version(2).stores({
users: "++id, name, email"
}).upgrade(tx => {
return tx.users.toCollection().modify(user => {
user.email = user.email || "";
});
});
db.version(3).stores({
users: "++id, name, email, age"
}).upgrade(tx => {
// 필요한 경우 데이터 변환
});
버전별 스키마 정의를 코드로 깔끔히 나눌 수 있고, .upgrade()도 버전 단위로 분리할 수 있다.
Dexie가 알아서 버전 차이를 감지하고, 순차 업그레이드를 실행한다.
추가적으로, 직접 적용해 본 결과 코드가 말도 안되게 간소화 되었다.
ex) Dexie 적용 전 saveConversation()
export async function saveConversation(
record: ConversationRecord
): Promise<void> {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(record);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error("IDB tx error"));
});
}
Dexie 적용 후 saveConversation()
export async function saveConversation(
record: ConversationRecord
): Promise<void> {
await db.conversations.put(record);
}
idb와 Dexieidb 는 ‘원래 해야 할 일을 깔끔하게 정리해서 코드로 쓰게 해주는’ 도구. ↔︎ Dexie 는 실제로 ‘여러 부가 기능 유틸을 제공해주는’ 도구에 가까움.
but. idb 의 강점: 가벼운 용량, 공식 문서에서의 채택.
idb를 이용해 버전 관리openDB('userDB', 3, {
upgrade(db, oldV, newV, tx) {
if (oldV < 1) db.createObjectStore('users', { keyPath: 'id' })
if (oldV < 2) tx.objectStore('users').createIndex('by_email', 'email')
if (oldV < 3) tx.objectStore('users').createIndex('by_age', 'age')
}
})
Dexie
- 버전 업이 잦다, 스키마가 점점 복잡해질 것 같다
- 데이터 변환(마이그레이션) 로직이 자주 필요하다
- 인덱스/쿼리/배치 같은 고급 기능도 같이 쓰고 싶다
idb
- 저장 구조가 단순하고, 스키마 변경이 드물다
- 번들 크기가 매우 중요하다
- IndexedDB 저수준 제어를 그대로 유지하고 싶다