ClaudeExpo2025/11/19 14:30

Going Universal: From a brownfield React Native and Next.js stack to one Expo app

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

claudejamodel: claude-sonnet-4-20250514

React NativeとNext.jsからExpo Universalアプリへの段階的移行

Key Points

  • 段階的移行で大規模リライトを回避
  • 共通UIパッケージでコード重複を解消
  • Expo Routerによる統一ルーティング

Summary

小規模チームがReact NativeとNext.jsの2つのアプリを、大規模なリライトなしに1つのExpo Universalアプリに統合した事例。段階的なアプローチで共通UIと機能を実現し、開発効率を大幅に向上させた。

Key Points

  • 段階的移行: 一度に全てを書き換えるのではなく、触る機能から順次移行
  • Monorepo構成: Turborepoを使用してapps/web、apps/native、packages/uiに分離
  • 共通UI: React Native + react-native-webでWeb/モバイル両対応のコンポーネント作成
  • 一時的プロバイダー: 認証・分析などの既存システムを段階的に移行するためのブリッジ実装
  • Expo Router: ファイルベースルーティングでWeb/モバイル統一
  • 新機能優先: 新機能は最初から共通パッケージで実装
  • 複雑な機能から: 決済フローなど複雑な機能を早期に移行して抽象化を検証

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

claudejamodel: claude-sonnet-4-20250514

ユニバーサル化への道:ブラウンフィールドなReact NativeとNext.jsスタックから1つのExpoアプリへ

ユニバーサル化への道:ブラウンフィールドなReact NativeとNext.jsスタックから1つのExpoアプリへ

小さなチームがブラウンフィールドなReact Native & Next.jsスタックを1つのユニバーサルExpoアプリに統合しました。大きなリライトは不要。共有UIと共有機能への着実なステップだけです。

私たちは小さなチームです。40以上の画面を持つExpoアプリとNext.js Webアプリがありました。そして両方を同期させるのに十分な人員がいませんでした(この製品は実際には1人の開発者によって保守されています)。そこで、すべてを2回構築するのをやめることにしました。私たちは(段階的に!)ユニバーサル化しました。

これは、大規模なリライトなしにNoonaをユニバーサルアプリに変えた方法の物語です。着実なステップだけで。

出発点

2つのアプリ、2つのコンポーネントセット、ルーティング、アナリティクス、認証など、すべてを2つの方法で実装。一方でバグを修正するたびに、もう一方が嫉妬する状況でした。

  • モバイル: Expo (React Native)
  • Web: Next.js
  • 現実: 機能の乖離、コピー&ペーストコード、レビュー疲れ

多くの方がこの痛みに馴染みがあるでしょう。私たちにとって、すべてを2回やり続けることは意味がありませんでした。そこで移行を開始しました。

小さな最初の勝利:モノレポと1つの共有Button

最小限のセットアップでTurborepoモノレポに移行しました:

  • apps/web(既存のWebアプリ)
  • apps/native(既存のExpoアプリ)
  • packages/ui(共有React Nativeコンポーネント)
  • packages/config(TS、ESLint、Prettier)

最初の唯一の目標:Web、iOS、Androidで1つの共有Buttonをレンダリングすること。このラウンドトリップが機能すれば、道筋は明確です。TypeScriptエラーがないこと、両プラットフォームで機能するプロダクションビルドがあることを確認し、良いチェックポイントのために明確にコミットします。

なぜこれが役立つか:

  • 「コードをコピー」ではなく「コードを共有」と考えるようになる
  • ビルド + ツールの前提に対する迅速なフィードバックを得られる
  • Turborepoはシンプルなワークスペースとキャッシュを提供する

共有UI:React Native → Web

React Nativeプリミティブの上にpackages/uiを構築し、react-native-webを使用してWebのDOM用に適応させました。

