Daily-It

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

What Is WatermelonDB in React Native? Offline-First Local DB Usage Compared with SQLite

Summary

WatermelonDB is for quickly handling large amounts of local data in React Native apps. An offline-first responsive database frameworkall. Internal storage mainly uses SQLite, but rather than developers continuing to write SQL directly, the app data flow is structured through Schema, Model, Query, Writer, and Sync API. It may be an overkill for a simple cache or storing a few settings, but for apps that require thousands to tens of thousands of records, synchronization, relational data, and automatic screen updates, it can be more productive than using SQLite directly.

If you want to organize SQLite concepts first, The advantages, limitations, WAL, and lock error resolution of serverless file-based DB are SQLite Practical Guide ↗It has been summarized in detail separately.

Verification criteria: At the time of writing, the latest version of npm is @nozbe/watermelondb It is confirmed as 0.28.0, but the official document page is displayed as 0.27.1. Before installation, it is safer to check the project’s React Native/Expo version and WatermelonDB release/issue.

Table of Contents

Background

When creating a React Native app, at first it seems sufficient to just call the server API and draw the screen. But when the next request comes, the story changes.

  • It must be possible to create/edit even if the network is disconnected.
  • The app needs to quickly display thousands of tasks, messages, orders, and customer data immediately after launching.
  • When local data changes, related screens should be automatically updated.
  • Server and local data must be synchronized periodically.
  • Relational data is needed, not a simple key-value store.

The option that often comes to mind at this time is SQLite. SQLite is close to the de facto standard for mobile local DB, and has good performance and stability. However, when using SQLite directly in React Native, SQL writing, result mapping, screen updating, synchronization conflict handling, migration, etc. must be designed directly in the application code.

WatermelonDB is a tool that tries to solve this problem. Rather than replacing SQLite, A framework that builds a data layer suitable for React/React Native apps on top of SQLite.close to

What Is WatermelonDB?

WatermelonDB is an open source local database framework created by Nozbe. The main points of the official introduction are as follows.

  • It aims to quickly handle large amounts of local data in React Native and React apps.
  • Rather than loading all of the data into JavaScript memory at once, it is read lazily when needed.
  • Queries are performed in a native DB such as SQLite.
  • By providing an observable structure based on RxJS, the UI can be automatically updated when data changes.
  • You can implement offline priority synchronization by connecting your own backend.

In other words, WatermelonDB is not just a SQLite wrapper. The following layers are provided together.

Component Role
Schema Table and column definitions
Model Define data objects handled by your app
Collection Record access unit for a specific table
Query API Condition-based lookup
Writer Write transaction concept that securely bundles create/modify/delete operations
Observable UI automatically reflected when data changes
Sync API Synchronization of changes between server and local DB

Why WatermelonDB Is Fast

WatermelonDB’s performance points do not end with “It’s fast because it’s SQLite.” The more important part is Structure that does not unnecessarily pull data into the JavaScript areaall.

The general state management + persistence method is easy to load a lot of data into JS memory when the app starts. There is no problem when there are hundreds of pieces of data, but when the number increases to thousands or tens of thousands, app startup speed and memory usage can become noticeably worse.

WatermelonDB reduces this problem in the following way:

1. When starting the app, do not load the entire DB into JS memory. 2. Execute only the necessary queries on the necessary screens. 3. Conditional search is processed through DB query, not JS array filtering. 4. Only components connected to changed data are updated as observable.

If you are experiencing the problem of “the app becomes heavier as the amount of local data increases” in your React Native app, it is worth considering WatermelonDB.

Basic React Native Usage

The example below is a simple structure for storing blog posts and comments. In an actual project, you can adjust the folder structure and import alias accordingly.

1. Installation

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

Or, if you use Yarn, install it as follows.

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

WatermelonDB uses decorator syntax, so Babel configuration is required.

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

On iOS, CocoaPods setup may be required. The official document is simdjson Add pod pod installguide you through

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

I tend to get stuck here often. especially use_frameworks!, Expo, and React Native New Architecture combination require additional confirmation depending on the project status.

2. Schema definition

In WatermelonDB, DB Schema is first defined. Table names and column names are usually snake_caseUse .

// 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_idColumns frequently used in relationship searches are as follows: isIndexed: trueIt is better to add . Just as indexes are important in SQLite, in WatermelonDB, index design that matches the query pattern affects performance.

