Daily-It

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

WatermelonDB使用レビュー:次のReact Nativeプロジェクトではこう使いたい

React Native アプリが小さいうちは、AsyncStorage や薄い SQLite ラッパーでも十分に感じる。けれど、オフラインでも一覧や詳細を見せたい、検索したい、あとでサーバーと同期したい、という要件が入ると話が変わる。そこで改めて見直したのが WatermelonDB だった。

以前 WatermelonDB の紹介記事は書いた。今回は概念紹介ではなく、使ってみて感じた良い点、重い点、そして次のプロジェクトならどう始めるかという実践寄りのメモとして整理する。

関連記事

なぜ WatermelonDB を見直したのか

理由は単純だ。ローカルに持つデータが増え、ネットワークを常に信頼できず、ユーザーがオフラインでもある程度作業を続ける必要が出てきたからだ。WatermelonDB は、数百件から数万件のレコードまで扱える React / React Native 向け reactive database framework と説明されている。React Native では SQLite adapter、Web では LokiJS adapter を使う構造だ。

重要なのは lazy loading だ。起動時にデータベース全体を JavaScript メモリへ読み込むのではなく、画面が必要とするデータをその都度取りにいく。バックエンド開発者の感覚でもこれは自然だ。サーバー側でも全部をメモリに載せるのではなく、条件で問い合わせ、インデックスを使い、変更単位を管理する。

良かったところ

1. 起動時の負担を下げやすい

一番の利点は、多くのレコードを扱うアプリで起動時の負担を下げやすいことだ。状態管理 + persistence はデータが小さいうちは便利だが、保存する JSON が大きくなると起動時の復元コストが重くなる。WatermelonDB では SQLite 側で条件を処理し、画面ごとに必要なデータだけを扱いやすい。

2. React 画面とローカルデータの変更をつなげやすい

withObservables を使うと、モデルやクエリ結果を画面に反応的につなげられる。HOC スタイルは最近の hook 中心の React から見ると少し古く感じるが、ローカルDBの変更が画面に反映される流れは明確だ。

3. オフラインファースト設計が明確になる

WatermelonDB はローカルDBであり、バックエンド同期は自分で作る必要がある。これは短所でもあるが、バックエンド寄りの開発者にはむしろ設計を明確にできる利点でもある。pullChangespushChangeslastPulledAtschemaVersion、migration 情報を前提に、同期契約をきちんと決められる。

実際の数値で見るとどうか

WatermelonDB、SQLite ラッパー、AsyncStorage を同一条件で比較した公式ベンチマークは見つからなかった。WatermelonDB の GitHub にも AsyncStorage との比較ベンチマークが必要だという Issue がある。なので、以下は公式保証ではなく公開ベンチマークを読むための参考値として見るのがよい。

React Native ストレージ get 性能

Marc Rousavy の StorageBenchmark では、React Native 0.68、Hermes、iPhone 11 Pro、Debug build で、単一文字列の get を 1,000 回実行した結果が公開されている。

ストレージ 1,000回 get 結果 メモ
react-native-mmkv 12ms このテストでは最速
WatermelonDB 53ms AsyncStorageより速く、DB/ORM層もある
RealmDB 81ms オブジェクトDB
react-native-quick-sqlite 82ms SQLiteラッパー
AsyncStorage 242ms シンプルな key-value storage

この条件では WatermelonDB は AsyncStorage より約4.5倍速く、quick-sqlite よりも少し速く出ている。ただし、これは単一値の get を繰り返したテストだ。一覧クエリ、relation、同期、大量 insert/update、release build、低スペック Android では結果が変わる可能性がある。

local-first Web 比較

client-side-databases プロジェクトは、Angular の Web チャットアプリで複数の local-first DB を比較している。ここでの WatermelonDB は React Native SQLite adapter ではなく LokiJS adapter なので、モバイル判断にそのまま使うべきではない。それでも local-first アプリでの相対感は参考になる。

