요약
WatermelonDB는 React Native 앱에서 대량의 로컬 데이터를 빠르게 다루기 위한 오프라인 우선(offline-first) 반응형 데이터베이스 프레임워크다. 내부 저장소는 주로 SQLite를 사용하지만, 개발자가 SQL을 직접 계속 작성하는 방식이 아니라 Schema, Model, Query, Writer, Sync API를 통해 앱 데이터 흐름을 구조화한다. 단순 캐시나 몇 개의 설정값 저장에는 과한 선택일 수 있지만, 수천~수만 건의 레코드, 동기화, 관계형 데이터, 화면 자동 갱신이 필요한 앱이라면 SQLite를 직접 쓰는 것보다 생산성이 높아질 수 있다.
SQLite 개념을 먼저 정리하고 싶다면 서버리스 파일 기반 DB의 장점, 한계, WAL, 락 오류 해결은 SQLite 실무 가이드 ↗에서 따로 자세히 정리했습니다.
확인 기준: 작성 시점에 npm 최신 버전은
@nozbe/watermelondb0.28.0으로 확인되지만, 공식 문서 페이지는 0.27.1 기준으로 표시되어 있다. 설치 전에는 프로젝트의 React Native/Expo 버전과 WatermelonDB 릴리스/이슈를 같이 확인하는 편이 안전하다.
목차
- 배경
- WatermelonDB란?
- WatermelonDB가 빠른 이유
- React Native 기본 사용법
- SQLite와 WatermelonDB 비교
- 동기화 구조
- 실제로 막히는 부분과 해결 방향
- 언제 쓰고, 언제 피해야 할까?
- 결론
- 참고 자료
배경
React Native 앱을 만들다 보면 처음에는 서버 API만 호출해서 화면을 그리면 충분해 보인다. 그런데 다음 요구사항이 들어오면 이야기가 달라진다.
- 네트워크가 끊겨도 작성/수정이 가능해야 한다.
- 앱 실행 직후 수천 개의 할 일, 메시지, 주문, 고객 데이터를 빠르게 보여줘야 한다.
- 로컬 데이터가 바뀌면 관련 화면이 자동으로 갱신되어야 한다.
- 서버와 로컬 데이터를 주기적으로 동기화해야 한다.
- 단순 key-value 저장소가 아니라 관계형 데이터가 필요하다.
이때 흔히 떠올리는 선택지는 SQLite다. SQLite는 모바일 로컬 DB의 사실상 표준에 가깝고, 성능과 안정성이 좋다. 다만 React Native에서 SQLite를 직접 쓰면 SQL 작성, 결과 매핑, 화면 갱신, 동기화 충돌 처리, 마이그레이션 등을 애플리케이션 코드에서 직접 설계해야 한다.
WatermelonDB는 이 지점을 해결하려는 도구다. SQLite를 대체한다기보다, SQLite 위에 React/React Native 앱에 맞는 데이터 계층을 얹은 프레임워크에 가깝다.
WatermelonDB란?
WatermelonDB는 Nozbe가 만든 오픈소스 로컬 데이터베이스 프레임워크다. 공식 소개의 핵심은 다음과 같다.
- React Native와 React 앱에서 대량의 로컬 데이터를 빠르게 다루는 것을 목표로 한다.
- 데이터를 한 번에 JavaScript 메모리로 모두 올리지 않고, 필요한 시점에 lazy하게 읽는다.
- 쿼리는 SQLite 같은 네이티브 DB에서 수행된다.
- RxJS 기반의 observable 구조를 제공해 데이터 변경 시 UI를 자동 갱신할 수 있다.
- 자체 백엔드를 연결해 오프라인 우선 동기화를 구현할 수 있다.
즉 WatermelonDB는 단순한 SQLite wrapper가 아니다. 다음 레이어를 함께 제공한다.
| 구성 요소 | 역할 |
|---|---|
| Schema | 테이블과 컬럼 정의 |
| Model | 앱에서 다루는 데이터 객체 정의 |
| Collection | 특정 테이블의 레코드 접근 단위 |
| Query API | 조건 기반 조회 |
| Writer | 생성/수정/삭제 작업을 안전하게 묶는 쓰기 트랜잭션 개념 |
| Observable | 데이터 변경 시 UI 자동 반영 |
| Sync API | 서버와 로컬 DB 간 변경분 동기화 |
WatermelonDB가 빠른 이유
WatermelonDB의 성능 포인트는 “SQLite라서 빠르다”에서 끝나지 않는다. 더 중요한 부분은 불필요하게 JavaScript 영역으로 데이터를 끌어올리지 않는 구조다.
일반적인 상태 관리 + persistence 방식은 앱 시작 시 많은 데이터를 JS 메모리로 로드하기 쉽다. 데이터가 수백 건일 때는 문제가 없지만, 수천~수만 건으로 늘어나면 앱 시작 속도와 메모리 사용량이 눈에 띄게 나빠질 수 있다.
WatermelonDB는 다음 방식으로 이 문제를 줄인다.
1. 앱 시작 시 전체 DB를 JS 메모리에 올리지 않는다. 2. 필요한 화면에서 필요한 Query만 실행한다. 3. 조건 검색은 JS 배열 필터링이 아니라 DB 쿼리로 처리한다. 4. 변경된 데이터와 연결된 컴포넌트만 observable로 갱신한다.
React Native 앱에서 “로컬 데이터가 많아질수록 앱이 무거워지는 문제”를 겪고 있다면 WatermelonDB를 검토할 만하다.
React Native 기본 사용법
아래 예제는 블로그 글과 댓글을 저장하는 간단한 구조다. 실제 프로젝트에서는 폴더 구조와 import alias에 맞게 조정하면 된다.
1. 설치
npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators
또는 Yarn을 사용한다면 다음처럼 설치한다.
yarn add @nozbe/watermelondb
yarn add --dev @babel/plugin-proposal-decorators
WatermelonDB는 decorator 문법을 사용하므로 Babel 설정이 필요하다.
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}
iOS에서는 CocoaPods 설정이 필요할 수 있다. 공식 문서는 simdjson pod 추가와 pod install을 안내한다.
# ios/Podfile
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true
cd ios
pod install
여기서 자주 막히는 편이다. 특히 use_frameworks!, Expo, React Native New Architecture 조합은 프로젝트 상태에 따라 추가 확인이 필요하다.
2. Schema 정의
WatermelonDB에서는 먼저 DB Schema를 정의한다. 테이블 이름과 컬럼 이름은 보통 snake_case를 사용한다.
// src/db/schema.js
import { appSchema, tableSchema } from '@nozbe/watermelondb'
export const mySchema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'posts',
columns: [
{ name: 'title', type: 'string' },
{ name: 'body', type: 'string' },
{ name: 'is_pinned', type: 'boolean' },
],
}),
tableSchema({
name: 'comments',
columns: [
{ name: 'body', type: 'string' },
{ name: 'post_id', type: 'string', isIndexed: true },
],
}),
],
})
post_id처럼 관계 조회에 자주 쓰는 컬럼은 isIndexed: true를 붙이는 것이 좋다. SQLite에서도 인덱스가 중요하듯 WatermelonDB에서도 조회 패턴에 맞는 인덱스 설계가 성능에 영향을 준다.
3. Model 정의
Schema가 실제 DB 구조라면 Model은 앱 코드에서 사용할 객체다.
// src/db/models/Post.js
import { Model } from '@nozbe/watermelondb'
import { field, children, writer } from '@nozbe/watermelondb/decorators'
export default class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
@field('title') title
@field('body') body
@field('is_pinned') isPinned
@children('comments') comments
@writer async updateTitle(title) {
await this.update(post => {
post.title = title
})
}
}
// src/db/models/Comment.js
import { Model } from '@nozbe/watermelondb'
import { field, relation } from '@nozbe/watermelondb/decorators'
export default class Comment extends Model {
static table = 'comments'
static associations = {
posts: { type: 'belongs_to', key: 'post_id' },
}
@field('body') body
@relation('posts', 'post_id') post
}
여기서 중요한 점은 DB 변경 작업을 아무 곳에서나 하지 않는다는 것이다. WatermelonDB에서는 생성, 수정, 삭제를 database.write() 또는 @writer 안에서 처리한다.
4. Database 생성
// src/db/index.js
import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'
import { mySchema } from './schema'
import Post from './models/Post'
import Comment from './models/Comment'
const adapter = new SQLiteAdapter({
schema: mySchema,
})
export const database = new Database({
adapter,
modelClasses: [Post, Comment],
})
5. 데이터 생성과 조회
// 글 생성
const newPost = await database.write(async () => {
return await database.get('posts').create(post => {
post.title = 'WatermelonDB 시작하기'
post.body = 'React Native에서 로컬 DB를 다루는 방법'
post.isPinned = false
})
})
// 글 목록 조회
import { Q } from '@nozbe/watermelondb'
const pinnedPosts = await database
.get('posts')
.query(Q.where('is_pinned', true))
.fetch()
6. React 컴포넌트와 연결
WatermelonDB의 장점은 여기서 드러난다. 데이터를 한 번 가져와서 끝나는 것이 아니라, observable로 연결하면 데이터 변경 시 화면이 자동으로 갱신된다.
import React from 'react'
import { Text, View } from 'react-native'
import { withObservables } from '@nozbe/watermelondb/react'
function PostItem({ post }) {
return (
<View>
<Text>{post.title}</Text>
<Text>{post.body}</Text>
</View>
)
}
const enhance = withObservables(['post'], ({ post }) => ({
post,
}))
export default enhance(PostItem)
리스트도 Query를 observable로 연결할 수 있다. 이 구조 덕분에 “DB 변경 → 상태 업데이트 → 리스트 재조회 → 화면 갱신” 흐름을 직접 매번 만들 필요가 줄어든다.
SQLite와 WatermelonDB 비교
WatermelonDB는 SQLite를 완전히 대체하는 별도 DB라기보다, SQLite를 React Native 앱에서 더 높은 추상화로 쓰게 해주는 도구에 가깝다. 그래서 비교 기준은 “SQLite가 더 좋다/WatermelonDB가 더 좋다”가 아니라 어느 레벨의 제어와 생산성이 필요한가다.
| 항목 | SQLite 직접 사용 | WatermelonDB |
|---|---|---|
| 기본 성격 | 로컬 관계형 DB 엔진/API | SQLite 기반 반응형 DB 프레임워크 |
| 데이터 접근 | SQL 직접 작성 | Model, Collection, Query API 사용 |
| 성능 | 쿼리 설계를 잘하면 매우 빠름 | lazy loading + native DB query + observable 구조 |
| React UI 갱신 | 직접 상태 관리 필요 | observable로 자동 갱신 가능 |
| 관계형 데이터 | SQL로 직접 조인/쿼리 | association과 relation 모델 제공 |
| 마이그레이션 | 직접 설계 | WatermelonDB migration API 사용 |
| 동기화 | 직접 구현 | Sync primitive와 프로토콜 제공, 단 백엔드는 직접 필요 |
| 러닝 커브 | SQL을 알면 접근 쉬움 | WatermelonDB 규칙과 decorator, writer 패턴 학습 필요 |
| 디버깅 | DB 파일/SQL 기준으로 추적 | WatermelonDB 추상화와 네이티브 빌드 이슈까지 같이 봐야 함 |
| 적합한 앱 | 단순 로컬 저장, SQL 제어가 중요한 앱 | 오프라인 우선, 대량 데이터, 자동 UI 갱신, 동기화 중심 앱 |
SQLite를 직접 쓰는 편이 나은 경우
다음과 같은 앱이라면 WatermelonDB까지 도입하지 않아도 된다.
- 저장할 데이터가 많지 않다.
- 단순 설정값, 캐시, 최근 검색어 정도만 저장한다.
- SQL을 직접 작성하고 튜닝하는 편이 더 중요하다.
- React UI와 자동으로 연결되는 observable 구조가 필요 없다.
- 팀이 WatermelonDB의 writer/model/sync 규칙을 학습할 여유가 없다.
React Native에서는 expo-sqlite, react-native-sqlite-storage, op-sqlite, react-native-quick-sqlite 같은 선택지도 있다. 특히 Expo 기반 프로젝트라면 expo-sqlite가 설치와 호환성 면에서 더 단순할 수 있다.
WatermelonDB가 더 어울리는 경우
반대로 아래 조건이 있으면 WatermelonDB가 강점이 있다.
- 로컬에 수천~수만 건의 데이터를 저장한다.
- 앱 실행 속도가 중요하다.
- 오프라인 상태에서도 데이터를 생성/수정해야 한다.
- 서버와 변경분 동기화가 필요하다.
- 화면 여러 곳에서 같은 데이터를 바라보고 자동 갱신되어야 한다.
- SQL보다 앱 도메인 모델 중심으로 코드를 구성하고 싶다.
한 줄로 정리하면 이렇다.
SQLite는 “로컬 DB 엔진”이고, WatermelonDB는 “React Native 앱을 위한 SQLite 기반 데이터 레이어”다.
동기화 구조
WatermelonDB는 로컬 DB만 제공하는 도구가 아니다. 오프라인 우선 앱을 염두에 두고 동기화 기능도 제공한다. 다만 여기서 오해하면 안 되는 부분이 있다.
WatermelonDB가 서버를 대신 만들어 주지는 않는다.
공식 문서 기준으로 WatermelonDB는 다음을 제공한다.
- 로컬에서 생성/수정/삭제된 변경분 추적
synchronize()APIpullChanges,pushChanges형태의 동기화 흐름- 백엔드가 맞춰야 할 changes object 구조
프론트엔드에서는 대략 이런 형태가 된다.
import { synchronize } from '@nozbe/watermelondb/sync'
export async function syncDatabase(database) {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const response = await fetch(
`https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}&schema_version=${schemaVersion}`
)
if (!response.ok) {
throw new Error(await response.text())
}
const { changes, timestamp } = await response.json()
return { changes, timestamp }
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(
`https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
}
)
if (!response.ok) {
throw new Error(await response.text())
}
},
migrationsEnabledAtVersion: 1,
})
}
백엔드는 다음 형태의 변경분을 주고받아야 한다.
{
"changes": {
"posts": {
"created": [
{ "id": "post_1", "title": "Hello", "body": "...", "is_pinned": false }
],
"updated": [],
"deleted": []
},
"comments": {
"created": [],
"updated": [],
"deleted": ["comment_1"]
}
},
"timestamp": 1710000000000
}
여기서 핵심은 서버 timestamp와 변경분 조회의 일관성이다. 공식 문서도 pull endpoint가 lastPulledAt 이후의 변경을 빠짐없이 반환해야 하며, 서버 시간이 일관된 기준으로 잡혀야 한다고 설명한다. 이 부분을 대충 만들면 “어떤 레코드는 영원히 동기화되지 않는” 문제가 생길 수 있다.
실제로 막히는 부분과 해결 방향
WatermelonDB는 강력하지만 React Native 네이티브 모듈, Babel, CocoaPods, Expo, 동기화 백엔드가 얽힌다. 실제 개발 중에는 아래 지점에서 자주 막힌다.
사례 1. decorator 문법을 인식하지 못한다
증상은 보통 이런 식이다.
SyntaxError: Support for the experimental syntax 'decorators' isn't currently enabled
먼저 Babel 설정을 확인한다.
npm ls @babel/plugin-proposal-decorators
.babelrc 또는 babel.config.js에 legacy decorator 설정이 들어갔는지 확인한다.
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
],
}
여기서 자주 하는 실수는 TypeScript 설정만 보고 Babel 설정을 놓치는 것이다. WatermelonDB 예제는 decorator를 사용하므로 Metro/Babel 파이프라인에서 해당 문법을 처리해야 한다.
사례 2. iOS에서 simdjson.h 또는 Pod 관련 빌드 오류가 난다
iOS 빌드에서 simdjson 관련 오류가 나오면 Podfile과 pod install 상태를 먼저 본다.
cd ios
pod install
Podfile에 공식 문서가 안내하는 simdjson 설정이 들어갔는지도 확인한다.
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true
use_frameworks!를 사용하는 프로젝트는 더 주의해야 한다. WatermelonDB 공식 설치 문서도 frameworks 모드를 권장하지 않는다는 취지의 안내를 하고 있으며, GitHub 이슈에서도 관련 빌드 문제가 계속 언급된다. 이미 프로젝트가 use_frameworks!에 의존한다면, WatermelonDB 도입 전에 작은 샘플 브랜치에서 iOS 빌드부터 검증하는 편이 안전하다.
사례 3. Expo Go에서 네이티브 모듈을 찾지 못한다
Expo Go는 임의의 네이티브 모듈을 마음대로 포함하지 않는다. WatermelonDB처럼 네이티브 코드가 필요한 라이브러리는 Expo Go에서 바로 동작하지 않을 수 있다.
증상 예시는 다음과 비슷하다.
NativeModules.WMDatabaseBridge is not defined
이 경우 선택지는 보통 세 가지다.
1. Expo 개발 빌드(development build)를 사용한다. 2. config plugin 또는 커스텀 네이티브 설정을 검토한다. 3. 요구사항이 단순하면 expo-sqlite로 범위를 낮춘다.
Expo 프로젝트라면 “WatermelonDB가 좋은가?”보다 먼저 “현재 Expo 워크플로에서 네이티브 설정을 관리할 수 있는가?”를 확인해야 한다.
사례 4. React Native New Architecture 호환성이 불확실하다
React Native 0.7x 이후 New Architecture, Bridgeless, 최신 Expo SDK 조합에서는 WatermelonDB 호환성 이슈를 반드시 확인해야 한다. GitHub 이슈에는 New Architecture, RN 0.76+, Expo SDK 54+ 관련 질문과 문제가 올라와 있다.
운영 앱이라면 다음 순서로 확인하는 것이 좋다.
npm view @nozbe/watermelondb version
npm view react-native version
그리고 프로젝트의 RN/Expo 버전과 WatermelonDB GitHub 이슈를 같이 확인한다. 새 아키텍처를 켜기 전에 로컬 DB가 정상 초기화되는지, iOS/Android 릴리스 빌드까지 되는지 먼저 검증해야 한다.
사례 5. 동기화가 중복되거나 누락된다
WatermelonDB 동기화는 프론트엔드 코드만으로 끝나지 않는다. 백엔드가 lastPulledAt 이후 변경분을 정확히 반환해야 한다.
문제가 생기면 다음을 확인한다.
- 서버 timestamp를 언제 찍는가?
- pull 도중 변경된 데이터가 누락될 가능성은 없는가?
- 삭제된 레코드는 ID 목록으로 내려오는가?
- 클라이언트가 push한 변경을 다음 pull에서 다시 받는 구조를 이해하고 있는가?
- 충돌 발생 시 서버가 어떤 정책으로 실패/허용하는가?
동기화가 핵심인 앱에서는 WatermelonDB 도입보다 백엔드 sync protocol 설계가 더 중요할 수 있다.
언제 쓰고, 언제 피해야 할까?
WatermelonDB를 추천하는 경우
- 오프라인 우선 앱을 만들고 있다.
- 로컬 데이터가 많고 앱 실행 속도가 중요하다.
- 관계형 데이터 모델이 필요하다.
- 데이터 변경이 여러 화면에 자동 반영되어야 한다.
- 자체 백엔드를 통해 sync protocol을 구현할 수 있다.
- 팀이 React Native 네이티브 빌드 문제를 처리할 수 있다.
WatermelonDB를 피하거나 보류할 경우
- 단순 설정 저장만 필요하다.
- 서버 API 응답을 잠깐 캐시하는 정도다.
- Expo Go만으로 개발해야 하고 네이티브 빌드 구성이 어렵다.
- 최신 React Native New Architecture를 적극 사용 중인데 호환성 검증 시간이 없다.
- SQL을 직접 제어해야 하는 복잡한 분석 쿼리가 많다.
- 동기화 백엔드를 직접 설계할 여력이 없다.
결론
WatermelonDB의 핵심은 “React Native에서 SQLite를 더 편하게 쓰는 라이브러리” 정도로만 보면 조금 부족하다. 더 정확히는 대량 로컬 데이터, 반응형 UI, 오프라인 우선 동기화를 함께 고려한 데이터 프레임워크다.
SQLite와 비교하면 WatermelonDB는 직접 제어권을 일부 내려놓는 대신, Model 중심 개발, observable UI 업데이트, sync primitive 같은 앱 레벨 기능을 얻는다. 그래서 단순 저장소가 필요한 앱에는 과하고, 오프라인에서도 데이터가 계속 쌓이고 바뀌는 앱에는 꽤 잘 맞는다.
React Native 프로젝트에서 WatermelonDB를 검토한다면 먼저 이렇게 판단하면 된다.
1. 데이터가 정말 많아질 예정인가? 2. 오프라인 생성/수정이 필요한가? 3. 서버와 동기화해야 하는가? 4. Expo/RN/iOS/Android 빌드 호환성을 검증할 시간이 있는가? 5. SQL 직접 제어보다 앱 도메인 모델과 반응형 UI가 더 중요한가?
이 질문에 대부분 “예”라면 WatermelonDB를 시도해볼 만하다. 반대로 두세 개만 해당한다면, 우선 expo-sqlite나 op-sqlite 같은 SQLite 라이브러리로 단순하게 시작하고, 데이터 구조가 커질 때 WatermelonDB로 넘어가는 전략도 현실적이다.
참고 자료
- WatermelonDB 공식 문서: https://watermelondb.dev/docs
- WatermelonDB Installation: https://watermelondb.dev/docs/Installation
- WatermelonDB Schema: https://watermelondb.dev/docs/Schema
- WatermelonDB Model: https://watermelondb.dev/docs/Model
- WatermelonDB Querying: https://watermelondb.dev/docs/Query
- WatermelonDB CRUD: https://watermelondb.dev/docs/CRUD
- WatermelonDB Sync Intro: https://watermelondb.dev/docs/Sync/Intro
- WatermelonDB Sync Backend: https://watermelondb.dev/docs/Sync/Backend
- WatermelonDB GitHub: https://github.com/Nozbe/WatermelonDB
- npm @nozbe/watermelondb: https://www.npmjs.com/package/@nozbe/watermelondb
- Expo SQLite 문서: https://docs.expo.dev/versions/latest/sdk/sqlite/
- op-sqlite GitHub: https://github.com/OP-Engineering/op-sqlite
- react-native-sqlite-storage GitHub: https://github.com/andpor/react-native-sqlite-storage
연관 글
- React Native Nitro와 TurboModule 정리: New Architecture에서 네이티브 모듈을 선택하는 기준: https://daily-it.duckdns.org/2026/06/18/react-native-nitro-turbomodule-guide/
- SQLite란? 서버 없이 쓰는 파일 기반 데이터베이스의 장점, 한계, 실무 사용법: https://daily-it.duckdns.org/2026/06/18/sqlite-local-database-guide/