ダイニーで実践している React Native アプリのデプロイ技術について
この記事は React Native Advent Calendar の一日目の記事です。この記事ではダイニーにおける React Native + Expo アプリのデプロイを支える技術についてご紹介しようと思います。
ダイニーにおける React Native の活用事例
ダイニーでは、すべての人の飲食のインフラになるため、飲食店の店内向けに4つのモバイルアプリケーションを React Native にて開発しています。内訳は次のとおりです。
お会計のためのレジアプリ
対象 → iOS (iPad)
Expo, Bare Workflow
オーダーテイクのためのハンディアプリ
対象 → Android (OPPO A73, HUAWEI P30, etc)
Expo, Managed Workflow
厨房でオーダーを確認するためのキッチンディスプレイアプリ
対象 → iOS (iPad Pro 12.9'')
Expo, Bare Workflow
店頭で注文を取るためのキオスクアプリ
対象 → iOS (iPad)
Expo, Bare Workflow
ご覧の通り、Expo の Bare Workflow によって作られたアプリが多いです。ダイニーにおいて Expo を利用している理由は、ひとえに OTA Update のおかげです。Expo の OTA Update はストアの申請を経ずにユーザーが利用しているアプリをリアルタイムに書き換えることができるため、高速に開発サイクルを回していく必要があるスタートアップにおいては非常に重要な武器になります。個人的には、この OTA Update が存在しており、かつセットアップも簡単で運用しやすいというただ一点のみで React Native (Expo) はその他のクロスプラットフォーム技術を差し置いて採用する価値のあるものだと思っています。
この記事は、ダイニーで最も多く採用している React Native (iOS) + Expo (Bare Workflow) の構成でアプリケーションを継続的にデプロイしていく方法をご紹介します。
※ アップデートという観点では Managed Workflow の方が考えることが少なくて楽ではあるのですが、ダイニーのアプリでは主にプリンタとのネイティブ連携のために Bare Workflow を採用しています。
ランタイムとバンドル
React Native アプリは大きく分けるとネイティブ部分の「ランタイム」と JavaScript 部分の「バンドル」に別れています。Expo の OTA Update によって更新することができるのは「バンドル」部分で、ランタイムの更新には依然としてストアからのアプリの更新が必要になります。
バンドルの更新について
バンドルは Expo のサーバーから配信されるため、アップデートを行いたいときは expo publish コマンドでバンドルをビルドし Expo のサーバーに登録するだけで OK です。ただし、単に expo publish コマンドを実行しただけだと、そのバンドルを本番環境のアプリに配信したいのか、それともステージング環境に配信したいのかがわかりません。そういった配信先の環境をコントロールするのが「リリースチャンネル」です。
リリースチャンネルは Bare Workflow においては Expo.plist の EXUpdatesReleaseChannel キーを設定すれば良いです。ダイニーの場合は develop, staging, beta, production の4つのリリースチャンネルがあるため、それぞれの環境に応じて Expo.plist の中身を書き換えています(下図の EXUpdatesReleaseChannel を参照)。
ここで、設定値が ${ENVIRONMENT} のようになっていますが、この値は Expo.plist で直接指定するのではなく、ネイティブ側で [UpdatesController.sharedInstance setConfiguration:] を使う形で指定しています → 参考
結局、更新を配布する際のコマンドは以下のようになります。
npx expo publish --release-channel "$ENVIRONMENT" --target bare --clear
細かい点ですが、フラグとして --clear を指定しないと Metro bundler のキャッシュが使い回されてしまい、思わぬ事故につながるので注意です(もちろんダイニーではデプロイはそのたびに作り直す CI 環境から行っているので基本的には問題はないはずですが)。
また、OTA Update ライブラリとしての挙動については、昨年の React Native Advent Calendar の記事に詳しく書かれていましたので、ご参照ください(勝手に参照してごめんなさい)(非常に良い内容ですので、自分が改めて説明する需要もないだろうと判断いたしました)。
カスタムランタイムについて
前述のようにすれば、環境ごとの配信の出し分けは可能になりますが、それでもまだ互換性の問題があります。それはネイティブ部分をアップデートした際のバンドルとランタイムの互換性です。たとえば、ダイニーのレジアプリではアプリを通じて EPSON のプリンターでレシートを印刷する機能がありますが、このレシートの印刷機能のネイティブ側のインターフェイスが変わったときに、バンドル部分だけを更新してネイティブ部分を更新しないとなると、互換性の問題が生じてしまいます。
※ なお、Expo のバージョン自体を上げた場合にはバージョンの異なるバンドルの OTA Update を受け取ることはできないので、互換性の問題はなくなります。この仕様のおかげで、Expo の更新以外でネイティブの更新がない Managed Workflow ではランタイムの互換性の問題が生じることはありません(近頃は EAS Build という Managed Workflow でもランタイムを書き換えることのできる機能がリリースされているので、そういう場合は問題になってしまいそうですが…)。
こういったランタイムの互換性の問題を防ぐための方法が Expo.plist の EXUpdatesRuntimeVersion です。このバージョンが異なっていれば OTA Update により、更新が配布されることはないので安心です。実は前述の画像にも EXUpdatesRuntimeVersion の設定が見て取れます。
古いランタイムをアップデートさせる方法
ここからの項は React Native はあまり関係ない iOS アプリ固有の事情ですが、React Native で初めてアプリを書く Web エンジニアの方には有用な情報かと思いますので一応書いておきます。
iOS の設定でアプリの自動更新をオンにしていても、運が悪いとストアでの新しいバージョンの公開から数日が経ってもアプリが自動更新されないことがあります(運の問題なんですかね?詳しい情報をご存知の方がいらっしゃったら至急私までご連絡を頂きたいです…)。
こういった場合はアプリ上でアップデートを促すような通知を出してやりたいです。こういった場合は、リアルタイム性を求めるのであれば自社のサーバーにて最新バージョンを通知するようなエンドポイントを用意したり Firebase Remote Config などの仕組みを使ったりしてアプリに新しいバージョンを教えてあげれば良いです。ただ、ダイニーの場合はそれすらも面倒なため Apple のサーバーにリクエストを送ることで最新のストアバージョンを取得してしまっています。
$ curl "<https://itunes.apple.com/lookup?id=1543656422&country=JP>" | jq .results[0].version
"2.52.0"
ストアバージョンは Info.plist の CFBundleShortVersionString にて設定できます。
テスト配布
ここまでの手順で、無事に任意の環境のビルドを作れるようになりました。次はそのビルドを App Store 以外で配信するための方法についてです。
ダイニーではステージング環境およびベータ環境の配信は Test Flight にて行っております。これまでに DeployGate や Firebase App Distribution といったサービスによる配信方法も試したのですが、やはり新しい端末を用意するたびにビルドをしなければいけないという制約が面倒なため、そういった条件がなく Public Link から誰でも登録ができる Test Flight を活用しています。
環境ごとにリリースチャンネルの異なるアプリを配信したいので、ダイニーではリリースしているアプリの Test Flight を用いるのではなく、環境ごとに別々のアプリとして App Store に登録し、それぞれの環境で Test Flight を利用しています。
また、App Store へのアプリのアップロードは Fastlane で自動化しており、Fastlane の実行自体も GitHub Actions の CI 環境から行えるのでローカル環境の状態に左右されずアプリをビルドすることができるようになっています。
まとめ
ダイニーでは、少人数のエンジニアで複数のモバイルアプリケーションを開発する必要があるため、環境への投資効率や技術の学習効率を高めるためにクロスプラットフォーム技術を採用しています。クロスプラットフォーム技術を採用するよくある理由は toC のサービスでの省コスト化だと思いますが、ダイニーでは別の角度からの省コスト化と、Expo OTA Update によるリリースサイクルの高速化を目的として技術選定を行っております。
高速に機能をリリースして検証していきたいスタートアップにとって Expo OTA Update は非常に素晴らしい武器ですが、公式の説明 はやや辞書的すぎて実際にどう活用するかという点では自由度があります。この記事ではそういった技術を使ってダイニーがどのように安全かつ高速なリリースサイクルを実現しているかという一つの実例を紹介しました。これから初めて React Native でアプリをリリースするという Web エンジニアの方にお役に立つ情報が提供できていましたら幸いです。
近頃はクロスプラットフォーム技術といえば Flutter がもてはやされており、実際ユーザー体験という意味では良い技術だと思いますが、Node.js 系の豊富なツールチェインを活用でき Expo のような最高のシステムがある React Native も良いものですよ!
そんな感じで Node.js 系の技術を限界まで活用してやろうという JavaScript 脳の JavaScript 人間の皆様にはダイニーは最高の環境だと思いますので、興味があればぜひご応募ください。カジュアル面談や、ダイニーの加盟店でのお食事などのお誘いもお待ちしております!
Node.js で飲食店の新しい価値を作るフルスタックエンジニアを募集! - 株式会社dinii