3. Model definition

If Schema is the actual DB structure, Model is an object to be used in app code.

// 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
}

The important point here is that DB changes are not done anywhere. In WatermelonDB, you can create, modify, and delete database.write() or @writer Process it from within.

4. Create 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. Data creation and inquiry

// 글 생성
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. Connect with React component

This is where WatermelonDB’s strengths are revealed. Rather than just fetching the data once, if you connect it to an observable, the screen is automatically updated when the data changes.

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)

Lists can also be connected to Queries as observables. Thanks to this structure, the need to manually create the “DB change → status update → list recheck → screen update” flow each time is reduced.

SQLite vs. WatermelonDB

Rather than being a separate DB that completely replaces SQLite, WatermelonDB is closer to a tool that allows SQLite to be used at a higher abstraction in React Native apps. So the standard of comparison is not “SQLite is better/WatermelonDB is better” What level of control and productivity do you need?all.

item Using SQLite directly WatermelonDB
basic personality Local relational DB engine/API SQLite-based responsive DB framework
data access Write your own SQL Using Model, Collection, Query API
performance Very fast if you design your query well lazy loading + native DB query + observable structure
React UI update Requires direct state management Automatic renewal possible with observable
relational data Join/Query directly with SQL Association and relationship models provided
migration Design your own Using WatermelonDB migration API
synchronization implement it yourself Sync primitive and protocol provided, but backend is directly required
running curve Easy to access if you know SQL Need to learn WatermelonDB rules and decorator and writer patterns
Debugging Tracking based on DB file/SQL WatermelonDB abstraction and native build issues must be viewed together
suitable app Apps where simple local storage and SQL control are important Offline-first, high-data, auto-UI refresh, sync-centric apps

When it is better to use SQLite directly

For the following apps, there is no need to introduce WatermelonDB.

  • There is not much data to store.
  • Only simple settings, cache, and recent search words are saved.
  • It is more important to write and tune SQL yourself.
  • There is no need for an observable structure that is automatically connected to React UI.
  • The team cannot afford to learn WatermelonDB’s writer/model/sync rules.

In React Native expo-sqlite, react-native-sqlite-storage, op-sqlite, react-native-quick-sqlite There are similar options. Especially if it’s an Expo-based project. expo-sqlitemay be simpler in terms of installation and compatibility.

When WatermelonDB is better suited

Conversely, if the following conditions are met, WatermelonDB has strengths.

  • Thousands to tens of thousands of pieces of data are stored locally.
  • App execution speed is important.
  • Data must be created/edited even when offline.
  • It is necessary to synchronize changes with the server.
  • The same data must be viewed from multiple locations on the screen and updated automatically.
  • I want to organize my code around the app domain model rather than SQL.

If you summarize it in one line, it is like this.

SQLite is a “local DB engine,” and WatermelonDB is a “SQLite-based data layer for React Native apps.”

Synchronization Architecture

WatermelonDB is not a tool that only provides local DB. It also provides a sync function with offline-first apps in mind. However, there is something that should not be misunderstood here.

WatermelonDB does not create a server for you.

Based on official documentation, WatermelonDB provides the following:

  • Track changes created/edited/deleted locally
  • synchronize() API
  • pullChanges, pushChanges synchronization flow in the form
  • changes object structure that the backend must adapt to

On the front end, it looks roughly like this.

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

The backend must exchange changes in the following format:

{
  "changes": {
    "posts": {
      "created": [
        { "id": "post_1", "title": "Hello", "body": "...", "is_pinned": false }
      ],
      "updated": [],
      "deleted": []
    },
    "comments": {
      "created": [],
      "updated": [],
      "deleted": ["comment_1"]
    }
  },
  "timestamp": 1710000000000
}

The key here is consistency of server timestamp and change tracking. The official documentation also has a pull endpoint lastPulledAt It explains that all subsequent changes must be returned without exception, and server time must be set on a consistent basis. If you make this part sloppily, you may end up with the problem of “some records will never be synced.”

Actual blockage and direction for solving it

WatermelonDB is powerful, but it involves React Native native modules, Babel, CocoaPods, Expo, and a synchronization backend. During actual development, it often gets stuck at the point below.

Case 1. Decorator syntax is not recognized

Symptoms usually look like this:

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

First, check Babel settings.

