Skip to content
This repository has been archived by the owner on Jun 13, 2023. It is now read-only.

IndexedDB

HYUNJIN LEE edited this page Aug 8, 2022 · 13 revisions

개요

  • IndexedDB는 브라우저에 내에서 제공된 트랜잭션 객체 저장소 데이터베이스(transactional object store database)이다.
  • IndexedDB에서 수행하는 작업은 트랜잭션으로 그룹화된다. 트랜잭션 내의 모든 작업이 성공하거나 혹은 실패한다.
  • IndexedDB는 객체 저장소 데이터베이스이다.
  • 객체를 저장하는 객체 저장소로 구성된다. 각 데이터베이스는 다수의 객체 저장소를 갖고 각각의 저장소는 다수의 객체를 갖을 수 있다.
  • 데이터베이스 -> 저장소 -> 객체
  • 객체는 JavaScript 객체, 불린, 숫자, BLOB(Binary Large Object) 및 JavaScript가 처리할 수 있는 대부분의 데이터 포맷 중 하나이다.
  • 각 객체 저장소에는 한가지 타입의 데이터가 들어있다. (사용자, 채팅기록, 예약 내역등)
  • 객체 저장소에는 키(key)-값(value) 쌍으로된 레코드가 들어있다.
  • 키는 객체 저장소의 개별 값을 참조하는데 사용. 키는 단순한 숫자 식별자가 될 수 있고, 값에 대한 특정 경로가 될 수 있다.
  • IndexedDB는 동일 출처 정책(Same-Origin Policy)를 따른다. 사용자는 특정 사이트에서 작성된 데이터가 다른 사이트에 노출될 것을 걱정하지 않아도 다른 사이트에 방문할 수 있다. 다시 말하면 자신의 도메인 내에서는 데이터를 읽고 쓸 수 있지만, 다른 도메인의 IndexedDB에 기록된 데이터는 접근할 수 없다.
  • 데이터베이스는 버전을 갖는다. 객체 저장소를 생성하거나 구조를 수정하고 싶다면 새로운 버전으로 데이터베이스 커넥션을 열어야한다. 데이터베이스 커넥션을 열 때 upgrade-needed 이벤트가 발생한다. 현재 버전과 이전 버전 사이의 데이터베이스에 대한 변경 사항 반영은 이 이벤트 중에 처리될 수 있다.
  • 대부분의 IndexedDB 작업은 비동기이다. API는 값이 요청되었을 때 해당 값을 반환하지 않는다.값을 요청하는 대신 해당 이벤트를 처리하는 콜백 함수를 정의해야한다.
  • IndexedDB는 인덱스된 데이터베이스이다. 인덱스를 부여해서 원하는 객체를 검색하고 가져오는데 사용.
  • IndexedDB는 브라우저 기반이다. 모든 데이터는 사용자의 연결 상태에 관계없이 접근 및 조작 가능하다.

Pattern

IndexedDB 작업의 기본 패턴은 아래와 같다.

  1. 데이터베이스를 연다.
  2. 객체 저장소에 읽기 혹은 쓰기를 하기 위해 트랜잭션을 시작한다.
  3. 객체 저장소를 연다.
  4. 객체 저장소에서 필요한 작업을 수행한다.(객체 검색, 객체 추가, 객체 삭제 등)
  5. 트랜잭션을 완료한다.

1. 데이터베이스 커넥션 열기

const request = window.indexedDB.open("my-database", 1);
request.onerror = function (event) {
  console.log("Database error:", event.target.error);
};

request.onsuccess = function (event) {
  const db = event.target.result;
  console.log("Database", db);

  console.log("Object store names: ", db.objectStoreNames);
};

window.indexedDB.open() 호출은 데이터베이스 커넥션을 반환하지 않는다. 대신 데이터베이스 커넥션을 열기 위한 IDBRequest 객체를 반환한다. 이 객체를 통해 해당 요청에 대한 이벤트를 수신할 수 있다.

브라우저에서 이 코드를 실행시키면 브라우저 내에 my-database라는 이름의 데이터베이스가 생성되고 열린다. 이후 success 이벤트가 발생한다. success 이벤트 콜백에서는 열린 IDBDatabase 객체 및 해당 데이터 베이스에 포함된 객체 저장소 목록을 콘솔에 기록한다.

서비스 워커와 마찬가지로 IndexedDB도 버전을 갖는다. 객체 저장소 추가, 변경, 삭제와 같이 데이터베이스 구조를 변경할 때마다 새로운 버전을 생성해야한다.

IndexedDB.open()의 두번째 인수로 전달되는 버전 번호를 증가시켜 새 데이터베이스 버전을 만들 수 있다.

