본문 바로가기

Develop/Frontend 가이드

[FE] Web API - Client-side storage : IndexedDB

반응형

IndexedDB
IndexedDB

Web API - Client-side storage

브라우저 환경에서 데이터를 저장하는 방법을 시리즈 글로 소개하고 있습니다.

IndexedDB

IndexedDB 는 클라이언트에 데이터를 저장하는 방법 중 하나입니다. 따라서 네크워크 연결 여부와 관계없이 데이터를 활용할 수 있어, 모바일 플랫폼의 웹 앱에서 활용하기 좋습니다.

IndexedDB 는 CookielocalStorage 와 sessionStorage 와 달리 복잡한 데이터를 저장하기 위한 목적의 저장소입니다. 그래서 원리나 사용하는 방법도 다른 저장소에 비해 복잡합니다.

1. IndexedDB 초기화한다.

쿠키는 서버의 Set-Cookie 헤더에서 설정하고 localStorage 와 sessionStorage 는 사용하는 순간 저절로 초기화하기 때문에 굳이 초기화할 필요가 없었는데, IndexedDB 는 연결하고 스키마를 정비하는 등의 초기화 과정이 필요합니다.

IndexedDB 는 기본적인 사항들이 로드되기 전에 사용하면 오류가 발생할 수 있으므로, 전역 이벤트인 window.onload 이후에 초기화하는 걸 추천드립니다.

// 전역객체 window 를 통해 IDBFactory.open() 메서드로 연결한다. open 메서드로 반환된 request 의 인터페이스는 IDBOpenDBRequest 이다.
const request = window.indexedDB.open('notes_db', 1);

클라이언트 저장소 내에 동일한 이름의 DB 가 없으면 새로운 DB 가 생성됩니다. DB 가 새로 만들어지면 DB 스키마를 새롭게 생성해야 하기 때문에 업데이트로 간주하고 upgradeneeded 이벤트가 발생하며, upgradeneeded 이벤트를 처리하는 request.onupgradeneeded 이벤트 핸들러에서 스키마를 설정하는 코드를 아래와 같이 작성하면 됩니다.

request.addEventListener('upgradeneeded', event => {
  // 오픈한 DB 에 접근할 수 있는 IDBDatabase 인터페이스의 객체이다.
  const db = event.target.result;

  // createObjectStore 메서드로 db 에 객체를 저장할 수 있는 store 을 생성한다. 관계 DB 에서 테이블을 생성하는 것과 비슷하다.
  // 'autoIncrement' 을 true 로 설정하면 데이터를 추가할 때마다 id 가 자동으로 생성된다.
  // 'keyPath' 를 설정하면 데이터를 상징하는 id 를 저장할 객체 내 속성으로 지정할 수 있다.
  const noteStore = db.createObjectStore('notes_os', { keyPath: 'id', autoIncrement:true });

  // 생성된 store 의 스키마를 생성한다.
  noteStore.createIndex('title', 'title', { unique: false });
  noteStore.createIndex('body', 'body', { unique: false });

  // 위에 생성한 notes_os 의 스키마로 인해 저장되는 데이터는 아래와 같은 구조가 된다.
  // {
  //   id: 4,
  //   title: '어쩌다가',
  //   body: '로라엠포지엄',
  // }
});

만약 DB 가 이미 존재한다면 버전을 비교합니다. 그리고 버전이 높아졌다면 upgradeneeded 이벤트를 발생시켜 request.onupgradeneeded 이벤트 핸들러에서 아래와 같이 스키마를 새로운 버전에 맞게 다시 구축합니다.

const request = window.indexedDB.open('notes_db', 2);

request.addEventListener('upgradeneeded', event => {
  // 오픈한 DB 에 접근할 수 있는 db 객체
  const db = event.target.result;

  // 버전 업데이트에 대응하는 코드를 여기에 작성한다.
});

DB 의 버전을 '1.2' 와 같이 소수점을 사용하면 '1' 로 내림되어 버리니, DB 버전은 정수를 사용해야 합니다.

upgradeneeded 이벤트를 처리하고 나서 DB 초기화가 성공했으면 onsuccess 이벤트 헨들러가 호출되고, 초기화가 실패했으면 onerror 가 호출됩니다.

const db;

const request = window.indexedDB.open('notes_db', 1);

request.onerror = function() {
  // DB 초기화에 실패했을 때 대응하는 코드를 작성한다.
  // 사용자가 DB 가 생성되는걸 허락하지 않으면 에러가 발생한다.
  // 현재보다 낮은 버전으로 연결을 시도하면 에러가 발생한다.
};

request.onsuccess = function() {
  db = request.result;
  // DB 초기화에 성공했을 때 대응하는 코드를 작성한다.
};

2. 초기화한 IndexedDB 에 데이터를 get, add, put, delete 한다.

IndexedDB 도 다른 DB 와 마찬가지로 무결성을 보장하기 위해 트랙잭션이라는 개념이 있습니다. 따라서 IndexedDB 에 접근하려면 아래와 같이 트랙잭션 내에서 작업해야 합니다.

// DB 에 추가할 새로운 데이터다.
const item = {
  title: '새로운',
  body: '즐거운 나날',
};

// store 에 read 와 write 할 수 있는 트랙잭션을 생성한다.
const transaction = db.transaction(['notes_os'], 'readwrite');
// 트랙잭션을 사용해 store 에 접근한다.
const noteStore = transaction.objectStore('notes_os');
// store 에 새로운 데이터를 추가한다.
const request = noteStore.add(item);

// IDBTransaction.oncomplete
transaction.oncomplete = function() {
  // 트랜잭션이 성공적으로 완료되었을 때 호출된다.
}

// IDBTransaction.onerror
transaction.onerror = function() {
  // 트랜잭션에서 오류가 발생했을 때 호출된다.
}

