냐냐한 IT/냐냐한 실습 기록

IndexedDB API: IndexedDB 사용 - 3 (데이터 추가, 검색, 제거)

소소하냐 2022. 11. 10. 19:02
MDN 원문 참조: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#adding_retrieving_and_removing_data
날짜 : 2022.11.10 (문서 내용은 계속 변경되는 부분이라 정리한 날짜를 함께 기록)

위 참조 링크 내용을 정리하였습니다. 


[ 데이터 추가, 검색, 제거 (Adding, retrieving, and removing data) ] 

하위 목차

- 데이터베이스에 데이터 추가 (Adding data to the database)

- 데이터베이스에서 데이터 제거 (Removing data from the database)

- 데이터베이스에서 데이터 가져오기 (Getting data from the database)

- 데이터베이스의 항목 업데이트 (Updating an entry in the database)

- cursor 사용 (Using a cursor)

- 인덱스 사용 (Using an index)

- cursor의 범위와 방향 지정 (Specifying the range and direction of cursors)


- 데이터베이스에 작업을 하려면 트랜잭션(transaction) 시작이 필요 

 

트랜잭션(transaction)

-데이터베이스 객체에서 발생 (정리자 덧, indexedDB.open 의 결과로 받은 IDBDatabase 객체 : IDBDatabase.transaction ) 

