웹 애플리케이션에서 데이터를 클라이언트에 저장하는 방법은 여러 가지가 있지만, 가장 흔히 사용되는 것은 LocalStorage와 IndexedDB입니다. LocalStorage는 사용하기 간편하지만, 대용량의 구조화된 데이터를 다루기에는 한계가 있습니다. 이때 IndexedDB는 훌륭한 대안이 될 수 있습니다.
이 글에서는 TypeScript와 async/await를 사용하여 IndexedDB의 기본 CRUD 작업을 래핑하는 간단한 서비스 클래스를 만들고, LocalStorage와 비교하여 언제 IndexedDB를 선택해야 하는지 알아보겠습니다.
두 기술은 명확한 장단점을 가지고 있어, 상황에 맞게 선택하는 것이 중요합니다.
이제 IndexedDB의 복잡한 API를 TypeScript로 간단하게 만들어 보겠습니다.
데이터베이스에 저장할 User 인터페이스를 정의합니다.
// src/user.model.ts
export interface User {
id: number;
name: string;
email: string;
}
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);
});
}
}
작성한 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를 캡슐화하면, 그 강력한 기능을 훨씬 더 직관적으로 활용할 수 있습니다.