React + Viteで作っていたWebアプリを、React Native + Expoを使ったスマホアプリへ移行した。
今回の目的は、Web版を廃止することではなく、既存のバックエンドやデータ構造をなるべくそのまま利用しながら、Android / iOSで使いやすいネイティブアプリを追加すること。
単純にReactのコードをコピーすれば動くわけではなく、画面遷移、CSS、URL、ブラウザAPIなどはかなり考え方が変わった。実際に移行して分かったことを残しておく。
元の構成
元のWebアプリは、React、Vite、REST API、Webブラウザ向けCSS、URLによる画面共有、Flaskバックエンドという構成。
バックエンド側には既に必要なAPIが揃っていたので、今回は主にフロントエンドをReact Native化した。そのため「Webアプリを書き直す」というより、「同じAPIを利用する別フロントエンドを追加する」という考え方で進めた。
React Native側の構成
- React Native
- Expo
- Expo Router
- TypeScript
- React Query
- NativeWind
- i18n
- Toast
- Portal
- EAS Build
Expoを選んだ理由は、Android / iOSの両方を管理しやすく、ビルドやDeep Linkなどの周辺機能もまとめて扱いやすいため。
最初に共通化できる部分を整理した
移行時に最初にやったのは、そのまま使えるもの、少し直せば使えるもの、React Native用に作り直すものを分けること。
そのまま使いやすかったもの
- TypeScriptの型
- APIのデータ型
- バリデーション
- React Queryの考え方
- 計算処理
- 共通関数
- i18nの翻訳データ
修正が必要だったもの
- APIの呼び出し
- 認証処理
- 環境変数
- 画面遷移
- URL共有
- エラー処理
基本的に作り直したもの
- UI
- CSS
- モーダル
- ブラウザ依存処理
- Deep Link処理
- 戻る操作
特にUIについては、Web版のJSXを無理に移植するよりReact Native向けに作り直した方が早かった。
HTMLタグは使えない
Web版のdiv、h1、button、inputなどは、React NativeではView、Text、Pressable、TextInputなどへ置き換える。
Reactのコンポーネント設計自体は同じだが、UI部品は別物。
CSSはそのまま持ってこなかった
Web版のCSSをReact Nativeでそのまま利用することはできない。今回はNativeWindを利用した。Tailwind CSSに近い感覚で書けるのでWeb開発経験があると移行しやすい。
ただし、hover、CSS Grid、position、viewport単位、overflow、スクロール、テキスト省略などはブラウザCSSとの違いを意識する必要があった。
画面遷移はExpo Routerに変更
Web版ではURLを中心に画面を考えていたが、スマホアプリではナビゲーション履歴も重要になる。React Native版ではExpo Routerを使った。
ファイル構成とURLが対応するのでWeb開発経験があると比較的分かりやすい。ただしスマホの「戻る」操作まで含めて設計する必要がある。
「戻る」はWebより難しかった
React Native化して特に問題になったのが戻る操作。Androidの戻るボタン、画面上部の戻るボタン、下部ナビゲーション、Deep Link、モーダル、外部リンクから直接開いた画面などが同時に存在する。
特にDeep Linkから直接画面を開いた場合、通常の画面遷移とは履歴が違う。画面を表示できるだけでなく「どこから来て、戻ったらどこへ行くのか」まで設計しておいた方がよい。
URL共有はDeep Linkに変更
Webアプリでは共有URLを開けば目的のページを表示できる。スマホアプリでも同じ操作感にしたかったのでDeep Linkを実装した。
開発環境では独自scheme、本番ではWeb URLからアプリを起動できるApp Linksも利用した。共有機能のあるWebアプリをスマホ化する場合、Deep Linkはかなり重要。
APIはかなり再利用できた
バックエンドはFlaskのREST APIだったため、React Native側からも基本的にそのまま利用できた。フロントエンドとバックエンドが最初から分離されている場合、React Native化はかなりやりやすい。
Cookie認証は注意が必要
WebブラウザではCookieが自動的に送信されるが、React Nativeではブラウザとまったく同じ挙動にはならない。Cookie認証、Bearer Token、OIDC、Web版との認証共存などを最初に考えた方がよい。
環境変数も分けた
Web版のVITE_API_URLのような環境変数は、Expo側ではEXPO_PUBLIC_API_URLなどへ変更。ローカル開発、開発用アプリ、本番アプリで接続先を分けた。
スマホではlocalhostが開発PCではなくスマートフォン自身を意味する点にも注意。
Web API依存コードの置き換え
window、document、localStorage、navigator、locationなどはReact Nativeではそのまま利用できない。localStorageはAsyncStorageなどへ、共有やクリップボードもReact Native / ExpoのAPIへ置き換える。
移行前にWeb API依存箇所を検索しておくと、修正が必要な場所をかなり把握できる。
スマホでは入力操作も作り直した方がよい
Webで使いやすい入力画面が、そのままスマホでも使いやすいとは限らない。数字キーボード、フォーカス移動、入力欄の大きさ、キーボード表示時のレイアウトなどを調整した。
このあたりは「移植」ではなく「スマホ用UIとして再設計」と考えた方がよかった。
実機テストは早めに始めた方がよい
エミュレーターだけでは分からない問題が結構あった。特に戻るボタン、キーボード、Deep Link、共有、クリップボード、アプリ復帰、画面サイズなど。
ある程度画面ができた段階からAndroid実機で確認するようにした。
MaestroでE2Eテストを追加
スマホ版ではMaestroを使ってE2Eテストも追加した。グループ作成、メンバー追加、大会作成、卓作成、点数入力、戻る、Deep Linkなどの主要フローを自動化した。
React Native化するとWeb版では存在しなかったナビゲーション問題も増えるので、E2Eテストとの相性はよい。
Expo Doctorも定期的に確認
Expoでは依存パッケージのバージョン整合性も重要なので、npx expo-doctorを定期的に実行した。npmで単純に最新版へ上げればよいわけではない点は、通常のReact/Viteとは少し違う。
React Native化して良かった点
一番大きかったのは、既存APIをほぼそのまま利用できたこと。Web版で作ったデータベース、API、ビジネスロジック、データ構造を捨てずに済んだ。
useState、useEffect、hooks、component、TypeScriptなどReactの知識もそのまま使える。
想像以上に違った部分
逆に想像以上に違ったのは、CSS、画面遷移、戻る操作、Deep Link、Cookie、キーボード、ブラウザAPIあたり。特にナビゲーション関連はWeb版よりスマホ版の方が考える状態が多かった。
今ならこう進める
- Web版のAPIとUIを完全に分離できているか確認
- 型・API・共通ロジックを整理
- Expoプロジェクトを作成
- API通信だけ先に動かす
- Expo Routerで最低限の画面構成を作る
- NativeWindなどでUIを作り直す
- 認証を対応
- Deep Linkを対応
- 実機で戻る操作を確認
- Maestroなどで主要フローをE2E化
- EAS Buildでストア配布用ビルド
最初からWeb版の全画面をコピーするより、API接続 → ナビゲーション → 主要画面 → スマホ固有機能の順番の方が進めやすい。
まとめ
React/ViteからReact Nativeへの移行は、Reactを使っているという点ではかなり有利だった。
ただし「React WebをReact Nativeへ変換する」というより、「Web版と同じバックエンドを使うReact Native版フロントエンドを新しく作る」と考える方が実態に近かった。
再利用しやすいのはAPI、TypeScriptの型、ビジネスロジック、React Query、共通関数、翻訳データなど。作り直すことが多いのはUI、CSS、ナビゲーション、Deep Link、ブラウザ依存処理、スマホ特有の操作だった。
React/ViteでフロントエンドとAPIがきちんと分離されたWebアプリなら、React Native化との相性はかなりよい。今回もバックエンドを大きく変更することなくスマホアプリ化できたので、既存WebサービスにAndroid / iOSアプリを追加する方法としては使いやすい構成だった。

