OpenAIExpo2025/11/19 14:30

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

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

元記事

Quick Digest

要約

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

openaijamodel: gpt-5-mini-2025-08-07

ブラウンフィールドの React Native と Next.js を段階的に Expo 単一アプリへ統合

Key Points

  • モノレポで共有UI化
  • Expo Routerでルーティング統一
  • 触った箇所を段階的に移行

Summary

小規模チームが Next.js(Web)と Expo(React Native)で分散していたコードベースを、モノレポと共有パッケージを軸に段階的に一つの Expo アプリへ統一した事例。大規模な一括書き換えは行わず、まずは1つの共有 Button を動かすことから始め、機能ごとに packages/ui/features に移植していくことで並行してプロダクトを継続しつつ移行を進めた。

Key Points

  • モノレポ構成: Turborepo を使い apps/web, apps/native, packages/ui, packages/config を用意し、まずは TypeScript エラーとビルドが通る状態を確認する。
  • 共有 UI 層: React Native プリミティブ+react-native-web を基盤にし、Nativewind(Tailwind)、expo-image / next/image、lucide アイコンなどをプラットフォームごとに切り分ける(.native.ts/.web.ts)。
  • 機能パッケージ化: 新機能は packages/ui/features/feature-* として実装し、Web ではルートにインポート、Native では画面にインポートして再利用する。
  • 最小限のブリッジ: 認証や解析は最初から移行せず、user/analytics/feature flag/theme を受け取る“temporary provider”で両環境に実装を渡すことで即時共有を可能にした。
  • ルーティング統一: Expo Router に移行してファイルベースルーティングと deep link を共通化。段階的に Web を Expo Router 構成に合わせて切り替える。
  • 実務運用: 「触ったときに移す」戦略でプロダクト開発を止めずに移行を継続。最初の共有機能に難しい画面(決済・3DS を含む予約確認)を選び、抽象化を固めた。
  • オペレーションTips: 小さく始めて確証を得る(1つの Button)、コミットやビルドを区切りにする、進捗指標(共有UI採用率など)を追うとモチベーション維持に有効。

Practical migration checklist

  • Turborepo でワークスペースを作成し apps と packages を分離する
  • packages/ui で React Native ベースのコンポーネントを作り react-native-web で DOM 対応
  • プラットフォーム固有実装は .native.ts / .web.ts に分ける(画像、アイコン、リンク、決済)
  • 暫定プロバイダで auth/analytics を注入し、共有 UI を即時利用可能にする
  • 機能は触れたら packages 配下に移す「move when you touch」の運用を徹底する
  • Expo Router に合わせてルーティングを統一し、Web の移行は段階的に行う

最後に

時間はかかるがリスクは低く、結果としてバグ修正やデザインの一貫性が向上し、小規模チームでも機能の同時提供が可能になる。段階的に、かつ実際に動く状態を頻繁に作ることが成功の鍵。

Full Translation

翻訳

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

openaijamodel: gpt-5-mini-2025-08-07

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

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

Users • React Native • November 19, 2025 • 9 minutes read

Gunnar Torfi(ゲスト著者)

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

概要

私たちは小さなチームです。40 以上の画面を持つ Expo アプリと、Next.js の Web アプリがありました。両方を同期させる人手が足りませんでした(実際はこのプロダクトは単独の開発者が維持しています)。そこで、すべてを二度作るのをやめることにしました。段階的にユニバーサル化したのです。本稿は、Noona を大規模リライトなしでユニバーサルアプリに変えた方法の記録です。

出発点

  • 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)

最初の目標はシンプル:Button を共有して、web、iOS、Android でレンダリングすること。これが round‑trip(往復)で動けば道は見えます。

やるべきことは:TypeScript エラーがないこと、両プラットフォームで動作する production ビルドができること、チェックポイントとして明確なコミットを残すこと。

なぜこれが効くのか:

  • 「コードをコピー」する思考を止めて「コードを共有」する思考に変えられる。
  • ビルドやツール周りの前提に素早くフィードバックが得られる。
  • Turborepo はワークスペースとキャッシュがシンプルに使える。初めての場合は彼らの “structuring a repository” ドキュメントから始めると良い。

共有 UI:React Native → Web

packages/ui を React Native のプリミティブ上に作り、react-native-web で Web DOM に適合させました。最初から作った便利コンポーネントの例:

  • Nativewind:共有レイヤーで Tailwind を使うために Nativewind を native 側で使用。CSS 変数とダークモード周りで調整に時間はかかったが価値あり。
  • Image:native 側は expo-image、web 側は next/image を使用。
  • Icons:native は lucide-react-native、web は lucide-react。各アイコンは earth.tsxearth.web.tsx のようにファイルを分けている。
  • Link:native は expo-routerLink、web は next/link を使用。
  • 各種プラットフォーム固有コンポーネント:必要に応じてプラットフォーム固有実装を持たせている(例:Radix/base-ui は web、ネイティブはネイティブピッカーを使うなど)。その結果、チェックボックス、read more コンポーネント、スイッチ、日付ピッカー、modal root、Apple Pay ボタン、ポップオーバー、ポータル、ラジオボタンなどを個別実装した。

フック類

移行中に有用なフックを多数蓄積しました。各フックは native と web で別実装になっているものが多く、片方では no-op、もう片方でプラットフォーム API を呼ぶもの(例:useSearchParams)もあります。

