SQLite와 WatermelonDB — 모바일 데이터베이스 활용
AsyncStorage로 부족한 관계형 데이터나 대량 데이터 처리에는 SQLite가 적합하고, 반응형 쿼리가 필요하면 WatermelonDB가 좋습니다.
To-do 앱 수준을 넘어 오프라인 우선 앱, 대량 데이터 검색, 관계형 데이터를 다루려면 로컬 데이터베이스가 필요합니다.
expo-sqlite (Expo 프로젝트)
npx expo install expo-sqlite
import * as SQLite from 'expo-sqlite';
// 데이터베이스 열기
const db = await SQLite.openDatabaseAsync('myapp.db');
// 테이블 생성
await db.execAsync(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
`);
// 삽입
await db.runAsync(
'INSERT INTO todos (title) VALUES (?)',
'장보기'
);
// 조회
const todos = await db.getAllAsync<{
id: number;
title: string;
completed: number;
}>('SELECT * FROM todos WHERE completed = ?', 0);
// 업데이트
await db.runAsync(
'UPDATE todos SET completed = 1 WHERE id = ?',
todoId
);
// 삭제
await db.runAsync('DELETE FROM todos WHERE id = ?', todoId);
Repository 패턴
// database/todoRepository.ts
class TodoRepository {
private db: SQLite.SQLiteDatabase;
constructor(db: SQLite.SQLiteDatabase) {
this.db = db;
}
async getAll() {
return this.db.getAllAsync<Todo>('SELECT * FROM todos ORDER BY created_at DESC');
}
async getById(id: number) {
return this.db.getFirstAsync<Todo>('SELECT * FROM todos WHERE id = ?', id);
}
async create(title: string) {
const result = await this.db.runAsync(
'INSERT INTO todos (title) VALUES (?)',
title
);
return result.lastInsertRowId;
}
async toggleComplete(id: number) {
await this.db.runAsync(
'UPDATE todos SET completed = NOT completed WHERE id = ?',
id
);
}
async delete(id: number) {
await this.db.runAsync('DELETE FROM todos WHERE id = ?', id);
}
async search(query: string) {
return this.db.getAllAsync<Todo>(
'SELECT * FROM todos WHERE title LIKE ?',
`%${query}%`
);
}
}
WatermelonDB — 반응형 데이터베이스
WatermelonDB는 SQLite 위에 구축된 고성능 반응형 데이터베이스입니다. 데이터가 변경되면 관련 컴포넌트가 자동으로 리렌더링됩니다.
npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators
스키마 정의
// database/schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export const schema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'posts',
columns: [
{ name: 'title', type: 'string' },
{ name: 'body', type: 'string' },
{ name: 'is_published', type: 'boolean' },
{ name: 'created_at', type: 'number' },
],
}),
tableSchema({
name: 'comments',
columns: [
{ name: 'body', type: 'string' },
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'created_at', type: 'number' },
],
}),
],
});
모델 정의
// database/models/Post.ts
import { Model } from '@nozbe/watermelondb';
import { field, text, date, children } from '@nozbe/watermelondb/decorators';
export class Post extends Model {
static table = 'posts';
static associations = {
comments: { type: 'has_many' as const, foreignKey: 'post_id' },
};
@text('title') title!: string;
@text('body') body!: string;
@field('is_published') isPublished!: boolean;
@date('created_at') createdAt!: Date;
@children('comments') comments!: any;
}
반응형 쿼리와 컴포넌트 연결
import { withObservables } from '@nozbe/watermelondb/react';
// 데이터 변경 시 자동 리렌더링
const enhance = withObservables([], ({ database }) => ({
posts: database.get('posts').query().observe(),
}));
function PostList({ posts }: { posts: Post[] }) {
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ padding: 16 }}>
<Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
<Text numberOfLines={2}>{item.body}</Text>
</View>
)}
/>
);
}
export default enhance(PostList);
저장소 선택 가이드
| 요구사항 | 추천 솔루션 |
|---|---|
| 키-값 설정 저장 | AsyncStorage / MMKV |
| 간단한 관계형 데이터 | expo-sqlite |
| 대량 데이터 + 검색 | SQLite |
| 반응형 쿼리 + 동기화 | WatermelonDB |
| 오프라인 우선 앱 | WatermelonDB |
정리
- SQLite: 관계형 데이터와 복잡한 쿼리가 필요할 때 적합합니다
- WatermelonDB: 대량 데이터 + 반응형 UI + 서버 동기화가 필요할 때 강력합니다
- 데이터베이스 접근은 Repository 패턴 으로 추상화하면 유지보수가 편합니다
- 대부분의 앱은 AsyncStorage/MMKV + React Query(서버 동기화)로 충분합니다
댓글 로딩 중...