Daily-It

개발, AI, 인프라, 자동화와 일상 IT 제품 후기를 직접 써보며 정리하는 기술 블로그입니다.

What Is WatermelonDB in React Native? Offline-First Local DB vs SQLite

Summary

WatermelonDB in React Native is best understood as an offline-first data layer for apps that need fast local queries, reactive UI updates, and server synchronization on top of SQLite. It is not just SQLite with a nicer API: it adds schema/model rules, query APIs, writer transactions, observables, and sync primitives. For a few settings or a small cache it is probably too much, but for thousands or tens of thousands of records that must keep working offline, it can be more productive than hand-writing SQLite plumbing.

Need the SQLite basics first? WAL mode, file-based database behavior, and lock-error troubleshooting are covered separately in the SQLite practical guide ↗.

Version check: at the time this Korean source was written, npm showed @nozbe/watermelondb 0.28.0 while some official documentation pages still displayed 0.27.1. Before adopting it, check your React Native/Expo version, the WatermelonDB release notes, and open GitHub issues together.

In this article

Why this matters

A React Native app often starts by calling an API and rendering the response. That works until users must create or edit data offline, the app must show thousands of tasks or messages immediately after launch, local changes must refresh several screens automatically, or server synchronization becomes a requirement. SQLite is a strong local database choice, but direct SQLite use means you design SQL access, result mapping, state refresh, migrations, conflicts, and sync logic yourself. WatermelonDB puts a React/React Native-oriented data framework on top of SQLite.

What WatermelonDB is

WatermelonDB is an open-source local database framework by Nozbe. Its core idea is to keep large local datasets usable in React and React Native without loading everything into JavaScript memory at startup.

  • It reads data lazily instead of loading the entire database into JS memory.
  • Queries run in a native database such as SQLite.
  • RxJS-based observables can update UI when data changes.
  • It provides model, collection, query, writer, migration, and sync APIs.
  • For offline-first sync, you still need your own backend.
Component Role
Schema Defines tables and columns.
Model Defines the objects used in app code.
Collection Access point for records in a table.
Query API Runs condition-based queries.
Writer Groups create, update, and delete operations into safe write blocks.
Observable Lets UI react when records or queries change.
Sync API Defines how local and server-side changes are exchanged.

Why it can stay fast with large local data

The important point is not only that SQLite is fast. The bigger point is that WatermelonDB tries not to pull unnecessary data across the native-to-JavaScript boundary. It queries only what a screen needs, keeps filtering in the database instead of in JS arrays, and updates only the components connected to changed observable data.

Basic React Native setup

1. Install the packages

npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators
yarn add @nozbe/watermelondb
yarn add --dev @babel/plugin-proposal-decorators

WatermelonDB examples use decorators, so Metro/Babel must be configured for legacy decorators.

{
  "presets": ["module:metro-react-native-babel-preset"],
  "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]
}

On iOS, CocoaPods setup may also be required.

# ios/Podfile
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true
cd ios
pod install

Projects using use_frameworks!, Expo, or React Native New Architecture should verify iOS and Android builds in a small branch before making WatermelonDB part of the main code path.

2. Define schema and models

// 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 },
    ]}),
  ],
})

Columns such as post_id that are used frequently for relationship lookups should be indexed. Writes should happen inside database.write() or @writer; otherwise the data layer becomes difficult to reason about.

// src/db/index.js
import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'

const adapter = new SQLiteAdapter({ schema: mySchema })
export const database = new Database({ adapter, modelClasses: [Post, Comment] })
import { Q } from '@nozbe/watermelondb'

const pinnedPosts = await database
  .get('posts')
  .query(Q.where('is_pinned', true))
  .fetch()

3. Connect data to React UI

import { withObservables } from '@nozbe/watermelondb/react'

function PostItem({ post }) {
  return <Text>{post.title}</Text>
}

export default withObservables(['post'], ({ post }) => ({ post }))(PostItem)

