IndexedDB 사용

이 가이드에서는 IndexedDB API의 기본사항을 설명합니다. 여기서는 Jake Archibald의 IndexedDB Promised 라이브러리를 사용합니다. 이 라이브러리는 IndexedDB API와 매우 유사하지만 promise를 사용하므로 보다 간결한 구문을 만들기 위해 await할 수 있습니다. 이렇게 하면 구조를 유지하면서 API가 간소화됩니다.

IndexedDB란 무엇인가요?

IndexedDB는 사용자 브라우저의 모든 항목을 저장할 수 있는 대규모 NoSQL 스토리지 시스템입니다. IndexedDB는 일반적인 검색, 가져오기, 내보내기 작업 외에도 트랜잭션을 지원하며, 대량의 구조화된 데이터를 저장하는 데 적합합니다.

각 IndexedDB 데이터베이스는 origin(일반적으로 사이트 도메인 또는 하위 도메인)마다 고유하므로 다른 출처에서 액세스하거나 액세스할 수 없습니다. 데이터 스토리지 한도가 존재하는 경우 일반적으로는 크지만 브라우저마다 한도와 데이터 제거를 처리하는 방식이 다릅니다. 자세한 내용은 추가 자료 섹션을 참고하세요.

IndexedDB 용어

데이터베이스
가장 높은 IndexedDB 수준입니다. 여기에는 유지하려는 데이터가 포함되는 객체 저장소가 포함됩니다. 선택한 이름으로 여러 데이터베이스를 만들 수 있습니다.
객체 저장소
데이터를 저장할 개별 버킷으로, 관계형 데이터베이스의 테이블과 유사합니다. 일반적으로 저장하는 데이터의 유형 (자바스크립트 데이터 유형 아님)마다 객체 저장소 하나가 있습니다. 데이터베이스 테이블과 달리 매장 데이터의 JavaScript 데이터 유형은 일관될 필요가 없습니다. 예를 들어 앱에 세 사람에 대한 정보가 포함된 people 객체 저장소가 있는 경우 이러한 사람의 연령 속성은 53, 'twenty-five', unknown일 수 있습니다.
색인
데이터의 개별 속성에 따라 다른 객체 저장소 (참조 객체 저장소라고 함)에서 데이터를 구성하기 위한 일종의 객체 저장소입니다. 색인은 이 속성을 기준으로 객체 저장소에서 레코드를 검색하는 데 사용됩니다. 예를 들어 사람을 저장하는 경우 나중에 이름, 나이 또는 좋아하는 동물로 가져올 수 있습니다.
작업
데이터베이스와의 상호작용입니다.
트랜잭션
데이터베이스 무결성을 보장하는 작업 또는 작업 그룹을 둘러싸는 래퍼입니다. 트랜잭션의 작업 중 하나가 실패하면 어떤 작업도 적용되지 않고 데이터베이스는 트랜잭션이 시작되기 전의 상태로 돌아갑니다. IndexedDB의 모든 읽기 또는 쓰기 작업은 트랜잭션의 일부여야 합니다. 이렇게 하면 동시에 데이터베이스에서 작업하는 다른 스레드와 충돌할 위험 없이 원자적 읽기-수정-쓰기 작업을 수행할 수 있습니다.
Cursor
데이터베이스의 여러 레코드를 반복하는 메커니즘입니다.

IndexedDB 지원 확인 방법

IndexedDB는 거의 보편적으로 지원됩니다. 하지만 이전 브라우저를 사용하는 경우 만일에 대비해 기능 감지 지원을 사용하는 것은 바람직하지 않습니다. 가장 쉬운 방법은 window 객체를 확인하는 것입니다.

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

데이터베이스를 여는 방법

IndexedDB를 사용하면 원하는 이름으로 여러 데이터베이스를 만들 수 있습니다. 데이터베이스를 열려고 할 때 존재하지 않는 데이터베이스는 자동으로 생성됩니다. 데이터베이스를 열려면 idb 라이브러리의 openDB() 메서드를 사용합니다.

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