最初から作った便利なコンポーネント:

  • Nativewind: 共有レイヤーでTailwindを使用するため、ネイティブではNativewindを使用。CSS変数とダークモードに関して設定に時間がかかりましたが、確実に価値がありました。
  • Image: ネイティブではexpo-image、Webではnext/imageを使用
  • Icons: ネイティブではlucide-react-native、Webではlucide-react - 各アイコンには2つのファイル(例:earth.tsxearth.web.tsx
  • Link: ネイティブではexpo-routerのLink、Webではnext/linkを使用
  • 様々なコンポーネント: プラットフォームを活用するためのプラットフォーム固有コンポーネント。WebではRadix/base-ui、ネイティブではネイティブピッカーなどを使用可能に。checkbox、read moreコンポーネント、switch、date picker、modal root、Apple Payボタン、popover、portal、radio buttonsを作成。
  • 様々なhooks: 移行中に多くの有用なhooksを蓄積。すべてネイティブとWebで異なる実装。一方のプラットフォームではno-opだが、もう一方ではプラットフォームAPIを呼び出すものもあります(useSearchParamsなど)。ナビゲーション用のuseNavigate(ネイティブではexpo-router、Webではnext/router)、useSearchParamsuseFocusEffectuse3DSecureuseSessionStorageuseApplePayuseWindowFocusなどのhooksがあります。

機能はアプリファイルではなくパッケージとして存在

新機能はpackages/ui/features/feature-*に配置。各機能は両方のアプリでレンダリングできるコンポーネントをエクスポートします。

  • Web: ルートで機能をインポート
  • Native: 画面で機能をインポート
  • プラットフォームの接着剤(認証、ルーティング、ディープリンク)は薄く保つ

これにより出荷を続けられました。すべての新機能は一度書けば両プラットフォームで動作しました。

実際に機能させた「一時的なプロバイダー」

初日に認証やアナリティクススタック全体を移行しませんでした。代わりに、useranalytics.track、機能フラグ、テーマなどを受け入れる一時的なプロバイダーで共有UIをラップしました。両アプリが独自の実装を渡します。共有UIはインターフェースを呼び出すだけです。

これにより、数ヶ月の配管プロジェクトなしに今日から始めることができました。

ルーティング:Expo Routerへの移行

途中で、モバイルアプリをExpo Routerに切り替えました。理由:将来のWeb。ファイルベースルート + ディープリンクにより、プラットフォーム間で同じメンタルモデルが得られます。準備ができたら、Webもここでライブできます。

Expo RouterはReact Navigationの上に構築された、ネイティブとWeb用のファイルベースルーティングです。

長い中間期:遅く、着実で、退屈

ここからは手作業でした。移行のために製品を停止しませんでした。触ったときに移動するだけでした。

  • 機能に触る? packages/ui/features/feature-*に移植してから出荷
  • コンポーネントに触る? packages/uiに移動してから出荷
  • 休日と夏の週 = 小さなリファクタリングに良い時間

ボーナス効果: Webとモバイルがフィーチャーパリティでキャッチアップしました。

最初の共有機能は...チェックアウト😅

小さな設定画面から始めませんでした。新しい世界で最初に構築・出荷した機能は予約確認(チェックアウト)—最も複雑な画面でした。予約詳細の収集、支払いと3DSフローの処理、予約の確認を行います。

Adyenを中心としたユニバーサル決済インターフェースも構築しました:

  • ネイティブ: AdyenのReact Native SDKと通信
  • Web: AdyenのWebコンポーネントと通信
  • カード追加、支払いセレクター表示、CVV検証、3DSフローなどを処理

これにより、画面の1つのコードパスを保持し、内部で.native.ts.web.tsによってエクスポートされる決済レイヤーをインポートするだけで済みました。

切り替え

ほとんどの機能が共有パッケージに存在するようになったら、以下が可能になりました:

  • Webを共有機能モジュールと共有UIに向ける
  • WebルーティングをExpo Routerの構造に合わせて移動
  • 依存するものが何もなくなったら古いNext.jsアプリを削除

その時点で、どこでも動作する1つのExpoプロジェクトができました。

SEOが重要な場合、Expoの静的エクスポートはWeb用にページを事前レンダリングできます。これはExpo Router on Webを完全に採用する際に役立ちます。良いSEOが必要な動的コンテンツページのために、ExpoのSSRサポートを楽しみにしています。

私たちにとって有効だったこと

  • 極小から始める: 1つの共有Buttonがパイプラインを証明する
  • 新機能を「新しい方法」で書く: 大規模なリライトを待たない
  • 薄いプロバイダーブリッジを保つ: 今日から共有コードのブロックを解除
  • 触ったときに移動: 積極的に出荷している領域でのみリファクタリング
  • 早期に1つの複雑な機能を選ぶ: 抽象化を現実的にする

もう一度やるとしたら違うようにすること: シンプルなメトリック(例:共有UIを使用する画面の%)を追跡してモラルを維持する。

今後の展望:react-strict-domと仲間たち

react-strict-domの作業とネイティブモジュール用の標準化されたWeb APIへの推進に興奮しています。React Native 0.82もrefsを介してDOMライクなノードを導入しました。この方向性により、ユニバーサルアプリが「特別」ではなく、多くのプラットフォームで動作する通常のReactのように感じられるようになります。これを注意深く追跡しています。

最終的な注記

これには週ではなく月がかかりました。その間ずっと製品を出荷し続けました。休日と静かな週が私たちの「移行スプリント」になりました。

ブラウンフィールドなRN + Next.jsセットアップを抱える小さなチームなら:ドラマなしにこれを実行できます。忍耐強く。一度に1つずつ移動する。それが積み重なります。

ReactチームとエコシステムがUniversal Reactを可能にするために多大な努力を注いでいることは明らかです。Reactの未来は確実にUniversalだと信じています。

私たちにとって、ユニバーサル化はコードの重複を減らすだけでなく、製品構築の考え方を根本的に変えました。調整のオーバーヘッドなしにWebとモバイルで同時に機能を出荷できるようになりました。デザインシステムは一貫性を保ちます。バグ修正はどこにでも伝播します。そして小さなチームとして、その速度は重要です。

この道を検討している場合は、小さく始めてください。1つの共有コンポーネントでツールが機能することを証明します。そして出荷する各機能があなたを前進させるようにします。ブラウンフィールド移行は段階的ですが、現実的で、価値があります。