npm ls @babel/plugin-proposal-decorators

.babelrc or babel.config.jsCheck whether the legacy decorator setting is entered.

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

A common mistake here is to only look at the TypeScript settings and miss the Babel settings. Since the WatermelonDB example uses a decorator, the corresponding grammar must be processed in the Metro/Babel pipeline.

Case 2. On iOS simdjson.h Or, a Pod-related build error occurs.

In iOS build simdjson If a related error occurs, check the Podfile and pod install status first.

cd ios
pod install

The official documentation guides you through Podfile. simdjson Also check whether the settings have been entered.

pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true

use_frameworks!Projects that use should be more careful. The WatermelonDB official installation document also provides guidance to the effect that frameworks mode is not recommended, and related build issues are continuously mentioned in GitHub issues. The project is already use_frameworks!If you rely on , it is safer to first verify iOS builds on a small sample branch before introducing WatermelonDB.

Case 3. Native module not found in Expo Go

Expo Go does not include arbitrary native modules at will. Libraries that require native code, such as WatermelonDB, may not run directly in Expo Go.

Example symptoms are similar to:

NativeModules.WMDatabaseBridge is not defined

In this case, there are usually three options.

1. Use the Expo development build. 2. Review config plugin or custom native settings. 3. If the requirements are simple expo-sqliteLower the range to

For an Expo project, rather than asking “Is WatermelonDB any good?”, you should first ask “Can native configuration be managed in the current Expo workflow?”

Case 4. React Native New Architecture compatibility is uncertain

After React Native 0.7x, you must check WatermelonDB compatibility issues in the combination of New Architecture, Bridgeless, and the latest Expo SDK. Questions and problems related to New Architecture, RN 0.76+, and Expo SDK 54+ are posted in GitHub issues.

If it is an operating app, it is recommended to check in the following order.

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

Also, check the RN/Expo version of the project and the WatermelonDB GitHub issue. Before turning on the new architecture, you must first verify that the local DB is initialized properly and that the iOS/Android release build is complete.

Case 5. Duplicate or missing synchronization

WatermelonDB synchronization does not end with front-end code alone. backend lastPulledAt Afterwards, the changes must be returned accurately.

If a problem occurs, check the following:

  • When do I take a server timestamp?
  • Is there any possibility that changed data may be missing during pull?
  • Are deleted records brought down to the ID list?
  • Do you understand the structure of receiving changes pushed by the client again in the next pull?
  • What policy does the server have to fail/allow when a conflict occurs?

In apps where synchronization is key, backend sync protocol design may be more important than introducing WatermelonDB.

When to use it and when to avoid it?

When to recommend WatermelonDB

  • I’m building an offline-first app.
  • There is a lot of local data and app execution speed is important.
  • A relational data model is needed.
  • Data changes should be automatically reflected on multiple screens.
  • The sync protocol can be implemented through its own backend.
  • Teams can handle React Native native build issues.

When to avoid or hold off on WatermelonDB

  • Only simple settings saving is required.
  • It only briefly caches server API responses.
  • It must be developed only with Expo Go, and native build configuration is difficult.
  • I am actively using the latest React Native New Architecture, but I do not have time to verify compatibility.
  • There are many complex analytical queries that require direct control of SQL.
  • I can’t afford to design the synchronization backend myself.

Conclusion

If you look at the core of WatermelonDB as “a library that makes using SQLite more convenient in React Native”, it is a bit lacking. More precisely A data framework that considers large amounts of local data, responsive UI, and offline-first synchronization.all.

Compared to SQLite, WatermelonDB gives up some direct control, but gains app-level features such as model-driven development, observable UI updates, and sync primitives. So, it is overkill for apps that require simple storage, but it is quite suitable for apps where data continues to accumulate and change even when offline.

If you are considering WatermelonDB in a React Native project, you can judge it like this first.

1. Is there really going to be a lot of data? 2. Is offline creation/editing necessary? 3. Do I need to synchronize with a server? 4. Do you have time to verify Expo/RN/iOS/Android build compatibility? 5. Are the app domain model and responsive UI more important than direct control of SQL?

If you answered “yes” to most of these questions, you might want to give WatermelonDB a try. Conversely, if only two or three apply, first expo-sqliteme op-sqlite A strategy of simply starting with the same SQLite library and moving to WatermelonDB when the data structure grows is also realistic.

References