이 메서드는 데이터베이스 객체로 확인되는 프로미스를 반환합니다. openDB() 메서드를 사용할 때는 이름, 버전 번호, 이벤트 객체를 제공하여 데이터베이스를 설정합니다.

다음은 컨텍스트에서 openDB() 메서드의 예입니다.

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

익명 함수의 상단에서 IndexedDB 지원 여부를 확인합니다. 브라우저에서 IndexedDB를 지원하지 않으면 함수가 종료됩니다. 함수를 계속 사용할 수 있으면 openDB() 메서드를 호출하여 'test-db1'라는 데이터베이스를 엽니다. 이 예에서는 편의를 위해 선택적 이벤트 객체를 생략했지만 IndexedDB로 의미 있는 작업을 수행하려면 이 객체를 지정해야 합니다.

객체 저장소 사용 방법

IndexedDB 데이터베이스에는 객체 저장소가 하나 이상 포함되며 각 저장소에는 키의 열과 해당 키와 연결된 데이터를 위한 다른 열이 있습니다.

객체 저장소 만들기

잘 구조화된 IndexedDB 데이터베이스에는 지속해야 하는 각 데이터 유형별로 하나의 객체 저장소가 있어야 합니다. 예를 들어 사용자 프로필과 메모를 유지하는 사이트에는 person 객체가 포함된 people 객체 저장소와 note 객체가 포함된 notes 객체 저장소가 있을 수 있습니다.

데이터베이스 무결성을 보장하려면 openDB() 호출 시 이벤트 객체에서 객체 저장소를 만들거나 삭제하는 것만 가능합니다. 이벤트 객체는 객체 저장소를 만들 수 있는 upgrade() 메서드를 노출합니다. upgrade() 메서드 내에서 createObjectStore() 메서드를 호출하여 객체 저장소를 만듭니다.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

이 메서드는 객체 저장소 이름과 객체 저장소의 다양한 속성을 정의할 수 있는 선택적 구성 객체를 사용합니다.

다음은 createObjectStore()를 사용하는 방법의 예입니다.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

이 예에서는 이벤트 객체가 openDB() 메서드에 전달되어 객체 저장소를 만들고, 이전과 마찬가지로 객체 저장소를 만드는 작업은 이벤트 객체의 upgrade() 메서드에서 실행됩니다. 그러나 이미 존재하는 객체 저장소를 만들려고 하면 브라우저에서 오류가 발생하므로 객체 저장소가 있는지 확인하는 if 문에서 createObjectStore() 메서드를 래핑하는 것이 좋습니다. if 블록 내에서 createObjectStore()를 호출하여 'firstOS'라는 객체 저장소를 만듭니다.

기본 키를 정의하는 방법

객체 저장소를 정의할 때 기본 키를 사용하여 저장소에서 데이터를 고유하게 식별하는 방법을 정의할 수 있습니다. 키 경로를 정의하거나 키 생성기를 사용하여 기본 키를 정의할 수 있습니다.

키 경로는 항상 존재하며 고유한 값을 포함하는 속성입니다. 예를 들어 people 객체 저장소의 경우 이메일 주소를 키 경로로 선택할 수 있습니다.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

이 예시에서는 'people'라는 객체 저장소를 만들고 email 속성을 keyPath 옵션에서 기본 키로 할당합니다.

autoIncrement와 같은 키 생성기를 사용할 수도 있습니다. 키 생성기는 객체 저장소에 추가된 모든 객체에 대해 고유한 값을 만듭니다. 기본적으로 키를 지정하지 않으면 IndexedDB는 키를 만들어 데이터와 별도로 저장합니다.

다음 예시에서는 'notes'라는 객체 저장소를 만들고 자동 증분 숫자로 자동 할당되는 기본 키를 설정합니다.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

다음 예시는 이전 예시와 비슷하지만 이번에는 자동 증분 값이 'id'라는 속성에 명시적으로 할당됩니다.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