項目 WatermelonDB 気になった点
First full render 275ms 比較候補の中で速い部類
Insert one message 5ms かなり速い
20件メッセージ順次 insert 107ms Firebase 4639ms、PouchDB 241msより速い
Message insert to list change 4ms 画面反応が良い
Message search query time 23ms RxDB LokiJS 22msに近い
Storage usage 2164kb 速いが保存容量は小さい方ではない

この数値からの自分の結論は単純だ。AsyncStorage は設定値や小さな key-value には今でも便利。SQLite ラッパーは SQL を直接制御したいときに良い。WatermelonDB はその中間で、ローカルDB構造、relation、observable UI update、sync しやすいモデルをまとめてくれる選択肢だ。

重く感じたところ

1. 設定は軽くない

WatermelonDB は「インストールして終わり」のライブラリではない。React Native ではパッケージ、Babel decorator、schema、migration、model、adapter、database object まで順に設定する必要がある。TypeScript、decorator、React Native version、New Architecture、Expo、Hermes、JSI も早めに確認したい。

2. 同期は結局自分で設計する

同期の primitive と流れは提供されるが、バックエンド API は自分で作る。サーバーは pull で created / updated / deleted を返し、push ではクライアント変更をトランザクションで反映する必要がある。衝突処理も後回しにすると危ない。

3. schema と migration は初日から重要

ローカルDBはユーザー端末に残る。一度配布すると、サーバーDBのように直接修正しにくい。テーブル名、カラム名、server ID と local ID、削除ポリシー、デフォルト値、migration 失敗時の扱いは最初から考えたい。

次のプロジェクトならこう始める

まず WatermelonDB が担当するデータを決める。オフライン一覧、詳細、同期が必要な業務データは候補になる。単純な token、feature flag、一時的なUI状態、大容量ファイルは別の場所に置く。

データ種類 WatermelonDB 適合度 理由
オフライン一覧/詳細 高い ローカルクエリと高速表示が重要
同期する業務データ 高い pull/push 変更追跡と相性が良い
設定、token、flag 低い AsyncStorage や secure storage の方が単純
一時的なUI状態 低い React state や小さな状態管理で十分
大容量添付ファイル 低い ファイルは別保存し、DBにはパスやメタ情報だけ置く方がよい

基本インストール

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

schema と migration を先に作る

import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const mySchema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'projects',
      columns: [
        { name: 'server_id', type: 'string', isIndexed: true },
        { name: 'name', type: 'string' },
        { name: 'updated_at', type: 'number' },
        { name: 'is_deleted', type: 'boolean' },
      ],
    }),
  ],
})

Database object は一箇所に置く

import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'
import { mySchema } from './schema'
import migrations from './migrations'
import Project from './Project'

const adapter = new SQLiteAdapter({
  schema: mySchema,
  migrations,
  jsi: true,
  onSetUpError: error => {
    console.error('WatermelonDB setup failed', error)
  },
})

export const database = new Database({
  adapter,
  modelClasses: [Project],
})

jsi: true は性能面で有利だが、デバッグ方法にも影響する。DB設定と一緒にデバッグ方針も決めておきたい。

まとめ

WatermelonDB は単なる便利なローカルストレージではない。小さなローカルDBアーキテクチャに近い。単純なアプリには重いかもしれない。ただ、React Native でオフラインファースト、数千件以上のデータ、高速な一覧、同期が必要なら、今でも候補に入れる価値はある。

次のプロジェクトでは「同期はあとで付ければいい」とは言わない。schema、migration、ID、削除ポリシー、sync API を最初から一緒に設計したい。その方が WatermelonDB は重い道具ではなく、強い道具になる。

参考文献

韓国語原文: この記事は韓国語版をもとに、日本語読者向けに少し整えたものです。韓国語の原文を読む。
韓国語もぜひ応援してください。