IndexedDB는 브라우저에 내장된 NoSQL 데이터베이스로, 수백 MB의 구조화된 데이터를 저장할 수 있습니다. localStorage의 5MB 제한과 문자열만 저장 가능한 한계를 넘어서, 오프라인 앱이나 클라이언트 캐시에 필수적인 기술입니다.

데이터베이스 열기

JS
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 작업

JS
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);

인덱스 활용

JS
// 인덱스로 검색
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

JS
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

커서 — 대량 데이터 순회

JS
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;
  });
}

트랜잭션

JS
// 여러 작업을 하나의 트랜잭션으로
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

JS
// 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

항목localStorageIndexedDB
용량~5MB수백 MB+
데이터 타입문자열만구조화된 데이터
비동기X (블로킹)O
인덱스/검색불가가능
트랜잭션없음있음
Worker 접근불가가능

**기억하기 **: IndexedDB는 브라우저의 NoSQL 데이터베이스입니다. 콜백 기반 API가 복잡하므로 idb 같은 Promise 래퍼를 사용하는 것이 좋습니다. 인덱스로 효율적인 검색, 커서로 대량 데이터 순회, 트랜잭션으로 원자성을 보장할 수 있습니다.

댓글 로딩 중...