ブログ

なぜLaravel + React + PWAだったのか — NARA NEXT開発ログ②

ブログ

こんにちは、かつコードです。

前回は、NARA NEXTが「奈良公園から先へ行く隙間を埋めるルート案内サービス」というコンセプトに至った経緯をお伝えしました。今回は、そのコンセプトをどう実装したか——技術面の意思決定を振り返ります。

バックエンドは「APIファースト」で作ると決めた

結論:LaravelはPWA専用の画面を返すのではなく、JSONだけを返すAPIとして作りました。理由は、将来のネイティブアプリ化を見据えたためです。

「Blade で画面を返す」を選ばなかった理由

技術ロードマップは、Phase A(サイト+PWA)→ Phase B(React Native/Flutterでのネイティブアプリ化)→ Phase C(ストア申請)という順番で決まっていました。ここで一番の分かれ道になったのが、Laravel側の設計です。

PWAだけを見て「Laravel + Bladeで画面を返す」構成にすると、Phase Bでネイティブアプリを作る際にバックエンドを作り直すことになりがちです。そこで最初から、Laravelは/api/v1/*のJSON APIだけを返すバックエンドとして構築し、PWAはそのAPIを叩くクライアントの1つという位置づけにしました。管理画面(スポット・店舗・ルートの入稿)だけは例外で、こちらはブラウザから直接触るものなのでFilamentという管理パネルを載せています。

React Native or Flutter? 決め手は「今持っているスキルの再利用性」

Phase Bのネイティブアプリ化候補として、React NativeとFlutterを比較しました。

観点 React Native Flutter
PWAをReactで作った場合の再利用性 高い(JS/TS共通、APIクライアントやロジックを共有可能) ほぼなし(Dartは別言語)
チームの既存スキル 学習コストほぼゼロ(Laravel+React経験あり) Dartの新規習得が必要
ネイティブUIの完成度 良好だが差が出ることもある 一般的に高評価

PWAのフロントエンドをReactで作れば、Phase Bで書いたAPI通信・ロジック層をほぼそのまま持ち込めます。この再利用性を優先し、フロントエンドはReact、将来のネイティブアプリ化もReact Native(Expo)を軸に検討することにしました。

地図タイルは「自前で持たない」と決めた

結論:Leaflet + OpenStreetMapを採用し、地図タイルという重いデータを自社サーバーに一切持たせない設計にしました。過去の失敗を繰り返さないためです。

過去の教訓:GeoJSONでインフラ費用が跳ね上がった経験

以前、別のプロジェクトで分析用の日本地図ライブラリ(都道府県境界線などの重いGeoJSONデータ)を導入した際、データサイズが肥大化し、AWSのプラン引き上げが必要になった経験があります。地図データは「軽そうに見えて重い」典型例です。同じ失敗を繰り返さないよう、NARA NEXTでは地図データを自前サーバーに持たせない設計を最初から決めました。

  • 地図ライブラリ:Leaflet + OpenStreetMapタイル(軽量・APIキー不要・タイル画像は外部配信)
  • ナビゲーション:経路計算は自前で持たず、「Google Mapsで開く」ボタンで本家アプリに委譲
  • 本格的なオフライン地図(現在地を圏外でも表示する等)はMVPでは持たない。数十〜数百MB規模になり同じ罠にはまるため

オフライン対応は「地図」ではなく「テキスト」で

圏外時の代替として用意したのは、地図タイルのオフラインキャッシュではなく、route_stopsのテキストベース時系列リスト(スポット名・所要時間・徒歩/バス手順)です。これはコスト面の妥協ではなく、「場所ではなくルートを売る」というNARA NEXTのコンセプトともちょうど一致する選択でした。Service Workerのキャッシュ戦略としては、自前データ(spots/routes)はCache First、店舗の営業時間などの鮮度が重要な情報はNetwork Firstと使い分けています。

多言語対応はシンプルなJSON列で十分と判断した

結論:英語・フランス語程度の言語数であれば、翻訳テーブルに正規化せず、各モデルにJSON列を持たせるだけで十分と判断しました。

なぜ最初から正規化しなかったのか

多言語対応というと「翻訳テーブルを別に作る」設計を思い浮かべがちですが、対応言語が英語・フランス語(+管理用の日本語)程度のうちは、nameやdescriptionを{"en": "...", "fr": "...", "ja": "..."}という1つのJSON列に持たせるだけで十分にシンプルに運用できます。UIの固定文言はLaravel標準のlang/ファイルで対応し、スポット・店舗・ルートといった動的コンテンツはJSON列で管理する、と役割を分けました。将来10言語規模に増えたときに初めて、翻訳テーブルへの正規化を検討すればよいという判断です。

なぜフランス語なのか、という点にも触れておきます。英語は当然として、フランス語を選んだ理由は「競合がほぼ皆無のブルーオーシャンだから」です。奈良の外国人向け情報発信は英語中心のものが大半で、フランス語ネイティブ向けにきちんと作られたコンテンツはまだ少ない、という市場の隙間を踏まえた判断でした。

まとめ

NARA NEXTの技術選定は、機能を実装するための判断であると同時に、「将来どう拡張するか」を先取りした判断でもありました。APIファーストにしたのはネイティブアプリ化のため、地図タイルを自前で持たないのは過去の失敗を繰り返さないため、JSON列での多言語対応は今の規模に見合った身の丈の設計のためです。派手な技術より、「今のフェーズに合った選択」を積み重ねることを意識しました。

次回は、もう一段深いテーマ——「AIに奈良の情報を書かせない」という、コンテンツの信頼性に関する決断についてお伝えします。

今日からできる3つのアクション

  • 今作っているサービスが「将来どう拡張するか」を先に考え、今の設計に反映できる部分がないか見直してみる
  • 過去のプロジェクトで「軽いはずが重かった」という失敗がなかったか棚卸ししてみる
  • 多言語対応やデータ設計で、「今の規模に見合わない正規化」をしていないか確認してみる

このシリーズの他の回もあわせてご覧ください(着想編、コンテンツ信頼性編、Media設計編、収益化編、コンテンツ拡充編、まとめ編)。


実際に使ってみる → NARA NEXT