키를 정의하는 데 사용할 방법을 선택하는 것은 데이터에 따라 다릅니다. 데이터에 항상 고유한 속성이 있으면 이 속성을 keyPath로 만들어 이러한 고유성을 적용할 수 있습니다. 그렇지 않으면 자동 증분 값을 사용합니다.

다음 코드는 객체 저장소에서 기본 키를 정의하는 다양한 방법을 보여주는 3개의 객체 저장소를 만듭니다.

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

색인 정의 방법

색인은 지정된 속성을 통해 참조 객체 저장소에서 데이터를 검색하는 데 사용되는 객체 저장소의 일종입니다. 색인은 참조 객체 저장소 내에 있으며 동일한 데이터를 포함하지만 참조 저장소의 기본 키 대신 지정된 속성을 키 경로로 사용합니다. 색인은 객체 저장소를 만들 때 생성되어야 하며 데이터에 고유한 제약조건을 정의하는 데 사용할 수 있습니다.

색인을 만들려면 객체 저장소 인스턴스에서 createIndex() 메서드를 호출합니다.

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

이 메서드는 색인 객체를 만들고 반환합니다. 객체 저장소 인스턴스의 createIndex() 메서드는 새 색인의 이름을 첫 번째 인수로 사용하고 두 번째 인수는 색인을 생성할 데이터의 속성을 참조합니다. 마지막 인수를 사용하면 색인 작동 방식을 결정하는 두 가지 옵션, 즉 uniquemultiEntry를 정의할 수 있습니다. uniquetrue로 설정되면 색인은 단일 키에 중복 값을 허용하지 않습니다. 다음으로 multiEntry는 색인이 생성된 속성이 배열일 때 createIndex()의 동작을 결정합니다. true로 설정되어 있으면 createIndex()는 각 배열 요소의 색인에 항목을 추가합니다. 그렇지 않으면 배열이 포함된 단일 항목을 추가합니다.

예를 들면 다음과 같습니다.

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

이 예시에서 'people' 객체와 'notes' 객체 저장소에는 색인이 있습니다. 색인을 만들려면 먼저 createIndex()를 호출할 수 있도록 createObjectStore() (객체 저장소 객체)의 결과를 변수에 할당합니다.

데이터 작업 방법

이 섹션에서는 데이터를 생성, 읽기, 업데이트, 삭제하는 방법을 설명합니다. 이러한 작업은 모두 비동기식이며 IndexedDB API가 요청을 사용하는 프로미스를 사용합니다. 따라서 API가 간소화됩니다. 요청으로 트리거된 이벤트를 수신 대기하는 대신 openDB() 메서드에서 반환된 데이터베이스 객체에서 .then()를 호출하여 데이터베이스와의 상호작용을 시작하거나 데이터베이스 생성을 await할 수 있습니다.

IndexedDB의 모든 데이터 작업은 트랜잭션 내에서 실행됩니다. 각 작업의 형식은 다음과 같습니다.

  1. 데이터베이스 객체를 가져옵니다.
  2. 데이터베이스에서 트랜잭션을 엽니다.
  3. 트랜잭션 시 객체 저장소를 엽니다.
  4. 객체 저장에 대한 작업을 수행합니다.

트랜잭션은 작업 또는 작업 그룹 주변의 안전한 래퍼로 생각할 수 있습니다. 트랜잭션 내의 작업 중 하나가 실패하면 모든 작업이 롤백됩니다. 트랜잭션은 하나 이상의 객체 저장소와 관련이 있으며, 이러한 저장소는 트랜잭션을 열 때 정의합니다. 읽기 전용 또는 읽기/쓰기로 구성할 수 있습니다 이는 트랜잭션 내의 작업이 데이터를 읽는지 아니면 데이터베이스를 변경하는지를 나타냅니다.

데이터 만들기

데이터를 만들려면 데이터베이스 인스턴스에서 add() 메서드를 호출하고 추가하려는 데이터를 전달합니다. add() 메서드의 첫 번째 인수는 데이터를 추가할 객체 저장소이고, 두 번째 인수는 추가할 필드와 관련 데이터가 포함된 객체입니다. 다음은 단일 데이터 행을 추가하는 가장 간단한 예입니다.

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