- object store를 지정 (예 : db.transaction(["customers"]

- 트랜잭션에 들어가면, 데이터를 저장 및 요청하는 object store에 접근 가능 

 

- 사용 가능 모드 3가지 

   [ IDBFactory.open ] (정리자 덧, 예: indexedDB.open("MyTestDatabase", 3); )

   : versionchange - 데이터베이스의 "schema" 또는 구조 변경(object store 또는 index를 생성, 삭제)

 

   [ IDBDatabase.transaction  ] (정리자 덧, 예: db.transaction(["customers"], "readwrite"))

   : readonly - object store의 레코드를 읽기 

   : readwrite - object store의 레코드를 읽기, 변경(추가, 삭제, 수정)  

 

- IDBDatabase.transaction(storeNames, mode[optional])

   : storeNames - 접근하려는 object store의 배열로 정의된 scope  

     (모든 object store를 포괄하려면, 빈 배열 전달하면 가능, 하지만 빈 배열은 InvalidAccessError를 발생시킨다고 사양에 명시 되어있음)

   : mode - readonly 또는 readwrite (미지정 시 기본 readonly)

   : return - IDBIndex.objectStore 메서드를 포함한 transaction object

   : 정리자 덧, 예: db.transaction(["customers"], "readwrite") 

 

- 올바른 범위(scope)와 모드를 사용하여 데이터 액세스의 속도 향상 팁 : 

   : 필요한 object store들만 지정 : 겹치지 않는 범위로 여러 트랜잭션을 동시에 실행 가능  

   : 필요한 경우에만 readwrite 트랜잭션을 지정 : object store에 대해 하나의 readwrite 트랜잭션만 가능 

   : 참고 - IndexedDB 주요 특성 및 기본 용어(key characteristics and basic terminology)(원문) 문서의 transaction 

 

- 트랜잭션의 수명

   : event loop에 밀접하게 연결 

   : 트랜잭션을 만들고 그것을 사용하지 않고 이벤트 루프로 돌아가면 트랜잭션은 비활성화

   : 트랜잭션 활성을 유지하는 유일한 방법은 트랜잭션에 요청을 하는 것 

   : 요청이 완료되면 DOM event를 받게 되고, 요청이 성공했다고 가정하면, 해당 callback 중에 트랜잭션을 확장할 수 있는 또 다른 기회

   : 확장하지 않고 이벤트 루프로 돌아가면 비활성 됨

   : 보류중인 요청이 있는 한 트랜잭션은 활성 상태를 유지 

   : TRANSACTION_INACTIVE_ERR 에러 코드가 표시되기 시작하면 뭔가 잘 못 된 것

 

- 3가지 타입의 DOM event를 받음: error, abort, complete

   : error

     - 에러 이벤트는 버블링되며, 생성된 모든 요청에서 에러 이벤트를 받는다. 

     - 에러의 기본 동작은 에러가 발생한 트랜잭션을 중단하는 것

     - 에러 이벤트에서 stopPropagation()을 먼저 호출하여 다른 작업을 해서 에러를 처리하지 않는 한, 전체 트랜잭션이 롤백 

     - 세분화된 오류 처리가 번거로운 경우, 데이터베이스에 모든 오류 처리기를 추가 

   : abort

     - 에러 이벤트를 처리하지 않거나 트랜잭션에서 abort()를 호출하면, 트랜잭션은 롤백되고 트랜잭션에 abort 이벤트가 발생 

   : complete 

     - 보류중인 요청이 완료되어야 complete 이벤트를 받게 됨 

 

   * 많은 데이터베이스 작업을 하는 경우, 개별 요청보다 트랜잭션을 추적하는 것이 더 확실한 판단 

// 작성자 덧, 위 "많은 데이터베이스 작업을 하는 경우, 개별 요청보다 트랜잭션을 추적하는 것이 더 확실한 판단"에 대한 추가 설명
const transaction = db.transaction(["customers"], "readwrite"); 

// 트랜잭션을 추적하는 것이 더 확실한 판단 
transaction.oncomplete = (event) => { };
transaction.onerror = (event) => { };

const objectStore = transaction.objectStore("customers"); 

// 개별 요청(추가,제거, 읽기 등)보다 위 트랜잭션을 추적하는 것이 더 확실
const request = objectStore.add(customer); // 추가 
const request = objectStore.delete("444-44-4444"); // 제거 
const request = objectStore.get("444-44-4444"); // 읽기

 

데이터베이스에 데이터 추가 (Adding data to the database)

const transaction = db.transaction(["customers"], "readwrite"); 
// 주의 :이전 실험적 구현에서는 "readwrite" 대신 deprecate된 상수 IDBTransaction.READ_WRITE를 사용
// 이러한 구현을 지원하려는 경우, 다음과 같이 작성 : 
// const transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

 

- 트랜잭션에서 object store 가져오기. (트랜잭션은 생성할 때 지정한 object store만 가질 수 있음) 

- 그런 다음, 원하는 모든 데이터를 추가 가능 (objectStore.add(customer))

// 모든 데이터가 데이터베이스에 추가되면 할 것
transaction.oncomplete = (event) => { console.log("All done!"); };

transaction.onerror = (event) => { console.log("에러 처리를 잊지 말 것!"); };

const objectStore = transaction.objectStore("customers");
customerData.forEach((customer) => {
  const request = objectStore.add(customer); //<--- 추가!!!!
  request.onsuccess = (event) => {
    // event.target.result === customer.ssn; 
  };
});

- add() 의 result : 추가된 값(value)의 key (event.target.result === customer.ssn; )

   (위 예의 경우, 추가된 객체(cusomer)의 ssn, object store(customers)의 key path가 ssn 이므로) 

- add() 함수는 데이터베이스에 같은 key를 가진 object가 없어야 함

 

- 기존 항목을 수정하거나 데이터 존재 여부를 신경쓰지 않으려면, 아래 데이터베이스에서 항목 업데이트(Updating an entry in the database) 부분에서 볼 수 있는 것 처럼 put() 함수를 사용 

 

데이터베이스에서 데이터 제거 (Removing data from the database)

데이터 삭제는 매우 비슷합니다 : 

const request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = (event) => {
  // 없어졌음!
};

 

데이터베이스에서 데이터 가져오기 (Getting data from the database) 

- 여러 방법으로 조회 가능 

  : get() - key를 사용하여 단일 값 조회 

  : openCursor() / getAll() / getAllKeys() - 모든 값 조회 (자세한 설명은 cursor 사용 항목에서 확인

  : index() - 설정된 index key로 get(), openCursor() 조회 (자세한 설명은 인덱스 사용 항목에서 확인)

- get() : key 제공 필수 (objectStore.get("444-44-4444"))

const transaction = db.transaction(["customers"]);
const objectStore = transaction.objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // 에러 처리!
};
request.onsuccess = (event) => {
  // request.result로 처리
  console.log(`Name for SSN 444-44-4444 is ${request.result.name}`);
};

위 코드 축약 : 

db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = (event) => {
  console.log(`Name for SSN 444-44-4444 is ${event.target.result.name}`);
};

위 코드 설명 : 

- 하나의 object store만 있으므로, 트랜잭션에 필요한 object store 목록이 아닌 string으로 이름을 전달

- 읽기만 하므로, "readwrite" 트랜잭션이 필요음. mode를 지정하지 않았기 때문에 기본 "readonly" 트랜잭션이 제공

- 여기에 또 다른 미묘한 점은 실제로 request object를 변수에 저장하지 않는 다는 것입니다. 

- DOM event는 target으로 request를 갖기 때문에, result 속성을 얻기 위해 event를 사용 

  (정리자 덧, event.target = request 할당됨, 그렇기 때문에 request.result.name 와 같이 event.target.result.name을 사용 가능) 

 

데이터베이스의 항목 업데이트 (Updating an entry in the database)

- 데이터 가져오기 및 업데이트 후 IndexedDB에 다시 넣기(put) 

이전 예제 업데이트 : 

// 1. objectStore를 생성, 쓰기가 필요하므로 readwrite 트랜잭션을 지정
const objectStore = db.transaction(["customers"], "readwrite").objectStore("customers");
// 2. ssn 값(444-44-4444)으로 식별되는 customer 레코드 요청
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // 에러 처리!
};
request.onsuccess = (event) => {
  // 3. 변수(data)에 요청의 결과(업데이트할 이전 값)를 넣고 
  const data = event.target.result;

  // 4. object의 age 속성을 업데이트
  data.age = 42;

  // 5. objectStore에 업데이트된 data를 넣는 두 번째 요청(requestUpdate)
  const requestUpdate = objectStore.put(data);
  requestUpdate.onerror = (event) => {
    // 에러 처리
  };
  requestUpdate.onsuccess = (event) => {
    // 성공 - 6. 데이터가 업데이트 됨! (이전 값 덮어씀)
  };
};

 

cursor 사용 (Using a cursor)

- get() 사용 : 조회하려는 key를 알아야 함 

- store의 모든 값을 단계별로 원하면 cursor를 사용 :

const objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(`Name for SSN ${cursor.key} is ${cursor.value.name}`);
    cursor.continue();
  } else {
    console.log("No more entries!");
  }
};

