IndexedDB는 파일이나 blob와 같은 많은 양의 구조화된 데이터를 클라이언트에 저장하기 위한 로우 레벨 API이다.
IndexedDB API는 인덱스를 사용하기 때문에 데이터를 고성능으로 탐색할 수 있다.
Web Storage는 적은 양의 데이터를 저장하는데 유용하지만 많은 양의 구조화된 데이터에는 적합하지 않은데, 이런 상황에서 IndexedDB를 사용할 수 있다.
이미지 출처 : 생활코딩 (유튜버)
IndexedDB는 관계형 데이터베이스와 같이 트랜잭션을 사용하는 데이터베이스 시스템이다.
그러나 IndexedDB는 RDBMS의 고정된 테이블을 사용하지 않고, 대신 JavaScript 기반의 객체자료형을 사용하는 데이터베이스이다.
IndexedDB의 데이터는 인덱스 키를 사용해 저장하고 검색할 수 있으며, The structured clone algorithm↗을 지원하는 객체라면 모두 저장할 수 있다.
사용하려면 데이터베이스 스키마를 지정하고, 데이터베이스와 통신을 연 후에, 일련의 트랜잭션을 통해 데이터를 가져오거나 업데이트해야 한다.
클라이언트에 저장되는 db인 만큼 버전관리또한 클라이언트에서 관리한다.
DB자체 또는 스키마가 변경되면 버전을 변경해야한다.
DB를 연결할 때 발생하는 대표적인 이벤트 3가지를 알아보자.
동작 순서는 다음과 같다.
이미지출처 : Hussein Nasser (유튜버)
다음의 코드를 보자.
const request = indexedDB.open("notes");
request.addEventListener("upgradeneeded", (e) => {
alert("upgradeneeded");
});
request.addEventListener("success", (e) => {
alert("success");
});
request.addEventListener("error", (e) => {
alert("error");
});
위의 코드를 처음 실행하면 "notes" 라는 DB가 없기 때문에 upgradeneeded
이벤트가 발생한다.
그 후, "notes" DB가 만들어지고 success
이벤트가 발생한다.
따라서 "upgradeneeded" -> "success" alert창이 두 번 연속으로 나타난다.
개발자 도구를 확인해보면 "note" 라는 DB가 만들어 진 것을 볼 수 있다.
만약 위의 코드를 한 번 더 실행하면 이미 DB가 생성되었기 때문에 upgradeneeded
이벤트는발생하지 않고 success
이벤트만 발생한다.
indexedDB.open()
메서드의 두 번째 매개변수로 버전을 지정할 수 있다.
기본값은 1이다.
DB 객체는 이벤트 객체의 target.result
로 가져올 수 있다.
let db = {};
const request = indexedDB.open("notes");
request.addEventListener("upgradeneeded", (e) => {
db = e.target.result;
console.log(`DB name : ${db.name}, DB Version : ${db.version}`);
});
request.addEventListener("success", (e) => {
db = e.target.result;
console.log(`DB name : ${db.name}, DB Version : ${db.version}`);
});
request.addEventListener("error", (e) => {
console.error(e.target.error);
});
DB 객체의 클래스 이름은 IDBDatabase
이다.
Object Store은 데이터가 저장될 테이블이라고 보면 된다.
Object Store은 IDBDatabase.createObjectStore()
으로 생성할 수 있다.
첫 번째 매개변수는 name
, 두 번재 매개변수는 option
이다.
option
으로 primary key 역할을 하는 KeyPath
를 지정할 수 있고 해당 키에 autoIncrement
를 설정할 수 있다.
만약 KeyPath
를 지정하지 않은채 값만 넣으려면 Object Store가 out-of-line keys를 사용해야 한다는데 굳이 궁금하지 않아서 찾아보지 않았다.
그냥 KeyPath
를 사용할꺼다.
db = e.target.result;
const aNote = db.createObjectStore("personal_notes", {
keyPath: "id",
autoIncrement: true,
});
실행한 후 개발자 도구를 확인해 보자.
CRUD 작업은 transaction 객체 위에서 이루어 진다.
따라서 transaction 객체부터 만들어야 한다.
IDBDatabase.transaction()
으로 만들 수 있다.
note
객체의 형식이 다음과 같다고 가정해보자.
note = {
title: "title1",
content: "content1",
};
transaction(storeNames)
transaction(storeNames, mode)
transaction(storeNames, mode, options)
mode
의 값으로 readonly
와 readwrite
가 올 수 있으며, 파이어폭스일 경우 readwriteflush
라는게 있는데 더 알고 싶다면 공식문서를 참조하자.
options
으로 default
, strict
, relaxed
을 선택 할 수 있다.
성능과 속도를 높이려면 relaxed
를 선택하고
데이터 손실을 줄이려면 strict
를 선택하는 것을 권장한다.
단일 Object Store를 위한 트랜잭션을 사용하려면 storeNames
파라미터에 Object Store의 이름을 문자열로 전달하면 되고 여러 개의 Object Store를 위한 트랜잭션을 사용하려면 배열로 전달하면 된다. (배열의 아이템 개수가 1개라면 자동으로 단일 Object Store로 선택된다)
또는 아래와 같이 IDBDatabase.objectStoreNames
프로퍼티를 사용하여 전달할 수도 있다.
db = e.target.result;
const transaction = db.transaction(db.objectStoreNames, "readwrite");
transaction.addEventListener("complete", () => {
console.log("트랜잭션 완료");
});
complete
이벤트 말고 error
이벤트도 있는데 error
이벤트 발생시 자동으로 버블링 되어 IDBDatabase 객체로 전달되기 때문에 여기에선 에러 이벤트 핸들러를 사용할 필요가 없다.
다음은 note
객체를 각각의 Object Store에 넣는 코드이다.
const note = {
title: "my title",
content: "my content",
};
db = e.target.result;
const transaction = db.transaction(db.objectStoreNames, "readwrite");
const objectStore = transaction.objectStore("personal_notes");
const request = objectStore.add(note);
request.addEventListener("error", (e) => {
console.log("삽입 실패", e);
});
request.addEventListener("success", (e) => {
console.log("삽입 완료", e);
});
transaction.addEventListener("complete", (e) => {
console.log("트랜잭션 완료", e);
});
트랜잭션위에서 동작하는 여러개의 Object Store중에서 하나를 선택한다.
(위의 코드에서는 트랜잭션 위에 하나의 Object Store만 있다.)
그 후 add()
메서드를 통해 데이터를 추가한다.
위의 코드를 실행해보자.
add()
메서드는 IDBRequest 객체를 반환하며 이 객체는 error 와 success 이벤트를 가지고 있다.
이 이벤트를 통해 삽입이 정상적으로 이루어 졌는지를 확인할 수 있다.
삽입된 객체를 보면 id
키가 자동으로 생성된 것을 볼 수 있다.
이는 위에서 Object Store 스키마를 정의할때 KeyPath: "id"
를 설정했기 때문이다.
autoIncrement
도 설정했기 때문에 한 번 더 저장한다면 id
가 2인 데이터가 삽입될 것이다.
확인해 보자.
나머지는 쉽다.
Object Store 객체의 get()
, put()
, delete()
메서드를 이용하면 된다.
각각의 메서드들은 IDBRequest
객체를 반환하기 때문에 반환받은 IDBRequest
객체에 success
이벤트 핸들러를 달아서 추가 동작을 설정할 수 있다.
커서의 위치에 따라 데이터를 순차적으로 가져올 수 있다.
마치 제너레이션과 비슷하다.
const request = objectStore.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.value contains the current record being iterated through
// this is where you'd do something with the result
cursor.continue();
} else {
// no more results
}
};