IndexedDB

송우든·2023년 12월 25일
0

Dev

목록 보기
18/20
post-thumbnail

도입 계기

현재에 꾸준히 기술적인 부분을 디벨롭하고 있다. 학습적인 개선이기 때문에 따로 백엔드가 없었고, 실제 많은 데이터를 처리하기 위해 프론트단에서 어떻게 처리할지를 고민하게 됐다. 간단한 구조의 데이터라면 웹 스토리지에 저장하여 관리했겠지만, 객체 배열과 같은 복잡한 데이터를 웹 스토리지에서 관리하는 것이 이상하다고 생각이 들었다. 그래서 이번 기회에 IndexedDB를 학습하고 정리해보려고 한다.

웹 브라우저에 저장 방식

웹 브라우저에 데이터를 저장하는 방식은 크게 3가지로 나눌 수 있다. 대표적으로 Cookie, Storage 그리고 indexedDB가 존재한다. 아래와 같은 차이를 가지고 있다. IndexedDB는 다른 저장 방식과 달리 비동기로 동작한다는 점과 데이터 타입에 제한이 없어 객체도 저장할 수 있다는 특징이 있다.

생활코딩 - 웹브라우저에서 데이터를 저장하기 - IndexedDB

IndexedDB

파일이나 블롭과 같이 많은 양의 구조화된 데이터를 클라이언트에 저장하기 위한 로우 레벨 API이다. 여러개의 데이터를 그룹화하는 Object Store가 존재하고, Obejct Store 여러개를 Database로 만든다.

IndexedDB 실습

먼저, 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('테이블명')를 기본으로 사용한다.
  • 조회할 데이터가 1건인 경우, 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);
    });
  });
};
profile
개발 기록💻

0개의 댓글