add() 호출은 트랜잭션 내에서 발생하므로 프로미스가 성공적으로 해결된다고 해서 반드시 작업이 작동했음을 의미하지는 않습니다. 추가 작업이 실행되었는지 확인하려면 transaction.done() 메서드를 사용하여 전체 트랜잭션이 완료되었는지 확인해야 합니다. 이 프로미스는 트랜잭션이 자체적으로 완료될 때 결정되고 트랜잭션 오류가 발생하면 거부되는 프로미스입니다. 이 검사는 데이터베이스의 변경사항이 실제로 발생했는지 알 수 있는 유일한 방법이므로 모든 '쓰기' 작업에 대해 이 검사를 수행해야 합니다.

다음 코드는 트랜잭션 내에서 add() 메서드를 사용하는 방법을 보여줍니다.

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

데이터베이스를 열고 필요한 경우 객체 저장소를 만든 후에는 transaction() 메서드를 호출하여 트랜잭션을 열어야 합니다. 이 메서드는 거래하려는 매장의 인수와 모드를 사용합니다. 여기서는 저장소에 데이터를 쓰는 것이 좋으므로 이 예에서는 'readwrite'를 지정합니다.

다음 단계는 거래의 일부로 스토어에 상품을 추가하는 것입니다. 이전 예에서는 각각 프로미스를 반환하는 'foods' 저장소의 세 가지 작업을 다룹니다.

  1. 맛있는 샌드위치에 대한 기록을 추가하고 있습니다.
  2. 에그에 관한 기록을 추가 중입니다.
  3. 거래가 완료되었음을 알립니다 (tx.done).

이러한 작업은 모두 프로미스 기반이므로 모두 완료될 때까지 기다려야 합니다. 이러한 프로미스를 Promise.all에 전달하는 것은 이 작업을 할 수 있는 멋지고 인체공학적인 방법입니다. Promise.all는 프로미스의 배열을 수락하고 그 배열에 전달된 모든 프로미스가 결정되면 완료됩니다.

추가되는 두 레코드의 경우 트랜잭션 인스턴스의 store 인터페이스는 add()를 호출하고 데이터를 전달합니다. Promise.all 호출을 await하여 트랜잭션이 완료될 때 완료될 수 있습니다.

데이터 읽기

데이터를 읽으려면 openDB() 메서드를 사용하여 검색하는 데이터베이스 인스턴스에서 get() 메서드를 호출합니다. get()는 저장소 이름과 검색하려는 객체의 기본 키 값을 사용합니다. 다음은 기본적인 예시입니다.

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

add()와 마찬가지로 get() 메서드가 프로미스를 반환하므로 원하는 경우 await하거나 프로미스의 .then() 콜백을 사용할 수 있습니다.

다음 예시에서는 'test-db4' 데이터베이스의 'foods' 객체 저장소에 get() 메서드를 사용하여 'name' 기본 키로 단일 행을 가져옵니다.

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

데이터베이스에서 단일 행을 검색하는 것은 매우 간단합니다. 데이터베이스를 열고 데이터를 가져올 행의 객체 저장소와 기본 키 값을 지정하면 됩니다. get() 메서드가 프로미스를 반환하므로 이를 await할 수 있습니다.

데이터 업데이트

데이터를 업데이트하려면 객체 저장소에서 put() 메서드를 호출합니다. put() 메서드는 add() 메서드와 유사하며 add() 대신 사용하여 데이터를 만들 수도 있습니다. 다음은 put()를 사용하여 객체 저장소의 행을 기본 키 값으로 업데이트하는 기본적인 예시입니다.

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

다른 메서드와 마찬가지로 이 메서드는 프로미스를 반환합니다. put()를 트랜잭션의 일부로 사용할 수도 있습니다. 다음은 샌드위치와 계란의 가격을 업데이트하는 'foods' 저장소를 사용하는 예입니다.

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