This is the part that makes WatermelonDB different from a plain local-storage wrapper: a record or query can be observed, and connected UI can update when the underlying data changes.

SQLite vs. WatermelonDB

The practical question is not which one is universally better. It is how much control you need, and how much application-layer structure you want WatermelonDB to provide.

Area Using SQLite directly Using WatermelonDB
Base role Local relational database engine/API SQLite-based reactive data framework
Data access You write SQL and map results yourself You use models, collections, and query APIs
Performance Excellent when queries are designed well Lazy loading, native queries, and observable updates
React UI You manage state and refresh logic Observables can refresh connected UI
Sync You implement everything Sync primitives are provided, but the backend is still yours

Use SQLite directly when

  • The local dataset is small.
  • You only need settings, cache, or recent search history.
  • SQL control and tuning matter more than model abstraction.
  • You do not need automatic React UI updates from local data changes.

For React Native, alternatives such as expo-sqlite, react-native-sqlite-storage, op-sqlite, and react-native-quick-sqlite may be simpler, especially in Expo projects.

Consider WatermelonDB when

  • You are building an offline-first app.
  • The local database may grow to thousands or tens of thousands of rows.
  • Data must be created and edited while offline.
  • Multiple screens need to react to the same local data.
  • You can implement and maintain the backend sync protocol.
  • The team can handle React Native native build issues.

How synchronization works

WatermelonDB includes sync primitives, but it does not build the server for you. The client side calls synchronize() and provides pullChanges and pushChanges. The backend must return a consistent changes object and timestamp.

import { synchronize } from '@nozbe/watermelondb/sync'

export async function syncDatabase(database) {
  await synchronize({
    database,
    pullChanges: async ({ lastPulledAt, schemaVersion }) => {
      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,
  })
}

The hard part is making sure the backend never misses changes after lastPulledAt, handles deleted IDs, uses a consistent server timestamp, and has a clear conflict policy.

Where teams usually get stuck

Case 1. Decorator syntax is not recognized

SyntaxError: Support for the experimental syntax 'decorators' isn't currently enabled

Check that the dependency and Metro/Babel config are both present. A common mistake is to look only at TypeScript settings.

Case 2. iOS build fails around simdjson.h or Pods

Check ios/Podfile, run pod install, and compare your setup with the official installation guide. If the project uses use_frameworks!, verify carefully.

Case 3. Expo Go cannot find the native module

NativeModules.WMDatabaseBridge is not defined

Expo Go does not include arbitrary native modules. Use an Expo development build, review config/native setup, or reduce scope to expo-sqlite if the requirements are simple.

Case 4. New Architecture compatibility is uncertain

For React Native 0.7x+, Bridgeless, and newer Expo SDK combinations, check WatermelonDB issues before adoption.

npm view @nozbe/watermelondb version
npm view react-native version

Case 5. Sync duplicates or misses records

If sync is the core requirement, backend protocol design matters more than simply installing WatermelonDB. Check server timestamp generation, possible missing changes during pull, deleted ID handling, and conflict policy.

When to use it, and when to avoid it

Good fit

  • Offline-first app behavior is a real requirement.
  • The local dataset is large and startup speed matters.
  • Relational data and reactive screens are important.
  • You can build a reliable sync backend.
  • Your team can debug React Native native modules.

Hold off

  • You only store settings or a short-lived API cache.
  • You must stay inside Expo Go without native build management.
  • You are already adopting a new RN architecture and cannot spend time verifying compatibility.
  • You need complex SQL analytics more than a model-based app data layer.
  • You cannot design or operate the sync backend.

Conclusion

WatermelonDB is more than a convenience wrapper around SQLite. It is a data framework for React Native apps that combine large local datasets, reactive UI, and offline-first synchronization. If most of those needs are real, test WatermelonDB in a small branch. If you only need a small cache or a few settings, start with a simpler SQLite library and move later if the data layer grows.

References

Original Korean version: This article is based on the Korean version and lightly adapted for English readers. Read the original Korean post.
Please show some love to Korean, too.