例:useNavigate(native は expo-router、web は next/router)、useSearchParamsuseFocusEffectuse3DSecureuseSessionStorageuseApplePayuseWindowFocus

react-native-web のリポジトリはこの "RN to DOM" のジャンプに慣れるための良い出発点です。

機能はアプリのファイルではなくパッケージとして生きる

新しい機能は packages/ui/features/feature-* に入ります。各 feature は両アプリでレンダリングできるコンポーネントをエクスポートします。構成は自由で、各機能を別パッケージにしたり、すべてを 1 つのパッケージにまとめたりできます。

  • Web:ルートで feature を import
  • Native:スクリーンで feature を import

この設計を薄いままにしておくことで、毎回機能を一度書いて両プラットフォームで動かすことができました。

薄いプラットフォーム接着層(認証、ルーティング、ディープリンクなど)

これが私たちの継続的なデリバリを支えました。すべての新機能は一度書かれ、両方で動作しました。

『一時的プロバイダー』が実際の動作を可能にした

認証や解析スタックを初日にすべて移行することはしませんでした。代わりに、共有 UI をラップする一時的なプロバイダーを作り、useranalytics.track、フラグ、テーマなどを受け取れるようにしました。両アプリがそれぞれの実装を渡し、共有 UI はそのインターフェースを呼び出します。これにより、数ヶ月に及ぶ配線作業を待たずに今日から作業を始められました。

ルーティング:Expo Router に移行

途中でモバイルアプリを Expo Router に切り替えました。理由は「将来の web 対応」のためです。ファイルベースのルートとディープリンクはプラットフォームを超えて同じメンタルモデルを与えてくれます。準備ができれば web もここに置けます。

Expo Router は native と web 向けのファイルベースルーティングで、React Navigation の上に構築されています。

長い中間期間:ゆっくり着実、地味な作業

ここからは手作業でした。プロダクトを止めて「移行」を行うことはしませんでした。触ったときに移す、という方針です。

  • 機能に触る?packages/ui/features/feature-* に移してからリリース。
  • コンポーネントに触る?packages/ui に移してからリリース。
  • 休暇や夏の週は小さなリファクタの良い時間。

このやり方には副次効果があり、Web と Mobile の機能差が徐々に埋まりました。

私たちの最初の共有機能は…チェックアウトでした 😅

最初に作ってリリースしたのは小さな設定画面ではなく、Reservation Confirmation(チェックアウト)という最も複雑な画面でした。予約詳細の収集、支払いと 3DS のフロー、予約確定を扱います。

ユニバーサルな支払いインターフェースも作りました(Adyen を中心に):

  • native:Adyen の React Native SDK と対話
  • web:Adyen の web コンポーネントと対話

カードの追加、支払い選択、CVV 検証、3DS フローなどを扱い、画面のコードパスは 1 つに保ちつつ、支払いレイヤーを .native.ts.web.ts でエクスポートしました。

切り替え(Cutting over)

ほとんどの機能が共有パッケージに移されたら、次のように進められます:

  • Web を共有モジュールと共有 UI に向ける
  • Web のルーティング(およびリンクルール)を Expo Router の構造に合わせる
  • 依存がなくなったら古い Next.js アプリを削除

その時点で、すべてを走らせる 1 つの Expo プロジェクトを持つことになります。SEO が重要なら、Expo の static export で web 向けにページをプリレンダリングできます。Expo Router を web で本格採用する場合に有用です。動的コンテンツの良好な SEO のために Expo の SSR サポートが進むのを楽しみにしています。

私たちに効いたこと

  • 微視的に始める。1 つの共有 Button がパイプラインを証明する。
  • 新しい機能は「新しいやり方」で書く。大規模リライトを待たない。
  • 薄い provider ブリッジを維持する。共有コードの即時アンロックになる。
  • 触ったときに移す。アクティブに出荷している領域だけをリファクタする。
  • 早い段階で手強い機能を一つ選ぶ。抽象化が現実的になる。

1 つだけ再度やるなら変えたいこと:共有 UI を使っている画面の割合(例:% of screens using shared UI)といったシンプルな指標を追うことで士気が上がる。

今後の注目点:react‑strict‑dom 等

react‑strict‑dom の動きや、ネイティブモジュール向けに標準化された Web API への推進に期待しています。React Native 0.82 も refs 経由で DOM ライクなノードを導入しました。この方向性はユニバーサルアプリが特別扱いされる感じを減らし、多数のプラットフォームで動く普通の React のように感じさせてくれます。私たちはこれを注視しています。

最後に

これは数週間ではなく数か月かかりましたが、その間もプロダクトを出荷し続けました。休暇や静かな週が私たちの「移行スプリント」になりました。

もし小さなチームでブラウンフィールドな RN + Next.js セットアップに悩んでいるなら、ドラマなしでこれを成し遂げられます。忍耐強く、1つずつ移してください。積み重なります。

React チームやエコシステムがユニバーサル React を実現するために多くの努力をしているのは明らかです。React の未来は確実にユニバーサルだと私は信じています。

私たちにとってユニバーサル化は単にコード重複を減らすこと以上の意味がありました。プロダクトの作り方が根本的に変わりました。今では Web と Mobile に同時に機能を出荷でき、調整コストが不要になりました。デザインシステムは一貫し、バグ修正はどこにでも波及します。小さなチームにとってこの速度は重要です。

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