TypeScript로 IndexedDB 시작하기 (feat. LocalStorage와 비교)

Adam Kim·약 24시간 전
0

typescript

목록 보기
18/18

웹 애플리케이션에서 데이터를 클라이언트에 저장하는 방법은 여러 가지가 있지만, 가장 흔히 사용되는 것은 LocalStorage와 IndexedDB입니다. LocalStorage는 사용하기 간편하지만, 대용량의 구조화된 데이터를 다루기에는 한계가 있습니다. 이때 IndexedDB는 훌륭한 대안이 될 수 있습니다.

이 글에서는 TypeScript와 async/await를 사용하여 IndexedDB의 기본 CRUD 작업을 래핑하는 간단한 서비스 클래스를 만들고, LocalStorage와 비교하여 언제 IndexedDB를 선택해야 하는지 알아보겠습니다.

LocalStorage와 IndexedDB: 언제 무엇을 사용해야 할까요?

두 기술은 명확한 장단점을 가지고 있어, 상황에 맞게 선택하는 것이 중요합니다.

LocalStorage

  • 장점:
    • 간단한 API: setItem(), getItem() 등 매우 직관적인 API를 가지고 있어 배우고 사용하기 쉽습니다.
    • 동기 방식: 코드가 동기적으로 작동하여 로직이 단순하고 이해하기 쉽습니다.
  • 단점:
    • 작은 용량: 브라우저마다 다르지만 약 5MB의 저장 공간 제한이 있습니다.
    • 문자열만 저장 가능: 객체나 배열을 저장하려면 JSON.stringify()와 JSON.parse()를 반드시 사용해야 합니다.
    • 메인 스레드 차단 (Blocking): 동기 방식으로 작동하기 때문에, 데이터 I/O 작업이 메인 스레드를 차단하여 UI 렌더링에 영향을 줄 수 있습니다.
    • 쿼리 기능 부재: 특정 조건에 맞는 데이터를 효율적으로 검색할 수 없습니다.

IndexedDB

  • 장점:
    • 대용량 저장 공간: 수백 MB에서 GB 단위의 데이터를 저장할 수 있습니다. (브라우저 정책 및 사용자 동의에 따라 다름)
    • 다양한 데이터 타입: 문자열뿐만 아니라 객체, 파일(Blob), 배열 등 복잡한 JavaScript 객체를 그대로 저장할 수 있습니다.
    • 비동기 방식: 모든 작업이 비동기적으로 처리되어 메인 스레드를 차단하지 않으므로 UI 성능에 영향을 주지 않습니다.
    • 강력한 쿼리 및 인덱싱: 인덱스를 생성하여 특정 필드를 기준으로 데이터를 매우 빠르게 검색할 수 있습니다.
    • 트랜잭션 지원: 데이터의 추가, 수정, 삭제 작업을 하나의 트랜잭션으로 묶어 데이터 무결성을 보장합니다.
  • 단점:
    • 복잡한 API: 이벤트 기반의 비동기 API는 LocalStorage에 비해 훨씬 복잡하고 장황합니다. (이 글에서 이 문제를 해결해 봅니다.)

결론: IndexedDB가 더 유용한 경우

  • 대용량 데이터를 다룰 때 (예: 오프라인용 기사, 사용자 생성 콘텐츠)
  • 구조화된 객체를 자주 저장하고 검색해야 할 때
  • 오프라인 우선(Offline-First) 애플리케이션이나 프로그레시브 웹 앱(PWA)을 만들 때
  • 저장된 데이터에 대해 효율적인 검색이나 정렬이 필요할 때
  • 이미지나 파일 같은 바이너리 데이터를 저장해야 할 때

이제 IndexedDB의 복잡한 API를 TypeScript로 간단하게 만들어 보겠습니다.

DEMO 작성

1단계: 데이터 모델 정의

데이터베이스에 저장할 User 인터페이스를 정의합니다.

// src/user.model.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

2단계: IndexedDB 서비스 클래스 생성

IndexedDB의 복잡한 API를 캡슐화하는 DBService 클래스를 작성합니다.

// src/db.service.ts
import { User } from './user.model';

const DB_NAME = 'MyUserDB';
const STORE_NAME = 'users';
const DB_VERSION = 1;

export class DBService {
  private db: IDBDatabase | null = null;

  // 1. 데이터베이스 열기 및 초기화
  public async openDB(): Promise<void> {
    return new Promise((resolve, reject) => {
      // ... (이전 예제와 동일한 코드)
      if (this.db) return resolve();
      const request = indexedDB.open(DB_NAME, DB_VERSION);
      request.onerror = () => reject('Error opening database');
      request.onsuccess = () => { this.db = request.result; resolve(); };
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains(STORE_NAME)) {
          db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
        }
      };
    });
  }

  // 2. 트랜잭션 헬퍼
  private getStore(mode: IDBTransactionMode): IDBObjectStore {
    // ... (이전 예제와 동일한 코드)
    if (!this.db) throw new Error('Database not initialized!');
    return this.db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
  }

  // 3. CRUD 메서드 구현 (Promise 래핑)
  public async addUser(user: Omit<User, 'id'>): Promise<User> {
    const store = this.getStore('readwrite');
    const request = store.add(user);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve({ ...user, id: request.result as number });
      request.onerror = () => reject(request.error);
    });
  }

  public async getUser(id: number): Promise<User | undefined> {
    // ... (이전 예제와 동일한 코드)
    const request = this.getStore('readonly').get(id);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  public async updateUser(user: User): Promise<User> {
    // ... (이전 예제와 동일한 코드)
    const request = this.getStore('readwrite').put(user);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(user);
      request.onerror = () => reject(request.error);
    });
  }

  public async deleteUser(id: number): Promise<void> {
    // ... (이전 예제와 동일한 코드)
    const request = this.getStore('readwrite').delete(id);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

3단계: 서비스 사용 예제

작성한 DBService를 사용하는 예제입니다.

// src/main.ts
import { DBService } from './db.service';

(async () => {
  const dbService = new DBService();
  try {
    await dbService.openDB();
    // 1. 사용자 추가
    const newUser = await dbService.addUser({ name: 'Alice', email: 'alice@example.com' });
    console.log('User added:', newUser);
    // 2. 사용자 조회, 수정, 삭제 ...
  } catch (error) {
    console.error('An error occurred:', error);
  }
})();

결론

LocalStorage는 간단한 설정 값이나 토큰처럼 작은 데이터를 저장하는 데 적합합니다. 반면, 복잡하고 큰 데이터를 클라이언트에 저장하고 효율적으로 관리해야 한다면 IndexedDB가 정답입니다. TypeScript와 async/await를 통해 IndexedDB의 복잡한 API를 캡슐화하면, 그 강력한 기능을 훨씬 더 직관적으로 활용할 수 있습니다.

profile
Angular2+ Developer

0개의 댓글