- openCursor(query[optional], direction[optional]) : 

   - query[optional] : key range object 사용으로, 조회되는 항목의 범위(range)를 제한하여 1분 안에 가져올 수 있음

   - direction[optional] : 반복하려는 방향(direction)을 지정 (기본: 오름차순) 

   - success callback(onsucess(event)) : cursor 객체 자체는 요청의 결과입니다(위에서는, event.target.result) 

     - 실제 kev value는 cursor 객체의 key와 value 속성 (cursor.key, cursor.value.name)

     - 계속 하려면, cursor에서 continue()를 호출 

     - 데이터의 끝(또는 openCursor() 요청에 일치 항목이 없으면) success callback을 받지만, result 속성은 undefined 

 

- cursor의 일반적인 패턴, object store의 모든 object를 조회하고 배열에 추가하는 것 : 

const customers = [];

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    console.log(`Got all customers: ${customers}`);
  }
};

- 참고 : 또는, 이런 것을 처리하기 위해 getAll()을 사용(그리고 getAllKeys()). 다음 코드는 정확히 동일한 작업 : 

objectStore.getAll().onsuccess = (event) => {
  console.log(`Got all customers: ${event.target.result}`);
};

 

- object는 느리게 생성되므로, cursor의 value 속성을 보는 것과 관련된 성능 비용이 있음,

- 예를 들어 getAll()을 사용할 때, 브라우저는 모든 object를 한번에 생성해야 합니다. 

- 예로, 각각의 key를 보려고 하면, getAll()을 사용하는 것 보다 cursor을 사용하는 것이 더 효율적

- object store의 모든 object의 배열을 얻으려면 getAll()을 사용

 

 

 

 

인덱스 사용 (Using an index)

- 지금까지의 예제에서, name으로 customer 데이터를 찾으려면, 찾을 때 까지 데이터베이스에 있는 모든 SSN을 반복해야 함

- 이 방식은 매우 느릴 수 있으므로, index를 사용 

// 먼저 request.onupgradeneeded에 index를 생성했는지 확인:
// objectStore.createIndex("name", "name");
// 없으면 DOMException 발생. 