// IDBRequest.onsuccess
request.onsuccess = function() {
  // 요청이 성공적으로 완료되었을 때 호출된다.
  // 트랙잭션의 이벤트 헨들러가 아니기 때문에 데이터가 저장되었음을 보장하지는 않는다.
  // request.result 에 noteStore.add() 메서드 수행 결과가 보관되어 있다.
  const newNodeId = request.result.id; 
}

transaction 을 열 때 지정할 수 있는 mode 는 readonly, readwrite, versionchange 를 사용할 수 있습니다. versionchange 모드는 데이터만이 아니라 store 의 추가, 수정, 삭제도 가능합니다. 즉 DB 스키마를 수정할 수 있는 모드입니다. 스키마를 변경할 수 있기 때문에 다른 트랙잭션과 동시 진행이 전혀 불가능하고 단독으로 트랙잭션이 수행됩니다. 모드를 지정하지 않고 트랙잭션을 생성하면 readonly 모드로 생성됩니다. 트랙잭션 모드는 DB 성능에 직접적으로 영향이 줍니다. 따라서 최대한 동시에 진행하기 좋은 readonly 모드로 트랙잭션 모드를 생성하면 좋습니다.

트랙잭션의 수명은 이벤트 루프에 의해 결정됩니다. 만약 트랙잭션을 생성하고 아무런 요청을 하지 않고 함수를 빠져나가거나 해서 이벤트 루프가 실행되면, 트랙잭션은 바로 비활성화됩니다. 이후 트랙잭션에 접근하게 되면 에러가 발생합니다. 트랙잭션의 수명을 유지하려면 반드시 트랙잭션을 사용해 DB 에 요청을 하고 요청을 수행하거나 요청이 대기하고 있어야 하며, 만약 수행하는 요청도 없고 대기중인 요청도 없이 이벤트 루프가 실행되면 트랙잭션은 비활성화됩니다.

일반적인 DB 의 트랙잭션과 동일하게 IndexedDB 의 트랙잭션도 수행 도중 실패하게 되면 수행했던 내용을 DB 에 반영하지 않고 변경을 취소해서 이전 상태를 유지합니다.

읽기

const request = noteStore.get(item.id);

추가

const request = noteStore.add(item);

삭제

const request = noteStore.delete(item);

수정

// store 스키마를 생성할 때 지정한 key 와 동일한 객체가 업데이트된다. 
const request = noteStore.put(item);

cursor

만약 store 에 저장된 모든 객체를 순회하고 싶다면, 아래와 같이 cursor 를 사용하면 됩니다.

// store 에 read 와 write 할 수 있는 트랙잭션을 생성한다.
const transaction = db.transaction(['notes_os'], 'readwrite');
// 트랙잭션을 사용해 store 에 접근한다.
const noteStore = transaction.objectStore('notes_os');

noteStore.openCursor().onsuccess = function(event) {
  // cursor 는 현재 데이터다.
  var cursor = event.target.result;
  // cursor 에 데이터가 있는지 확인한다.
  if (cursor) {
    // cursor 가 가리키는 데이터의 키다.
    console.log(cursor.key);
    // cursor 가 가리키는 데이터의 값이다.
    console.log(cursor.value);
    // 다음 데이터로 cursor 를 이동한다.
    current.continue();
  }
  else {
    // cursor 가 순회를 마친 경우
  }
};

openCursor 메서드는 openCursor(query, direction) 처럼 query 문을 적용해서 전체 중 일부분만 순회할 수도 있고, direction 을 설정해서 순회의 방향을 결정할 수도 있습니다. 더 자세한 내용은 IDBKeyRange 글IDBCursorDirection 글을 참고해주시길 바랍니다.

만약 순회가 아니라 모든 데이터를 한번에 조회하고 싶다면 아래와 같이 조회하면 됩니다.

noteStore.getAll().onsuccess = function(event) {
  // 조회 결과는 event.target.result 에 있다.
  console.log(event.target.result);
};

위와 같이 getAll() 로 조회하면 지연 로딩 효과를 볼 수 없어서 cursor 보다 초기화 비용이 커서 대기가 길어질 수 있습니다.

index

objectStore.get() 메서드는 store 의 키로 데이터를 조회하지만, objectStore.index() 메서드를 사용하면 store 스키마의 인덱스로 데이터를 조회할 수 있습니다.

request.addEventListener('upgradeneeded', event => {
  // ...
  noteStore.createIndex('name', 'name', { unique: false });
});

// noteStore 의 name 인덱스로 데이터를 조회한다.
var index = noteStore.index('name');
index.get('gun').onsuccess = function(event) {
  console.log(event.target.result);
};

// noteStore 에 존재하지 않는 인덱스를 사용하면 에러가 발생한다.
// var index = noteStore.index('error');

index 는 아래와 같이 두 가지 타입의 cursor 로 데이터를 순회할 수 있습니다.

// index 는 일반적인 cursor 로 순회할 수 있다.
index.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
};

// 또는 keyCursor 로 순회할 수 있다.
index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log(cursor.key); // cursor.key 는 index 를 생성할 때 지정한 인덱스의 값이다.
    console.log(cursor.primaryKey); // cursor.primaryKey 는 store 스키마에서 지정한 키 값이다.

    // cursor.key 와 cursor.primaryKey 를 분리해서 사용하는 이유는 
    // cursor.key 는 cursor.primaryKey 와 달리 값이 중복될 수 있기 때문이다.

    cursor.continue();
  }
};

주의! 브라우저가 긴급하게 닫히면 트랜잭션이 정상적으로 종료되지 않을 수 있기 때문에, 매 트랜잭션마다 DB 무결성을 유지하도록 트랜잭션 요청을 구성해야 합니다.

반응형