브라우저가 기존 버전보다 큰 버전번호를 감지하면 upgrade needed 이벤트가 발생한다.

request.onupgradeneeded = function (event) {
  const db = event.target.result;

  db.createObjectStore("customers", { keyPath: "passport_number" });
};

이 코드는 데이터베이스가 upgrade needed 이벤트를 발생시키는 즉시 실행된다. upgrade needed 이벤트에서 데이터베이스 객체를 가져오고 customers라는 이름의 새 객체 저장소를 생성. 또한 여권번호를 저장소의 각 객체에 대한 고유 키로 정의하기 위해 keyPath를 사용.

2~4. 트랜잭션 시작 -> 객체 저장소 열기 -> 객체 저장소에서 필요한 작업 수행

request.onsuccess = function (event) {
  const db = event.target.result;

  const customerData = [
    {
      passport_number: "6651",
      first_name: "John",
      last_name: "Doe",
    },
    {
      passport_number: "6652",
      first_name: "Jane",
      last_name: "Doe",
    },
  ];

  const customerTransaction = db.transaction("customers", "readwrite");

  customerTransaction.onerror = function (error) {
    console.log("Error: ", error.target.error);
  };

  const customerStore = customerTransaction.objectStore("customers");

  for (let i = 0; i < customerData.length; i++) {
    customerStore.add(customerData[i]);
  }
};

위 코드는 새로운 readwrite 트랜잭션을 생성하고 작업의 범위를 customers 객체 저장소로 지정한다. 또한 트랜잭션 에러를 수신해 에러가 발생하면 이를 콘솔에 기록한다. 다음으로 생성된 트랜잭션의 objectStore() API를 호출해 customers 객체 저장소를 열고, 객체 저장소의 add() API를 호출해 객체를 추가한다.

IndexedDB에서 수행되는 대부분의 작업은 트랜잭션이다. 객체 저장소에 데이터를 추가하기 전에 새 트랜잭션을 시작해야한다. 트랜잭션은 데이터베이스 객체에서 transaction()을 호출하여, 첫번째 인자로 트랜잭션 범위를 전달하는 것으로 시작된다.

트랜잭션의 범위는 트랜잭션이 영향을 줄 수 있는 객체 저장소 이름 혹은 여러개의 객체 저장소 이름을 포함한 배열이다. 트랜잭션 범위를 정의하여, IndexedDB가 서로다른 트랜잭션 사이의 경쟁 상태를 방지할 수 있다. 두개 혹은 그 이상의 readwrite 트랜잭션 범위가 겹치는 경우 각 트랜잭션은 큐에 들어가 순차적으로 실행된다. 만일 서로다른 범위를 갖는 readwrite 트랜잭션이거나 readonly 트랜잭션인 경우 병렬로 실행될 수 있다.

트랜잭션 내에서 실행하는 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나, 모두 실패하는 원자성이 보장된다.

데이터 읽는 것에는 세가지 방법이 있다.

  1. 키를 사용하여 단일 객체를 요청
  2. 커서를 사용하여 저장소의 모든 객체를 순회
  3. 인덱스를 사용해 더 작은 데이터 그룹으로 검색

먼저 키를 사용해 객체 저장소에서 단일 객체를 읽어보자.

const request = window.indexedDB.open("my-database", 2);
request.onsuccess = function (event) {
  const db = event.target.result;
  const customerTransaction = db.transaction("customers");
  const customerStore = customerTransaction.objectStore("customers");
  const request = customerStore.get("7227");

  request.onsuccess = function (event) {
    const customer = event.target.result;
    console.log(customer);
  };
};

객체 저장소에서 get()을 호출하여 찾고자하는 고객 객체와 일치하는 키를 전달합니다. get()은 비동기적 작업이기 떄문에 결과를 즉시 반환하지는 않지만, 요청을 나타내는 객체를 반환합니다. 이 요청에 대한 onsuccess 이벤트를 수신하여 작업이 종료될 때까지 기다리고 요청한 객체를 반환할 수 있습니다.

대부분의 IndexedDB 메서드를 서로 연결하면 더 짧고 간결한 코드 작성이 가능합니다.

request.onsuccess = function (event) {
  event.target.result
    .transaction("customers")
    .objectStore("customers")
    .get("6651").onsuccess = function (event) {
    const customer = event.target.result;
    console.log(customer);
  };
};

버전 관리

마이그레이션은 특정 버전의 데이터베이스를 그 다음 상위 버전으로 올리기 위해 필요한 작업 모음이다. 원칙적으로 가장 오래된 데이터베이스도 각각의 단계를 거쳐서 최신 버전으로 마이그레이션 할 수 있다.

다음은 IndexedDB의 마이그레이션할 수 있는 방법 중 하나이다.

