はじめに
株式会社ハローでは、一般ユーザー向けの予約代行サービス「AutoReserve」に加えて、レストラン向けのオールインワン SaaS「Respo」を Web・iOS・Android の各プラットフォーム向けに開発しています。
本記事では、複数のプラットフォームに対応した開発を効率的に進めるための技術的アプローチについて紹介します。
過去に AutoReserve 側での取り組みをこちらの記事で紹介しているので合わせてご覧ください。
課題
レストラン向けサービスの開発においては、現場のオペレーションが複雑かつ多様であり、店舗ごとの運用方法も大きく異なるという課題があります。
例えば、複数店舗を運営しているレストラングループなどでは、現場ではタブレットを使用し、バックオフィス側はPCで作業できるようWebにも対応する必要があります。
こういった状況にも対応し、より多くのレストランにご利用していただくためにも Respo では Web・iOS・Android の複数プラットフォームでサービスを展開しており、それに伴って開発効率化の重要性がより一層高まっています。
開発の効率化
複数プラットフォーム対応という課題に対して、以下の2つの主要なアプローチを採用することで開発効率の向上を実現しています。
まず、React Native と React Native for Web を組み合わせることで、単一のコードベースから複数プラットフォーム向けアプリケーションを生成できる環境を構築しました。 次に、モノレポによるプロジェクト管理により、共通コンポーネントの再利用性を最大化し、プラットフォーム間の差分を最小限に抑える仕組みを整えています。
これらのアプローチにより、従来は各プラットフォームごとに個別開発が必要だった機能の大部分を共通化することが可能となり、開発工数の大幅な削減と品質の向上を同時に実現しています。
1. React Native と React Native for Web による共通化
開発効率化の中核となるのが、React Native と React Native for Web の組み合わせです。これにより、同一のコードベースからネイティブアプリと Web アプリの両方を開発することが可能になっています。
React Native for Web は、React Native のコンポーネントをブラウザ上で動作させるためのライブラリであり、View、Text、TouchableOpacity などの React Native 固有のコンポーネントを Web 上でもネイティブと同様に機能させることができます。
例えば、以下のようなコンポーネントは Native・Web の両方から参照可能です。
import React from 'react' import { View, Text, StyleProp, ViewStyle, TextStyle } from 'react-native' export function SharedComponent({ style, textStyle, }: { style?: StyleProp<ViewStyle> textStyle?: StyleProp<TextStyle> }) { return ( <View style={style}> <Text style={textStyle}>Hello! All Platforms.</Text> </View> ) }
弊社ではネイティブアプリの開発に Expo を利用しているため、Respo の Web 版でも Expo 経由で React Native for Web を活用しています。
webpack.config.js
const createExpoWebpackConfigAsync = require('@expo/webpack-config')
const config = async function (env, argv) {
// ...
const config = await createExpoWebpackConfigAsync(
{
...env,
// ...
},
{
...argv,
mode: env.mode,
}
)
// ...
return config
}
export default config
2. モノレポによる管理
フロントエンドのプロジェクトはモノレポで管理しており、以下のような構成を採用しています:
- packages/ar_shared: AutoReserve を含むサービス全体で使用される共通コンポーネントやロジックを管理
- packages/respo_app: Respo 専用の共通コンポーネントおよびロジックを管理
- apps/respo_native: Respo の iOS・Android 向け React Native アプリケーション
- apps/respo_web: Respo の Web 向けアプリケーション
なお、上記に加えて AutoReserve の Web・Native アプリケーションも同一リポジトリで管理されています。
モノレポによる恩恵
1. パッケージのバージョン管理
依存パッケージのバージョンをプロジェクトごとに個別管理する必要がなくなり、バージョンアップ時のメンテナンス性が大幅に向上しています。
モノレポ内で共通利用されるパッケージは syncpack によってバージョンの一貫性が保たれており、複数の異なるバージョンが指定された場合は CI で警告が出る仕組みを導入しています。
2. ツールの設定ファイル共通化
設定用パッケージを作成し、linter・formatter・i18n などの共通設定ファイルを集約することで、各アプリケーションから import して利用できるようにしています。これによりコードベースの統一化が実現され、全体の可読性が向上しています。
例)packages/ar_config/eslint/base.js に全体共通の lint 設定を記述し、個別アプリからは以下のように extend して利用
const config = {
// ...
extends: ['../../packages/ar_config/eslint/base'],
}
module.exports = config
実装の詳細と開発ワークフロー
上記の2つのアプローチにより、少ないコード量での効率的な開発が実現できています。ここでは、実際の開発フローを交えながら詳細を説明します。
実際の開発ワークフロー
開発は主に以下のような流れで進めています:
- 新機能の要件を分析し、共通化可能な部分とプラットフォーム固有の部分を特定
- 共通部分を
packages/respo_appに実装 - 必要に応じて、
apps/respo_nativeおよびapps/respo_webにプラットフォーム固有の実装を追加 - 各プラットフォームでのテストと調整を実施
具体的な実装
例えば、顧客一覧画面のようにプラットフォーム間で差分が全くない画面については、Web・Native 双方で共通コンポーネントの呼び出しのみで完結します。
画面に表示される内容や API 呼び出しなどはすべて respo_app/src/components/Customers に定義されており、プラットフォームごとの差分は現在のログイン情報取得の実装(useRestaurantId)以外ありません。
Native
import React from 'react' import { Customers } from '@hello-ai/respo_app/src/components/Customers' // apps/respo_app に定義される useRestaurantId import { useRestaurantId } from 'modules/useRestaurantId' function CustomersScreen() { const restaurantId = useRestaurantId() return <Customers restaurantId={restaurantId} /> } export default CustomersScreen
Web
import React from 'react' import { Customers } from '@hello-ai/respo_app/src/components/Customers' // apps/respo_web に定義される useRestaurantId import { useRestaurantId } from 'modules/useRestaurantId' function CustomersPage() { const restaurantId = useRestaurantId() return <Customers restaurantId={restaurantId} /> } export default CustomersPage
プラットフォームごとの差分が出る場合
プラットフォーム間で差分が生じる場合、上位レベルでの共通化が困難な場合は各アプリケーション側に実装します。
一方、respo_app からプラットフォームごとに異なる実装を呼び出したい場合は、fileName.native.ts、fileName.web.ts のようにファイルを分割し、インターフェースを統一することでプラットフォームごとの処理を分岐できます。
この手法は、Web に対応していない UI ライブラリを Native で使用する場合などにも有効です。
Native (AwesomeComponent.tsx)
import React from 'react' import { AwesomeComponent } from 'awesome-component-library' function AwesomeComponentNative(props) { return ( <AwesomeComponent restaurantId={restaurantId} /> ) } export default AwesomeComponentNative
Web (AwesomeComponent.web.tsx)
import React from 'react' function AwesomeComponentWeb(props) { return ( <> {/* AwesomeComponentと同様のUIを実装する, もしくは同様の UI を実現できる別のライブラリを使う */} </> ) } export default AwesomeComponentWeb
packages/respo_app で定義される共通コンポーネント側では、通常のコンポーネントのようにそのまま使用できます。
import React from 'react' import AwesomeComponent from 'components/AwesomeComponent' function Component(props) { return ( <AwesomeComponent {...awesomeComponentProps} /> ) }
上記で紹介した共通化の導入後、新規実装はもちろん、既存機能の修正・実装追加の際にも respo_app へ実装を集約し共通化を進めることで、機能を増やしながらもコードベースの増加を最小限に抑えることが実現できています。
OTA リリースによる恩恵
React Native アプリは Expo を使って開発しているため、OTA(Over-The-Air)リリースが可能です。 OTA とは、アプリストアを経由せずにネットワーク経由でアプリのアップデートを配信する仕組みであり、審査を待つことなく迅速に最新版を提供できます。
さらに、アップデートは自動でダウンロードされ、次回アプリ起動時に反映されるため、ユーザーに更新を促す必要がありません。
OTA リリースのもう一つの利点は、ロールバックの迅速性です。 通常、アプリに問題が発生した場合はストアの審査を経なければ修正を反映できませんが、OTA により即座に修正版を配信して復旧することができます。
レストランのオペレーションに関わる POS レジ機能などは、リアルタイムでの稼働が必須です。障害が発生すると注文や会計などすべての業務が停止し、非常に大きな影響を与えます。 そのため、バグを発生させないことはもちろん、万が一の際には迅速に復旧する体制が重要です。OTA による即時のロールバックは、こうした状況において非常に有効な手段となります。
まとめ
本記事では、Respo の開発におけるクロスプラットフォーム対応のアプローチを紹介しました。 React Native と React Native for Web の組み合わせ、そしてモノレポによる効率的なコード管理により、Web・iOS・Android の3プラットフォームに対して最小限のコード量でサービス提供を実現しています。
レストラン業界のように現場オペレーションの重要度が高いシステムでは、開発効率と品質の両立が求められます。 私たちは今後も技術の進化を取り入れながら、より効率的な開発手法を模索し、レストランの皆様に迅速かつ確実に価値を届けていきたいと考えています。
余談になりますが、直近では AI コーディングの活用も進んでおり、エンジニア全員に Anthropic・Gemini の API キーが配布されることに加え、先日の記事にもあるように CTO が率先して活用を広める活動*1を行っています。 活用しているメンバーのほぼ全員が業務効率の向上を実感しており、より迅速な価値提供につながっています。
終わりに
ハローでは、レストラン業界の複雑な課題に対して技術的な解決策を提案し、レストランで働く皆様により良い価値を届け続けることを目指しています。 本記事で紹介したクロスプラットフォーム開発のような、効率と品質を両立する技術的アプローチに興味を持ち、一緒に挑戦していただける本物のエンジニアを募集しています。
少しでも気になる方は、気軽に miup に DM でお声がけください!