IndexedDB 심화 — 트랜잭션, 인덱스, 커서로 대량 데이터 관리
IndexedDB는 브라우저에 내장된 NoSQL 데이터베이스로, 수백 MB의 구조화된 데이터를 저장할 수 있습니다. localStorage의 5MB 제한과 문자열만 저장 가능한 한계를 넘어서, 오프라인 앱이나 클라이언트 캐시에 필수적인 기술입니다.
데이터베이스 열기
function openDB(name, version = 1) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 오브젝트 스토어 생성 (테이블과 유사)
if (!db.objectStoreNames.contains("users")) {
const store = db.createObjectStore("users", { keyPath: "id", autoIncrement: true });
store.createIndex("name", "name", { unique: false });
store.createIndex("email", "email", { unique: true });
store.createIndex("age", "age", { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
CRUD 작업
class UserStore {
constructor(db) {
this.db = db;
}
// 추가
async add(user) {
return this.transaction("readwrite", (store) => {
return store.add(user);
});
}
// 조회
async get(id) {
return this.transaction("readonly", (store) => {
return store.get(id);
});
}
// 수정
async update(user) {
return this.transaction("readwrite", (store) => {
return store.put(user);
});
}
// 삭제
async delete(id) {
return this.transaction("readwrite", (store) => {
return store.delete(id);
});
}
// 전체 조회
async getAll() {
return this.transaction("readonly", (store) => {
return store.getAll();
});
}
transaction(mode, callback) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction("users", mode);
const store = tx.objectStore("users");
const request = callback(store);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// 사용
const db = await openDB("myApp");
const userStore = new UserStore(db);
await userStore.add({ name: "정훈", email: "j@test.com", age: 25 });
const user = await userStore.get(1);
인덱스 활용
// 인덱스로 검색
async function findByEmail(db, email) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const index = store.index("email");
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 인덱스 범위 검색
async function findByAgeRange(db, min, max) {
return new Promise((resolve, reject) => {
const range = IDBKeyRange.bound(min, max);
const tx = db.transaction("users", "readonly");
const index = tx.objectStore("users").index("age");
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
IDBKeyRange
IDBKeyRange.only(25); // age === 25
IDBKeyRange.lowerBound(20); // age >= 20
IDBKeyRange.upperBound(30); // age <= 30
IDBKeyRange.bound(20, 30); // 20 <= age <= 30
IDBKeyRange.bound(20, 30, true, false); // 20 < age <= 30
커서 — 대량 데이터 순회
async function processAllUsers(db) {
return new Promise((resolve) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue(); // 다음 레코드로
} else {
resolve(results); // 모든 레코드 처리 완료
}
};
});
}
// 조건부 업데이트 (커서 사용)
async function incrementAllAges(db) {
return new Promise((resolve) => {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const user = cursor.value;
user.age += 1;
cursor.update(user);
cursor.continue();
}
};
tx.oncomplete = resolve;
});
}
트랜잭션
// 여러 작업을 하나의 트랜잭션으로
function batchInsert(db, users) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
users.forEach((user) => store.add(user));
tx.oncomplete = resolve; // 모든 작업 성공
tx.onerror = reject; // 하나라도 실패하면 전체 롤백
tx.onabort = () => reject(new Error("트랜잭션 중단"));
});
}
Promise 래퍼 라이브러리 — idb
// idb 라이브러리를 사용하면 훨씬 간결
import { openDB } from "idb";
const db = await openDB("myApp", 1, {
upgrade(db) {
db.createObjectStore("users", { keyPath: "id", autoIncrement: true });
},
});
// CRUD가 Promise 기반
await db.add("users", { name: "정훈" });
const user = await db.get("users", 1);
const all = await db.getAll("users");
await db.delete("users", 1);
IndexedDB vs localStorage
| 항목 | localStorage | IndexedDB |
|---|---|---|
| 용량 | ~5MB | 수백 MB+ |
| 데이터 타입 | 문자열만 | 구조화된 데이터 |
| 비동기 | X (블로킹) | O |
| 인덱스/검색 | 불가 | 가능 |
| 트랜잭션 | 없음 | 있음 |
| Worker 접근 | 불가 | 가능 |
**기억하기 **: IndexedDB는 브라우저의 NoSQL 데이터베이스입니다. 콜백 기반 API가 복잡하므로 idb 같은 Promise 래퍼를 사용하는 것이 좋습니다. 인덱스로 효율적인 검색, 커서로 대량 데이터 순회, 트랜잭션으로 원자성을 보장할 수 있습니다.
댓글 로딩 중...