항목이 업데이트되는 방식은 키를 설정하는 방법에 따라 다릅니다. keyPath를 설정하면 객체 저장소의 각 행이 인라인 키와 연결됩니다. 이전 예에서는 이 키를 기반으로 행을 업데이트하며, 이 상황에서 행을 업데이트할 때는 해당 키를 지정하여 객체 저장소에 있는 적절한 항목을 업데이트해야 합니다. autoIncrement를 기본 키로 설정하여 라인 외부 키를 만들 수도 있습니다.

데이터 삭제

데이터를 삭제하려면 객체 저장소에서 delete() 메서드를 호출합니다.

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

add(), put()와 마찬가지로 이 메서드를 트랜잭션의 일부로 사용할 수 있습니다.

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

데이터베이스 상호작용의 구조는 다른 작업과 동일합니다. Promise.all에 전달하는 배열에 tx.done 메서드를 포함하여 전체 트랜잭션이 완료되었는지 확인해야 합니다.

모든 데이터 가져오기

지금까지 스토어에서 한 번에 하나씩만 객체를 가져왔습니다. getAll() 메서드 또는 커서를 사용하여 객체 저장소 또는 색인에서 모든 데이터 또는 하위 집합을 검색할 수도 있습니다.

getAll() 메서드

객체 저장소의 모든 데이터를 검색하는 가장 간단한 방법은 다음과 같이 객체 저장소 또는 색인에서 getAll()를 호출하는 것입니다.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

이 메서드는 어떠한 제약도 없이 객체 저장소의 모든 객체를 반환합니다. 이는 객체 저장소에서 모든 값을 가져오는 가장 직접적인 방법이지만 유연성이 가장 낮습니다.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

이 예에서는 'foods' 객체 저장소에서 getAll()를 호출합니다. 그러면 'foods'의 모든 객체가 기본 키 순으로 반환됩니다.

커서 사용 방법

커서를 사용하면 여러 객체를 검색할 수 있습니다. 커서는 객체 저장소의 각 객체를 선택하거나 색인을 하나씩 생성하므로, 객체가 선택되었을 때 데이터로 무언가를 할 수 있습니다. 커서는 다른 데이터베이스 작업과 마찬가지로 트랜잭션에서 작동합니다.

커서를 만들려면 트랜잭션의 일부로 객체 저장소에서 openCursor()를 호출합니다. 이전 예시의 'foods' 저장소를 사용하여 객체 저장소의 모든 데이터 행을 통해 커서를 이동하는 방법은 다음과 같습니다.

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

이 경우 트랜잭션은 'readonly' 모드에서 열리고 openCursor 메서드가 호출됩니다. 후속 while 루프에서 커서의 현재 위치에 있는 행은 keyvalue 속성을 읽을 수 있으며 앱에 가장 적합한 방식으로 이러한 값에 대해 작업할 수 있습니다. 준비가 되면 cursor 객체의 continue() 메서드를 호출하여 다음 행으로 이동할 수 있으며 커서가 데이터 세트의 끝에 도달하면 while 루프가 종료됩니다.

범위 및 색인과 함께 커서 사용

색인을 사용하면 기본 키 이외의 속성으로 객체 저장소의 데이터를 가져올 수 있습니다. 모든 속성에 색인을 만들 수 있습니다. 색인은 색인의 keyPath가 되고, 해당 속성에 범위를 지정하고, getAll() 또는 커서를 사용하여 범위 내의 데이터를 가져올 수 있습니다.

IDBKeyRange 객체와 다음 메서드 중 하나를 사용하여 범위를 정의합니다.

upperBound()lowerBound() 메서드는 범위의 상한과 하한을 지정합니다.

IDBKeyRange.lowerBound(indexKey);

또는

IDBKeyRange.upperBound(indexKey);

이들은 각각 하나의 인수, 즉 상한 또는 하한으로 지정하려는 항목의 색인 keyPath 값을 사용합니다.

bound() 메서드는 상한과 하한을 모두 지정합니다.

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