const index = objectStore.index("name");

index.get("Donna").onsuccess = (event) => {
  console.log(`Donna's SSN is ${event.target.result.ssn}`);
};

- index("인덱스명").get("이름")

   : "name" 인덱스는 고유하지 않기 때문에, "Donna"라는 이름의 항목은 하나 이상일 수 있음. 이런 경우 항상 가장 낮은 key값의 항목을 반환

 

- index("인덱스명").openCursor() 

   : 주어진 인덱스명("name")으로 모든 항목에 접근하려면 cursor를 사용 

   : 두 가지 유형의 cursor

     - 일반(normal) cursor는 object store의 object에 index 속성을 매핑 

     - key cursor는 index 속성을 object store의 object를 저장하는데에 사용하는 key에 매핑.

   : 차이점은 다음 예제 확인

// normal cursor : 모든 customer 레코드 객체 얻기 
index.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key는 "Bill"과 같은 이름, cursor.value는 전체 객체 
    console.log(`Name: ${cursor.key}, SSN: ${cursor.value.ssn}, email: ${cursor.value.email}`);
    cursor.continue();
  }
};

// key cursor : customer 레코드 객체의 key 얻기
index.openKeyCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key는 "Bill"과 같은 이름, cursor.value는 SSN 
    // 저장된 객체의 나머지를 직접 가져올 방법은 없음. 
    console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
    cursor.continue();
  }
};

 

cursor의 범위와 방향 지정 (Specifying the range and direction of cursors)

- 표시되는 값의 범위를 제한하려면 IDBKeyRange 객체를 openCursor() 또는 openKeyCursor()에 첫 번째 인수로 전달 

   (const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true); index.openCursor(boundKeyRange).onsuccess...)

- 키 범위 (key range) 

   : 단일 key 

   : lower(이하,미만) / upper(이상,초과)

   : lower(이하,미만)and upper(이상,초과) (정리자 덧, between) 

- 경계는 "closed"(예. 주어진 값을 포함한 key 범위) 또는 "open"(예. 주어진 값이 포함되지 않은 key 범위) 

- 다음 예를 확인 : 

// "Donna"만 일치 
const singleKeyRange = IDBKeyRange.only("Donna");

// "Bill"을 포함한, "Bill" 이전(past) 모든 것과 일치  
const lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// "Bill"을 포함하지 않는, "Bill" 이전(past) 모든 것과 일치 
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// "Donna"를 포함하지 않는, 최대 모든 것과 일치 
const upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// "Donna"를 포함하지 않는, "Bill"과 "Donna" 사이의 모든 것과 일치 
const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// key 범위 중 하나를 사용하려면, openCursor()/openKeyCursor()의 첫 번째 인수로 전달
index.openCursor(boundKeyRange).onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 일치한 것에 작업 
    cursor.continue();
  }
};

- 오름차순(cursor의 기본 방향)이 아닌 내림차순으로 반복 : 방향 전환은 openCursor()의 두번째 인수로 prev를 전달

objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

- 방향(direction)만 지정하고 결과 제한은 하지 않으려면, 첫 번째 인수에 null을 전달 : 

objectStore.openCursor(null, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

- "name" 인덱스는 unique가 아니기 때문에, 같은 이름의 여러 항목이 있을 수 있음

- 인덱스에 대한 cursor 반복 중, 중복을 필터링하려면, direction 파라미터로 nextunique (또는 뒤로 이동하는 경우 prevunique)를 전달

nextunique 또는 prevunique가 사용될 때, 가장 낮은 key를 갖는 항목이 항상 반환

index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

- 유효한 direction 인수(argument)는 "IDBCursor Constants(원문)"를 참고 

 


IndexedDB 정리 목록

IndexedDB API: Intro (개요)

IndexedDB API: IndexedDB 사용 - 1 (개요)

IndexedDB API: IndexedDB 사용 - 2 (저장소(store) 생성 및 구조화)

IndexedDB API: IndexedDB 사용 - 3 (데이터 추가, 검색, 제거)

IndexedDB API: IndexedDB 사용 - 4 (버전 변경 시 다른 탭, 보안, 브라우저 종료 시 경고 등 나머지 내용)

IndexedDB API: IndexedDB 주요 특징 및 기본 용어