Indexed DB 정리

@developer/takealittle.time·2025년 9월 2일
post-thumbnail

도입 배경

사내 프로덕트 구현 도중 특정 데이터를 임시 저장해야하는 기능에 대한 논의가 있었다.
Local StorageSession Storage 보다 가용한 용량이 크고, 비동기 API를 제공하는 Indexed DB를 해당 기능 구현에 적용하기로 하였다.

이에, Indexed DB에 대해 학습하게 된 부분에 대해 정리를 해보고자 한다.


Indexed DB

브라우저에 내장된 비관계형(NoSQL) 데이터베이스.

localStorage , Session Storage 보다 훨씬 강력한 기능 제공.

  • 비관계형 (NoSQL) 데이터베이스

    • 테이블이 아닌, 오브젝트 스토어(Object Store)를 사용해 데이터를 저장.

    • JSON 형식의 객체를 그대로 저장 가능

  • 대용량 데이터 저장 가능

    • localStorage 의 최대 용량은 약 5MB, Indexed DB는 수백MB ~ 수 GB까지 저장 가능.

    • 브라우저마다 편차가 있을 수 있으나, 일반적으로 50MB 이상 지원

  • 비동기 API

    • Promise 기반 API를 제공 → UI가 멈추지 않고 데이터 작업 수행 가능. (localStorage는 동기적으로 데이터 처리)
  • 트랜잭션 지원

    • 데이터 무결성 유지를 위해 트랜잭션(Transaction) 지원.

    • 여러 작업을 한 번에 트랜잭션 단위로 묶어 실행 가능

  • 인덱싱(Indexing) 기능 제공

    • 특정 속성을 기준으로 빠른 검색이 가능하도록 인덱스(Index)를 생성 가능.

    • 대량의 데이터를 검색할 때 성능 최적화 가능

  • 브라우저 내장 기능 형태

    • 모든 최신 브라우저(Chrome, Firefox, Edge, Safari 등)에서 이용 가능

작동 원리

IndexedDB는 오브젝트 스토어(Object Store)를 사용해 데이터를 저장한다.

1. 데이터베이스 생성 및 버전 관리

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

2. 데이터 추가 (Add)

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("고객 정보가 추가되었습니다.");
};

3. 데이터 조회 (Read)

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("데이터가 존재하지 않습니다.");
  }
};

4. 데이터 업데이트 (Update)

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

5. 데이터 삭제 (Delete)

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를 사용함에 있어 코드를 훨씬 간소화 할 수 있다.

또, 버전 관리 면에서 용이하다.

Indexed DB의 버전 관리

  • 순수 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 코드가 실행된다.

  • 라이브러리(e.g. 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);
}

idbDexie

idb‘원래 해야 할 일을 깔끔하게 정리해서 코드로 쓰게 해주는’ 도구. ↔︎ 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')
  }
})
  • 마이그레이션을 ‘직접’ 분기로 관리해야 함. → 버전 수가 늘어날수록 하나의 upgrade 블록이 길어지고, 유지보수 비용이 쌓임.

언제 무엇을 고르나 (실무 기준)

Dexie

  • 버전 업이 잦다, 스키마가 점점 복잡해질 것 같다
  • 데이터 변환(마이그레이션) 로직이 자주 필요하다
  • 인덱스/쿼리/배치 같은 고급 기능도 같이 쓰고 싶다

idb

  • 저장 구조가 단순하고, 스키마 변경이 드물다
  • 번들 크기가 매우 중요하다
  • IndexedDB 저수준 제어를 그대로 유지하고 싶다
profile
능동적으로 사고하고, 성장하기 위한. 🌱

0개의 댓글