이러한 함수의 범위는 기본적으로 포함됩니다. 즉, 범위의 제한으로 지정된 데이터가 포함됩니다. 이러한 값을 생략하려면 truelowerBound() 또는 upperBound()의 두 번째 인수로 전달하거나 bound()의 세 번째와 네 번째 인수로(하한값 및 상한값) 각각을 전달하여 범위를 제외로 지정합니다.

다음 예시에서는 'foods' 객체 저장소의 'price' 속성에 색인을 사용합니다. 이제 저장소는 범위의 상한과 하한에 대한 두 개의 입력이 있는 양식도 연결됩니다. 다음 코드를 사용하여 가격이 이러한 한도 사이에 있는 음식을 찾습니다.

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

예시 코드는 먼저 한도 값을 가져오고 한도가 존재하는지 확인합니다. 다음 코드 블록은 값에 따라 범위를 제한하는 데 사용할 메서드를 결정합니다. 데이터베이스 상호작용에서 평소와 같이 트랜잭션의 객체 저장소를 연 후 객체 저장소에서 'price' 색인을 엽니다. 'price' 색인을 사용하면 가격별로 항목을 검색할 수 있습니다.

그러면 코드는 색인에서 커서를 열고 범위를 전달합니다. 커서는 범위의 첫 번째 객체를 나타내는 프로미스를 반환하거나 범위 내에 데이터가 없는 경우 undefined를 반환합니다. cursor.continue() 메서드는 다음 객체를 나타내는 커서를 반환하고 범위 끝에 도달할 때까지 루프를 계속합니다.

데이터베이스 버전 관리

openDB() 메서드를 호출할 때 두 번째 매개변수에 데이터베이스 버전 번호를 지정할 수 있습니다. 이 가이드의 모든 예에서는 버전이 1로 설정되어 있지만 어떤 식으로든 수정해야 하는 경우 데이터베이스를 새 버전으로 업그레이드할 수 있습니다. 지정된 버전이 기존 데이터베이스 버전보다 높은 경우 이벤트 객체의 upgrade 콜백이 실행되어 새 객체 저장소와 색인을 데이터베이스에 추가할 수 있습니다.

upgrade 콜백의 db 객체에는 브라우저가 액세스할 수 있는 데이터베이스의 버전 번호를 나타내는 특수한 oldVersion 속성이 있습니다. 이 버전 번호를 switch 문에 전달하여 기존 데이터베이스 버전 번호를 기반으로 upgrade 콜백 내에서 코드 블록을 실행할 수 있습니다. 예를 들면 다음과 같습니다.

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

이 예에서는 데이터베이스의 최신 버전을 2로 설정합니다. 이 코드가 처음 실행되면 데이터베이스가 아직 브라우저에 없으므로 oldVersion0이고 switch 문은 case 0에서 시작됩니다. 이 예에서는 데이터베이스에 'store' 객체 저장소를 추가합니다.

요점: switch 문에서는 일반적으로 각 case 블록 뒤에 break가 있지만 여기서는 의도적으로 사용하지 않습니다. 이렇게 하면 기존 데이터베이스가 몇 버전 차이 나거나 존재하지 않는 경우 최신 상태가 될 때까지 코드가 나머지 case 블록까지 계속 진행됩니다. 따라서 이 예시에서 브라우저는 case 1를 통해 계속 실행되어 store 객체 저장소에 name 색인을 생성합니다.

'store' 객체 저장소에서 'description' 색인을 만들려면 다음과 같이 버전 번호를 업데이트하고 새 case 블록을 추가합니다.

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

이전 예에서 만든 데이터베이스가 여전히 브라우저에 있는 경우 이 작업이 실행되면 oldVersion2가 됩니다. 브라우저에서 case 0case 1를 건너뛰고 case 2의 코드를 실행하여 description 색인을 만듭니다. 그러면 브라우저에 namedescription 색인이 있는 store 객체 저장소가 포함된 버전 3의 데이터베이스가 있게 됩니다.

추가 자료

다음 리소스는 IndexedDB 사용에 관한 추가 정보와 컨텍스트를 제공합니다.

IndexedDB 문서

데이터 스토리지 한도