현재에 꾸준히 기술적인 부분을 디벨롭하고 있다. 학습적인 개선이기 때문에 따로 백엔드가 없었고, 실제 많은 데이터를 처리하기 위해 프론트단에서 어떻게 처리할지를 고민하게 됐다. 간단한 구조의 데이터라면 웹 스토리지에 저장하여 관리했겠지만, 객체 배열과 같은 복잡한 데이터를 웹 스토리지에서 관리하는 것이 이상하다고 생각이 들었다. 그래서 이번 기회에 IndexedDB
를 학습하고 정리해보려고 한다.
웹 브라우저에 데이터를 저장하는 방식은 크게 3가지로 나눌 수 있다. 대표적으로 Cookie
, Storage
그리고 indexedDB
가 존재한다. 아래와 같은 차이를 가지고 있다. IndexedDB
는 다른 저장 방식과 달리 비동기로 동작한다는 점과 데이터 타입에 제한이 없어 객체도 저장할 수 있다는 특징이 있다.
생활코딩 - 웹브라우저에서 데이터를 저장하기 - IndexedDB
파일이나 블롭과 같이 많은 양의 구조화된 데이터를 클라이언트에 저장하기 위한 로우 레벨 API이다. 여러개의 데이터를 그룹화하는 Object Store
가 존재하고, Obejct Store
여러개를 Database로 만든다.
먼저, indexedDB
를 생성해보자. 아래 코드와 같이 open
메서드를 통해 IndexedDB
를 열 수 있다. 해당 메서드는 두 개의 인자를 받는데, indexedDB
에 사용할 이름과 버전이다.
const dbRequest = indexedDB.open("이름", 1);
이렇게 생성한 IndexedDB
에 이벤트를 등록하여 상태를 관리한다. 각각의 이벤트는 아래와 같은 상황에서 발생한다.
success
- IndexedDB
가 성공적으로 만들어졌을 때error
- IndexedDB
에 오류가 발생했을 때upgradeneeded
- 데이터 업그레이드가 필요할 때 (버전이 변경될 때)dbRequest.addEventListener('success', function(event){
db = event.target.result;
});
dbRequest.addEventListener('error', function(event){
const error = event.target.error;
console.error('error', error.name);
});
dbRequest.addEventListener('upgradeneeded', function(event){
db = event.target.result;
let oldVersion = event.oldVersion;
if(oldVersion < 1){
db.createObjectStore('report', {keyPath:'uuid', autoIncrement:true});
}
});
또한, createObjectStore('이름', 옵션)
메서드를 사용하여 Obejct Store
를 생성할 수 있다. 데이터베이스에서 테이블을 만드는 과정이라고 생각하면 된다. 내가 만드는 report
라는 이름의 Object Store
는 keyPath(식별자)의 이름을 uuid
, 자동으로 데이터가 추가될 때마다 증가하게 설정을 해둔 상태이다.
++ 추가적으로 index에 대해 간단하게 정리해보자.
index는 테이블에서 원하는 데이터를 쉽고 빠르게 찾기 위해 사용한다. 특정 데이터를 탐색하는 과정에서 성능적인 측면에서 효율적이고, 대량의 데이터를 처리할 때 효과적이다.
createIndex(indexName, keyPath, options)
형태로 사용할 수 있고 아래 최종 코드에 예시가 포함되어 있다.
/**
* 데이터베이스를 초기화합니다.
* @returns {Promise<boolean>} 데이터베이스 생성 성공 여부
*/
const initDB = (): Promise<boolean> => {
return new Promise((resolve) => {
request = indexedDB.open("edu-db", 1);
request.onupgradeneeded = () => {
db = request.result;
if (!db.objectStoreNames.contains(Stores.REPORT)) {
console.log("[IndexedDB] : create report Store!");
const _store = db.createObjectStore(Stores.REPORT, {
keyPath: "uuid",
autoIncrement: true,
});
_store.createIndex("userId", "userId", { unique: false });
}
};
/** onsuccess event를 감지합니다. */
request.onsuccess = () => {
db = request.result;
version = db.version;
console.log("[IndexedDB] : onsuccess()... version: ", version);
resolve(true);
};
/** onerror event를 감지합니다. */
request.onerror = (e) => {
console.error("[IndexedDB] : onerror()", e);
resolve(false);
};
});
};
이제 만들어진 IndexedDB
를 직접 사용해보자. 먼저, 데이터를 추가하고 조회할 때 IndexedDB
에서는 transaction()
을 사용한다. 기본적으로 다음과 같은 형태를 가지고 있다.
transaction(storeNames, mode?, options?)
mode
(옵션) - readonly 디폴트readwrite
)db.tranaction('테이블명', 'readwrite').objectStore('테이블명')
형태를 사용한다.
/**
* IndexedDB의 storeName을 찾아 데이터를 추가합니다.
* @param {string} storeName
* @param {T} requestObject
* @returns void
*/
export const addStoreData = <T>(
storeName: string,
requestObject: T
): Promise<void> => {
if (!isValidStoreName(storeName)) {
return Promise.reject("Invalid store name");
}
return new Promise((resolve, reject) => {
const _store = db
.transaction(storeName, "readwrite")
.objectStore(storeName);
const _addRequest = _store.add(requestObject);
_addRequest.addEventListener("success", () => {
console.log("[IndexedDB]: addStoreData()...");
resolve();
});
_addRequest.addEventListener("error", (e) => {
console.error("[IndexedDB]: addStoreData()...", e);
reject(e);
});
});
};
readonly
)db.tranaction('테이블명', 'readonly').objectStore('테이블명')
를 기본으로 사용한다.get(param)
을 이용해서 가져온다.getAll()
을 이용해서 가져온다.IDBCursor
객체를 이용한다./**
* IndexedDB에 저장된 Store의 데이터 1건에 대해 조회합니다.
* @param {string} storeName 탐색한 스토어 이름
* @param {numbeR} targetId 타겟 식별자
* @returns {T} 결과 데이터
*/
export const getStoreDataById = <T>(
storeName: string,
targetId: number
): Promise<T> => {
if (!isValidStoreName(storeName)) {
return Promise.reject("Invalid store name");
}
return new Promise((resolve, reject) => {
const _store = db.transaction(storeName, "readonly").objectStore(storeName);
const _getRequest = _store.get(targetId);
_getRequest.addEventListener("success", (e: Event) => {
console.log("[IndexedDB]: getStoreDataById()...");
const data = e.target as IDBRequest;
const result = data!.result;
resolve(result);
});
_getRequest.addEventListener("error", (e) => {
console.error("[IndexedDB]: getStoreDataById()...", e);
reject(e);
});
});
};
/******************************************************************************/
/**
* IndexedDB에 저장된 Store의 데이터 여러건에 대해 조회합니다.
* @param {string} storeName 탐색한 스토어 이름
* @returns {T} 결과 데이터
*/
export const getAllStoreData = <T>(storeName: string): Promise<T> => {
if (!isValidStoreName(storeName)) {
return Promise.reject("Invalid store name");
}
return new Promise((resolve, reject) => {
const _store = db.transaction(storeName, "readonly").objectStore(storeName);
const _getRequest = _store.getAll();
_getRequest.addEventListener("success", (e: Event) => {
console.log("[IndexedDB]: getAllStoreData()...");
const data = e.target as IDBRequest;
const result = data!.result;
resolve(result);
});
_getRequest.addEventListener("error", (e) => {
console.error("[IndexedDB]: getAllStoreData()...", e);
reject(e);
});
});
};
또한, 이전에 만들어둔 index
를 이용해서 데이터를 조회할 수 있다. (IDBCursor
)
/**
* Object Store에 특정 속성으로 데이터를 조회합니다.
* @param {string} storeName 탐색한 스토어 이름
* @param {string} propertyName 탐색할 속성 이름
* @param {string} target 탐색할 속성 값
* @returns
*/
export const getDataByProperty = <T>(
storeName: string,
propertyName: string,
target: string
): Promise<T[]> => {
if (!isValidStoreName(storeName)) {
return Promise.reject("Invalid store name");
}
return new Promise((resolve, reject) => {
const _store = db.transaction(storeName, "readonly").objectStore(storeName);
if (!_store.indexNames.contains(propertyName)) {
return Promise.reject(`Invalid index name... ${propertyName}`);
}
const _index = _store.index(propertyName);
const _responseData: T[] = [];
const _requestCursor = _index.openCursor();
_requestCursor.addEventListener("success", (e: Event) => {
const data = e.target as IDBRequest;
const _cursor = data.result;
if (_cursor) {
if (_cursor.value[`${propertyName}`] === target) {
_responseData.push(_cursor.value);
}
_cursor.continue();
} else {
resolve(_responseData);
}
console.log(`[IndexedDB]: getStoreDataBy${propertyName}()...`);
});
_requestCursor.addEventListener("error", (e) => {
console.log(`[IndexedDB]: getStoreDataBy${propertyName}()...`);
reject(e);
});
});
};