雑記
【React + Expo】既存ブラウザシステム を ネイティブアプリ で再構築した話
John
公開日:2026/06/16
「会議室のタブレットがよく止まるので、なんとかしてほしい」
そんな依頼を受けたのがこのプロジェクトの始まりです。各会議室に設置されたタブレット端末でブラウザを常時起動してスケジュールを表示する既存システムがあり、それが頻繁にフリーズ・クラッシュするという問題でした。
現状を調べてみると、問題はタブレットの性能ではなく、「ブラウザで Web ページを長時間表示し続ける」という構造そのものにあることがわかりました。
そこでネイティブアプリとして会議室管理システムを全面的に作り直しています。本記事では、調査で見えた課題と、そこから導いた技術選定・設計の工夫について、現在進行中の内容として紹介します。
調査:何が問題だったのか
依頼を受けてまず既存システムを調べました。構成はシンプルで「タブレット → ブラウザ → Web ページ」というものです。タブレット上のブラウザが常時起動して会議室のスケジュールページを表示し続けています。
問題の根本は2つありました。
1. ブラウザの長時間稼働によるクラッシュ
モバイル端末のブラウザはリソースが限られており、長時間同じページを描画し続けるとメモリリークやレンダリング問題が蓄積してクラッシュします。ネットワークの一時切断なども引き金になります。
2. クラッシュしても自動復旧しない
ブラウザが落ちると、担当者が現地に行ってブラウザを再起動し、URL を手入力してページを表示し直す必要があります。会議室は複数拠点にまたがっており、この対応負荷が積み重なっていました。
加えて、バックエンドは旧世代の PHP フレームワーク(CodeIgniter)で構築されており、コードの保守性も低下していました。
| 課題 | 詳細 |
|---|---|
| 頻繁な画面ダウン | タブレットのブラウザが定期的にフリーズ・クラッシュ |
| 手動復旧の手間 | 再起動のたびにブラウザを開いて URL を入力し直す必要がある |
| 自動復旧なし | クラッシュしても自動で再表示される仕組みがない |
| バックエンドの老朽化 | 旧世代フレームワークで保守性が低い |
方針決定:ネイティブアプリ化と Laravel 刷新
調査の結果、根本的な解決策は「ブラウザをやめて、ネイティブアプリにする」ことだと判断しました。
ネイティブアプリであれば、端末のランチャーにアイコンが表示され、タップ1回で起動できます。また、OS レベルの自動起動設定と組み合わせることで、クラッシュ後の復旧を大幅に簡略化できます。同時に、バックエンドを Laravel 11 に刷新し、PC ブラウザからも同じシステムにアクセスできる Web 版も併せて提供することにしました。
技術スタックは以下のように決定しました。
| 区分 | 採用技術 | 選定理由 |
|---|---|---|
| バックエンド | Laravel 11(PHP 8.3) | CodeIgniter からの刷新。保守性・拡張性の向上 |
| Web フロントエンド | React 18 + Vite + TypeScript | PC ブラウザ対応。Laravel と同一サーバーで提供 |
| モバイルアプリ | Expo(React Native)+ TypeScript | クロスプラットフォーム対応。ストア登録不要でサイドロード配布 |
| DB | MySQL(既存スキーマ流用) | テーブル構造を変更せず、データ移行コストをゼロに |
なぜ React Native(Expo)を選んだのか
モバイルアプリの実装技術として最初に候補に上がったのは以下の3つです。
| 選択肢 | 概要 | 判断 | 理由 |
|---|---|---|---|
| Android ネイティブ Kotlin / Java |
OS ネイティブ API を直接使用 | 却下 | Android 単体では完結するが、Web 版との共通化・iOS 対応を見据えるとクロスプラットフォームに劣る |
| Flutter Dart |
Google 製クロスプラットフォーム | 却下 | クロスプラットフォーム要件は満たすが、Dart 言語の習得コストがかかる |
| React Native Expo |
JS/TS でネイティブアプリを構築 | 採用 | クロスプラットフォームかつ Web 版(React)の知識をそのまま活かせる |
今回は Android タブレット向けの単体アプリとして開発していますが、将来的な iOS 対応や Web 版との技術統一を見据えて、クロスプラットフォームで開発できることを重視しました。また、Web フロントエンドに React を採用していたため、同じコンポーネント設計・状態管理の知識がモバイルにもそのまま流用できる点が大きな後押しになりました。
React と React Native は、コンポーネント・JSX・useState・useEffect・Props の受け渡しといった根幹の考え方が共通しています。「<div> の代わりに <View> を使う」「<p> の代わりに <Text> を使う」といった差異を吸収するだけで、ほぼ同じ感覚でモバイルアプリを書き始めることができます。
|
1 2 3 4 5 6 7 8 9 |
<span style="color:#6c7086">// React(Web)での書き方</span> <span style="color:#89dceb"><div</span> <span style="color:#89b4fa">style</span>={{ <span style="color:#89b4fa">flex</span>: <span style="color:#fab387">1</span>, <span style="color:#89b4fa">backgroundColor</span>: <span style="color:#a6e3a1">'#F5F5F5'</span> }}<span style="color:#89dceb">></span> <span style="color:#89dceb"><p</span> <span style="color:#89b4fa">style</span>={{ <span style="color:#89b4fa">color</span>: <span style="color:#a6e3a1">'#231815'</span> }}<span style="color:#89dceb">></span>会議室名<span style="color:#89dceb"></p></span> <span style="color:#89dceb"></div></span> <span style="color:#6c7086">// React Native での書き方(構造はほぼ同じ)</span> <span style="color:#89dceb"><View</span> <span style="color:#89b4fa">style</span>={{ <span style="color:#89b4fa">flex</span>: <span style="color:#fab387">1</span>, <span style="color:#89b4fa">backgroundColor</span>: <span style="color:#a6e3a1">'#F5F5F5'</span> }}<span style="color:#89dceb">></span> <span style="color:#89dceb"><Text</span> <span style="color:#89b4fa">style</span>={{ <span style="color:#89b4fa">color</span>: <span style="color:#a6e3a1">'#231815'</span> }}<span style="color:#89dceb">></span>会議室名<span style="color:#89dceb"></Text></span> <span style="color:#89dceb"></View></span> |
スタイリングも CSS ではなく JavaScript オブジェクト形式(StyleSheet.create)で記述するため、CSS の知識がそのまま応用できます。
Expo とは何か
React Native だけでアプリをビルドしようとすると、Android SDK・Xcode・各種ビルドツールのセットアップが必要で、環境構築だけで相当の時間がかかります。Expo はこの問題を解決するために設計されたツールキットです。
① 開発中はアプリのビルドが不要
npx expo start でローカルサーバーを起動すると、スマートフォンにインストールした「Expo Go」アプリでそのままリアルタイムに動作確認できます。コードを変更すると即座にリロードされるため、Web 開発に近い開発体験で進められます。
② iOS / Android のクロスプラットフォーム対応
同一のコードベースから iOS 向けと Android 向けの両方のアプリをビルドできます。今回は Android タブレット専用ですが、将来的に iOS 対応が必要になっても追加コストが小さく済みます。
③ Expo SDK による機能追加が容易
カメラ・位置情報・プッシュ通知・ファイルシステムなど、ネイティブ機能を JavaScript から利用するためのライブラリ群(Expo SDK)が整備されています。将来の機能拡張を Expo の延長線上で実装できる点も、採用の後押しになりました。
④ expo prebuild によるネイティブプロジェクトの生成
APK のビルドが必要になったタイミングで expo prebuild --platform android を実行すると、android/ ディレクトリにネイティブプロジェクトが生成されます。あとは gradlew assembleRelease で APK をビルドするだけです。Expo SDK の恩恵を受けながら、最終的には完全なネイティブ APK として配布できる点が、後述のサイドロード配布とも相性抜群でした。
アーキテクチャ概要
システム全体の構成はシンプルです。
|
1 2 3 4 5 6 7 8 9 10 |
[Android タブレット(Expo アプリ)] | | HTTP GET/POST(社内 LAN) ↓ [Laravel 11 API サーバー(Linux / Docker)] | +--- React Web フロントエンドも同サーバーで提供 | ↓ [既存 MySQL DB(既存スキーマを流用)] |
重要な制約として「既存の DB をそのまま流用する」があります。テーブル構造の変更は一切行わず、データ移行コストをゼロにしています。既存システムとの並行稼働期間を短くし、スムーズな切り替えを目指した設計です。
設計上の工夫①:アダプタパターンによるデータソース切り替え
将来的にスケジュールシステムのデータソースが変わる可能性(例:クラウドカレンダーへの移行)を考慮しました。現行 DB への直接クエリをべた書きしてしまうと、そのタイミングで大規模な改修が必要になります。そこで、アダプタパターンを採用しました。
|
1 2 3 4 |
Controller └── RoomScheduleAdapter(インターフェース) ├── DBAdapter ← Phase 1: 現行 DB(稼働中) └── GoogleCalendarAdapter ← Phase 2/3: クラウド移行後 |
.env の設定を一行変えるだけでデータソースを切り替えられる設計にしたことで、移行時のコードへの影響を最小化しています。
|
1 2 |
<span style="color:#6c7086">// config/room.php</span> <span style="color:#a6e3a1">'adapter'</span> <span style="color:#89dceb">=></span> <span style="color:#89b4fa">env</span>(<span style="color:#a6e3a1">'SCHEDULE_ADAPTER'</span>, <span style="color:#a6e3a1">'db'</span>), |
現状は SCHEDULE_ADAPTER=db で既存 DB から読み取り、将来的に SCHEDULE_ADAPTER=google に変更するだけで Google Calendar API に切り替えられます。現時点の要件を満たしながら、将来の移行コスト削減も同時に織り込んだ設計です。
設計上の工夫②:ベースパスの動的注入
Web 版は Apache のサブディレクトリ以下にデプロイします。React Router の basename や API の BASE_URL をハードコードしてしまうと、サーバー移設や配置パスの変更のたびに複数箇所を書き換える必要が生じます。そこで、Laravel の Blade テンプレートでサーバーのベースパスを JavaScript のグローバル変数として注入する仕組みを作りました。
|
1 2 3 4 |
<span style="color:#6c7086">{{-- resources/views/app.blade.php --}}</span> <span style="color:#89dceb"><script></span> <span style="color:#cdd6f4">window.__APP_BASE</span> <span style="color:#89dceb">=</span> <span style="color:#a6e3a1">"{{ rtrim(parse_url(config('app.url'), PHP_URL_PATH), '/') }}"</span>; <span style="color:#89dceb"></script></span> |
|
1 2 3 4 5 6 7 8 |
<span style="color:#6c7086">// resources/js/app.tsx</span> <span style="color:#cba6f7">const</span> <span style="color:#cdd6f4">basename</span> <span style="color:#89dceb">=</span> (<span style="color:#cdd6f4">window</span> <span style="color:#cba6f7">as</span> <span style="color:#cba6f7">any</span>).__APP_BASE <span style="color:#89dceb">||</span> <span style="color:#a6e3a1">''</span>; <span style="color:#cdd6f4">root</span>.<span style="color:#89b4fa">render</span>( <span style="color:#89dceb"><BrowserRouter</span> <span style="color:#89b4fa">basename</span>={<span style="color:#cdd6f4">basename</span>}<span style="color:#89dceb">></span> <span style="color:#89dceb"><App /></span> <span style="color:#89dceb"></BrowserRouter></span> ); |
|
1 2 |
<span style="color:#6c7086">// resources/js/api/roomApi.ts</span> <span style="color:#cba6f7">const</span> <span style="color:#cdd6f4">BASE_URL</span> <span style="color:#89dceb">=</span> <span style="color:#a6e3a1">`${</span><span style="color:#cdd6f4">window.__APP_BASE</span><span style="color:#a6e3a1">}/api`</span>; |
これにより、.env の APP_URL を変えて npm run build するだけで、React Router のルーティングも API の URL も自動的に追従します。引き渡し後に環境を移設する際にも、修正箇所を最小限にできます。
アプリ配布方法:サイドロードで Google Play 費用ゼロ
通常、Android アプリを配布するには Google Play Developer アカウント(年間 $25)への登録が必要です。しかし、今回は社内の限られた端末のみへの配布なので、サイドロード(APK の直接インストール)という方式を採用しました。
- 開発 PC で
expo prebuild --platform android→gradlew assembleReleaseで APK をビルド - 生成した APK を社内共有ストレージにアップロード
- 対象タブレットでそのファイルをダウンロードしてインストール
ストアへの登録・審査・費用が一切不要で、更新も APK をアップロードし直すだけです。
テスト運用の選択肢:Expo Go + ローカルサーバー常時起動
APK をビルドする前の段階、あるいは頻繁に修正が入る検証フェーズでは、Expo Go アプリ + npx expo start によるローカルサーバー常時起動という運用形態も有効な選択肢です。
| 項目 | APK サイドロード | Expo Go + ローカルサーバー |
|---|---|---|
| 更新の反映 | APK の再ビルド・再インストールが必要 | コード保存と同時にリロード |
| 初期セットアップ | APK インストール + 設定変更 | Expo Go インストールのみ |
| 本番運用への適性 | 高い(スタンドアロン動作) | 低い(PC の起動が前提) |
| 向いている場面 | 本番・安定稼働フェーズ | 機能検証・UI 確認フェーズ |
60秒自動リロードとエラーハンドリング
会議室端末の一番の要件は「放置しておいても常に最新情報が表示されていること」です。各画面では setInterval を使い、60秒ごとに API からデータを再取得して画面を更新します。
|
1 2 3 4 5 6 7 8 9 |
<span style="color:#89b4fa">useEffect</span>(() <span style="color:#89dceb">=></span> { <span style="color:#89b4fa">fetchData</span>(); <span style="color:#6c7086">// 初回取得</span> <span style="color:#cba6f7">const</span> <span style="color:#cdd6f4">timer</span> <span style="color:#89dceb">=</span> <span style="color:#89b4fa">setInterval</span>(() <span style="color:#89dceb">=></span> { <span style="color:#89b4fa">fetchData</span>(); }, <span style="color:#fab387">60_000</span>); <span style="color:#cba6f7">return</span> () <span style="color:#89dceb">=></span> <span style="color:#89b4fa">clearInterval</span>(<span style="color:#cdd6f4">timer</span>); <span style="color:#6c7086">// 画面離脱時にクリア</span> }, [<span style="color:#cdd6f4">roomId</span>]); |
API エラーが発生した場合は Alert.alert() でエラーを表示しつつ、アプリ自体はクラッシュさせずに動作を継続します。次の 60秒後のリロードで自動的に回復する設計です。
まとめ
RoomSync のリニューアルで実現することをまとめます。
| 項目 | Before(既存システム) | After(RoomSync) |
|---|---|---|
| フロントエンド | ブラウザ表示 Web ページ | React Web + Expo ネイティブアプリ |
| 自動復旧 | なし(手動再起動が必要) | アプリ再起動で即座に復元 |
| バックエンド | CodeIgniter(旧世代) | Laravel 11 |
| PC ブラウザ対応 | なし | あり(Web 版) |
| アプリ配布コスト | — | ゼロ(サイドロード) |
| クラウド移行への対応 | 全面改修が必要 | アダプタ切り替えのみ |
「ブラウザをやめてネイティブアプリにする」というシンプルな解決策に加え、将来の移行を見据えたアダプタパターンやベースパスの動的注入といった設計上の工夫を組み合わせることで、運用コストの削減と保守性の向上を同時に実現することを目指して開発を進めています。
現在はテスト運用フェーズにあり、本番稼働に向けて検証を続けているところです。社内業務システムの刷新において「ストアを使わないネイティブアプリ配布」は意外と有効な選択肢です。同様の依頼を受けた方の参考になれば幸いです。
本記事で紹介したシステムは社内向けの業務システムです。現在も開発中のため、仕様や構成は変わる可能性があります。各ライブラリ・フレームワークのバージョンや仕様は記事執筆時点のものです。