request.onupgradeneeded = function (event) {
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion < 2) {
    db.createObjectStore("cusomers", {
      keyPath: "passport_number",
    });
  }

  if (oldVersion < 3) {
    db.createObjectStore("employees", {
      keyPath: "employee_id",
    });
  }
};

위 메서드는 이전 버전 번호를 확인해 모든 버전의 데이터베이스를 가장 최신 버전으로 가져오도록 수행할 수 있다. 이 방법은 데이터베이스 버전 1에서 가장 최신 버전으로 옮기는데는 유용하지만 수십개의 버전을 유지하기는 어렵다. 버전간의 데이터베이스를 업그레이드하는 방법은 데이터베이스의 현재 상태를 테스트하고 필요에 따라 변경하는 방법이다.

request.onupgradeneeded = function (event) {
  const db = event.target.result;

  if (!db.objectStoreNames.contains("customers")) {
    db.createObjectStore("customers", {
      keyPath: "passport_number",
    });
  }
};

이 방법을 사용하면 변경 전에 변경이 필요한지 항상 확인할 수 있으며, 존재하지 않는 객체 저장소만 추가할 수 있다. 이미 존재하는 인덱스인 경우에만 인덱스를 삭제할 수 있다.

커서로 객체 읽기

get()을 사용하여 객체 저장소에서 단일 객체를 검색하는 방법을 살펴보았다. 안타깝게도 이 방법은 정확한 키를 알고 단일 객체를 검색할 때에만 작동한다. 여러 객체를 검색하기 위해서는 커서를 사용해야한다.

커서란? SQL 기반 데이터베이스에 익숙하다면 SELECT * FROM table 쿼리를 실행시켜 커서를 오픈한다고 생각할 것이다. 이 쿼리가 WHERE이나 LIMIT으로 변경될 수 있는 것처럼 커서도 인덱스나 바운더리로 변경될 수 있다. SQL에서 반환된 결과와 달리, 커서가 결과를 포함하고 있지는 않다. SQL에서 반환된 결과와 달리 커서가 결과를 포함하고 있지는 않다. 이것은 단순히 객체 저장소에 존재하는 실제 객체에 대한 포인터 목록이다. 커서는 객체 저장소에 존재하는 하나의 레코드를 가리키며, continue()혹은 advance()를 통해서만 다음으로 넘어간다. 이렇게 하면 모든 객체를 들고 있을 메모리가 없이도 용량이 큰 객체저장소를 순회할 수 있다.

이제 커서를 열어보자.

const request = window.indexedDB.open("my-database", 3);

request.onsuccess = function (event) {
  const db = event.target.result;
  const customerTransaction = db.transaction("customers");
  const customerStore = customerTransaction.objectStore("customers");
  const customerCursor = customerStore.openCursor();

  customerCursor.onsuccess = function (event) {
    const cursor = event.target.result;
    if (!cursor) return;

    console.log(cursor.value.first_name);
    cursor.continue();
  };
};

저장소에 저장된 고객 코드를 순차적으로 돌며 콘솔에 이름이 출력된다.

onsuccess이벤트 리스너에서는 커서를 통해 현재 가리키고 있는 객체를 가져올 수 있다. 객체의 first_name의 값을 로그로 남기고, 커서가 다음 객체를 가리키도록 continue() 메서드를 호출한다. 커서가 앞으로 이동할 때마다 onsuccess 이벤트가 발생해 이벤트 리스너가 다시 실행되고 다음 고객의 이름을 로그로 남긴다.

객체 저장소가 비어있더라도 커서는 앞으로 이동할 때마다 onsuccess 이벤트가 발생하고 이때 커서(event.target.result)는 null을 가리킨다. onsuccess 함수가 커서에 접근하기 전에 커서가 null을 기리키고 있지 않은지 꼭 확인해야한다.

위로 Bubbling되는 IndexedDB 에러 처리

IndexedDB의 에러 이벤트는 위로 전파된다.

커서 오픈 요청 중 오류가 생기면 해당 요청의 onerror 핸들러에 오류가 잡힐 것이다. 만일 해당 요청에 이를 처리하기 위한 onerror 핸들러가 정의되지 않았다면 이 오류는 트랜잭션 에러 핸들러에 잡히도록 전파될 것이다. 트랜잭션도 에러 핸들러를 가지고 있지 않다면, 에러는 데이터베이스 객체의 에러 핸들러에 잡히도록 위로 전파도리 것이다.

이러한 작동 방식으로 인해 매 요청 및 트랜잭션마다 별도의 에러 핸들러를 정의하는 대신, 데이터베이스 객체에서 하나의 에러 핸드러를 작성해 공통적으로 사용할 수 있다.