React Native + Expoで正しいプロダクトを最速で作る
ユーザーにとって本当に価値のあるプロダクトを作るためには、人々がどのようなニーズや課題を持っているかを探ること。そしてプロトタイピングを通してプロダクトをブラッシュアップし続けることが必要です。つまり、デザインと開発をスピーディに行ったり来たりしながら、チーム一丸となってお客様を巻き込みながらプロダクト開発を進められるかがプロダクト開発成功の鍵だと言っても過言ではありません。
弊社がデザインおよび開発するプロダクトはWebアプリもあれば、iOSやAndroidアプリもありますしIoTデバイスもありますが、本記事では特に、iOSやAndroid端末向けにアプリを作り、リリースする際の話をしたいと思っています。
御存知の通り、2021年時点でスマートフォン向けにアプリを作ってリリースする方法としては、大きく分けて下記のような方法があリます。
- Native(iOSの場合はObjective-CやSwfit、androidの場合はJava)で作る
- クロスプラットフォームなフレームワーク(React NativeやFlutterなど)で作る
私は上記の方法について一通り試したものの、結果として特にゼロイチのフェーズにおいてはReact Native + Expoがベストであると考え、提案すうrことが多いです。
Fail Fast, Fail Oftenでプロダクトを作る
私が代表を務めるアンカーデザインでは、大企業の新規事業やDXプロジェクト、あるいはスタートアップの新規プロダクト開発のお手伝いをさせて頂くことが多くあります。この手のプロジェクトにおいて重要なことは「時間をかけてバグのないアプリを作る」ことではなく、そこそこのクオリティでも良いのでまずは形にして、想定する顧客に使ってもらいフィードバックを集めることです。
特に最初期のプロダクト開発において検証サイクルをいかに回すかは重要です。私達が「これがお客様にとって良いはずだ」と思って作ったUIや機能が最初からベストであることは殆どありません。検証を繰り返し実施する中で、改善点を見つけ、新たな発想を得てプロダクトを磨き込んでいくのです。このサイクルをいかに素早く回すかが、良いプロダクトを作れるかどうかの分水嶺ととなります。
昨今のスタートアップ業界に慣れ親しんだ方であればMVP(Minimum Viable Product)の概念について耳にしたことがある方も多いかと思います。MVPとは、実用最小限の機能をもったプロダクトです。下記のような図を用いで紹介されることも多いです。
上が良い例、下が良い例として示されています。「A地点からB地点に楽に移動したい」と考えている顧客がいたとして、上の例だとタイヤを作って、シャシーを作って…のような作り方だと顧客が実際に価値を享受できるのは殆ど最後のフェーズになってからです。一方で、下のような作り方をすれば最初の段階から顧客は価値を享受することができます。もちろん、自動車とスケートボードでは大きな違いがありますかから自動車と同じような移動方法は不可能です。それでも、「A地点からB地点に楽に移動したい」に対するソリューションとして、スケートボードを提供することで、解決方法の妥当性を検証する事ができるでしょう。
プロダクトの開発初期段階では、解決したい顧客の課題についても、顧客にとって適切なソリューションの形についても、どちらも抽象的であることが珍しくありません。ある程度の規模のシステム開発に慣れた方からすると適切に要求/要件定義されていないことが気持ち悪く感じるかもしれませんが、プロダクトを作りながら顧客の要求や、適切なソリューションの形を明らかにしていくことも、プロダクト開発の適切な姿のひとつでしょう。
そのようなプロダクト開発プロセスにおいて、プロダクトのリリースはできる限りクイックにできる必要があり、そのための方法としてReact Native + Expoが現時点で最適な選択肢だと思っています。
React Native + Expoの何が嬉しいのか。
ひとことで言ってしまうならば、コードを修正してから5分でお客さんのスマホにインストール済アプリに反映させられる点です。これについてもう少し具体的に説明します。まず、ネイティブで開発する場合の開発プロセス(特にアプリの新しいバージョンを配信する際)と、どの程度の違いがあるのかを少し説明します。
標準的なiOSアプリの開発プロセス
SwiftやObjective-CでiOSアプリを開発した場合は下記のような流れになるかと思います。
- XCodeでビルドする(5分ぐらい)
- TransportアプリでAppStoreConnectにアップロード(1分ぐらい?)
- AppStoreConnect側で処理が発生する(数時間以上かかる場合もある)
- AppStoreConnectで輸出コンプライアンスなどの質問に答える(慣れたら30秒ぐらい)
- 外部テスター向けに配信する場合は、Appleによるアプリの審査(数時間以上、数日かかる場合もある。)
- アプリを配信するとTestFlight経由でテスターに通知が行く
- 各テスターはTestFlightを開きアプリを更新する
なお、5については、アプリをビルドするときに、ビルドナンバーのみをインクリメントすることで、Appleによる審査をスキップする事もできます。ただしいわゆるセマンティック・バージョニング(Semantic Versioning)から厳密には逸脱してしまうの(やりようがないわけではないですが)で、開発体制によっては嫌悪されるかもしれません。
アプリをネイティブで開発する限り、アプリのプロトタイプを作ってそれを配信してチーム内や、ステークホルダーあるいはテスターに使ってもらおうと思った場合、どんなに軽微な修正であっても最低でも数時間かかります。修正に数時間かかるとなってしまうと、簡単に修正を入れてクイックに検証サイクルを回すことが難しくなってしまします。
React Native + Expoアプリの開発プロセス
React Native + Expoの場合、最低でも一度はビルドして上記フローを通る必要はあるのですが、それ以降は下記のようなプロセスでアプリにアップデートをかける事ができます。
- コマンドラインからexpo publishを実施する(数分待つ)
- iPhone端末にインストールされているアプリを再起動する。(回線速度によるけど数秒程度?)
つまり、配信済みのアプリに何らかの修正を加えたいとき、それに要する時間はネイティブで開発する場合に数時間かかるところ、React Native + Expoの場合、たったの数分で反映させる事ができるのです。
プロダクトの初期段階、つまり仕様がきっちり定まって居ない状態であーでもないこーでもないと言いながら開発するフェーズにおいてはこれは大変重要なポイントであると考えています。
気軽にアップデートをかけられるということは、いくつかのバージョンを気軽に比べてみることもできるでしょうし、とりあえず配信してみてチームの意見を聞くということもできるでしょう。アプリのアップデートにどの程度の時間がかかるかによって開発プロセスのカルチャーも変わってくるはずです。
もちろん、アプリが大規模化してきたときは別の方法を検討する必要があるかもしれませんが、スタートアップにとって一番重要なリソースは時間です。ゼロイチフェーズにおいてスピーディに価値を検証することの価値は計り知れない物があるでしょう。
React Native + Expoのデメリットについて
React Native + Expoの良いところばかり書いてましたが、当然良いところがあれば悪いところもあります。アプリの種類のよってはこれが致命的になる場合もありますから、どんなアプリにでも使えるわけではありません。メリット、デメリットについて把握しておき、それぞれのプロジェクトにおいて最適と思われる技術を選定していく必要があります。
OTAアップデートが使えないケースがある
OTAについてのメリットを説明させて頂きましたがアプリのコア部分に関するところについてはOTAでアップデートができません。例えばアプリのアイコンに変更を加えたい場合は、ビルドを作り直す必要があります。
また、ExpoのSDKそのものがアップデートされた場合もOTAでアップデートすることができませんのでビルドしてAppStoreで配布し直す必要があります。
とはいえ、アプリ開発の中でこのようなケースは結構珍しいはずで、ほとんどの修正はOTA経由で対応できるはずです。
使えないAPIやライブラリがある
React Native+Expoでアプリ開発する際に注意すべきポイントはこれでしょう。ExpoのAPI経由でiPhoneやiPadのハードウェア(マイクやスピーカー、センサー類など)の殆どを利用することはできるものの使用できないAPIも存在します。あるいは詳細な制御や、情報を取得できないものもあります。例えばカメラ周りですとネイティブアプリですとAR/VR系の機能を利用してアプリを開発することもできますが、Expoでは利用できません。また、Bluetoothなども多くの制約があります。
また、Objective-CやSwiftでアプリを開発する場合、オープンソースなどで開発されている様々なライブラリを利用する事ができますが、React Native+expoの場合、利用できるライブラリは限られています。とはいえnpmでそれなりの種類のライブラリが公開されているので、一般的なアプリを作る際に困ることはあまりないかと思います。
とはいえ、iOSの中でAIを活用した高度な自然言語処理を使いたいとか、カメラで撮影した動画像に対してコンピュータービジョン的な処理をしたいとなると、React Nativeのみでは限界がありますし、どうしてもやるとなったらインターネット経由で外部のサーバーを叩きに行き複雑な処理はサーバー側で実施する形になってしまうかと思います。テキストであればこれでも良いかもしれませんが、リアルタイムの動画処理となるとトラフィックや帯域幅の問題から難しいはずです。
(さらにクイックに開発したい場合)Expoに課金が必要
Expoは無料でも使うことができますが、有料のプランもあります。有料プランにすることでビルドの待ち時間が短くなります。時間帯にもよるのですが無料プランだとビルド開始までに数十分待たされる場合もあります。金額的には毎月29USDなので、待ち時間の低減に3000円程度を出せるかどうかは個人的に悩ましいところだなと思っています。
最初の1回はappleの審査を通す必要がある
React Native+Expoでアプリ開発する場合、開発者以外に使ってもらおうと思うとiOSの場合だとAppleのAppStoreConnectにビルドをアップロードして、TestFlightで配布することができます。この際に、個々人のAppleアカウントを内部テスターとして登録するのであれば特に問題とならないのですが、テスト用のリンクを配布してテスターを募る外部テスターと呼ばれる方法を利用する場合は、Appleの審査を通す必要があり、この審査に数日程度かかる場合があります。
この審査の基準について詳細まではわかりませんが、明らかに開発中だとわかるようなアプリだと審査に通りません。ですので中途半端に作り込むよりは、ある程度早い段階で審査を通したほうが良さそうだなと感じています。
iOSとAndroidで挙動が変わる場合がある
そこまで多いポイントではないのですが、iOSとAndroidで挙動が変わる場合があります。CSSの書き方によってiOSでは適用されたのにAndroidではうまく適用されないケースも散見されます。ですので、React Nativeで作ればiOS/Androidの2つのプラットフォームで完全に動くというわけではなく、動作確認はどうしても必要かなと感じています。
React Native + Expoを使いこなすためのTips
React Native + Expoでプロトタイピングを実施してきて、いくつかポイントとなる部分についてご紹介します。
バージョニングについて
OTAでガシガシアップデートをかけていくことによって、作っては試して作っては試してのサイクルが高速化されますが、そもそも自分がどのバージョンを使っているのかを意識することが難しくなります。TestFlight経由でアプリを配信する場合は、TestFlight側で明示的にバージョンを指定することができますが、Expoの場合は、ユーザーが意識しないうちにアプリがアップデートされる場合がありますし、今使っているバージョンがなんなのかを把握することが難しいです。
そのため、アプリの中でバージョンを簡単に確認できるようにしておくのが良いでしょう。アプリのバージョンはapp.jspnの中で設定したうえで下記のようにして取得することができます。
import Constants from "expo-constants";
console.info(Constants.manifest.version);
個人的なおすすめとしては、ログイン後のホーム画面にバージョン情報があっても「ログイン出来ないんだけど?」のようなトラブルに対応するのが難しくなってしまうので、ログイン前、アプリを起動すれば必ずバージョンを確認できたほうが適切かなと思っています。
なお、手動で毎回app.jsonを編集するのは大変なので、自動バージョニングを導入するのも良いかと思います。自動バージョニングのためのライブラリはいくつか存在しており、弊社でもプロジェクトごとに試行錯誤しているところですので、そのうち最適解と思われるものが見つかったら共有させて頂きます。
リリースチャンネルの使い方について
Expo環境ではリリースチャンネルと呼ばれる概念があります。リリースチャンネルとは、ビルド時に下記のような形で付与されるものです。
expo build:ios --release-channel=production
リリースチャンネルを設定してビルドしておくと、今後アプリをpublishするときに同様のリリースチャンネルでビルドしたアプリに対して配信されます。例えば上記のようにしてビルドしたアプリに対してOTAで更新したいときは下記のようにすることになります。
expo publish --release-channel=production
リリースチャンネルは作成できる個数に制限があるわけではないので、例えばビルド時にはすべてユニークなリリースチャンネルを指定する事もできます。例え2021年10月29日にビルドしたば本番環境向けのビルドであれば、production20211029のようなリリースチャンネルを付与することも可能です。
注意点として、ビルドごとにリリースチャンネルをつけていたとしても、iOSやAndroid端末でAppStoreに新しいバージョンのアプリを公開することによって自動的にアプリが更新される可能性がある点です。リリースチャンネルはアプリ(の箱のような部分)が持っている情報ですのでOSの仕組みでアップデートされた場合はリリースチャンネルも書き換わってしまいます。
リリースチャンネルは下記のようにすることで出力することができます。
import Constants from "expo-constants";
console.info(Constants.manifest.releaseChannel);
Expoアプリの場合、AppStoreに公開されるバージョンと、中身のバージョンの両方に気を使う必要があり、このあたりは概念として理解が難しいポイントかもしれません。
なお、Expoではビルド時に自動的にOTA配信がかかる点に注意が必要です。下記のようにしてビルドしてストアに配信したとしましょう。
expo build:ios --release-channel=production
その後、コードを修正して同様のコマンドを実施するとbuildにあわせてpublish(OTA更新)が走り、新しいコードの内容でアプリ内容が更新されます。
expo build:ios --release-channel=production
これを避けるためにビルド時に--no-publishを付与することができます。こうすることによって単純にビルドだけを実施することができます。
環境の切り替え方について
アプリ開発する上では開発中のソフトウェアをProduction、Staging、Developmentのように区別して管理する事があるかと思います。
TestFlightでのバージョンごとに、例えば0.1.1は開発環境、0.1.2はStaging、0.1.3は本番環境のバックエンドに接続しにいくビルドとしてリリースして、リリースチャンネル切り替えることによって、バージョンごとにアップデートを配信することもできますが、バージョンの管理をちゃんとやらないといけないので若干面倒です。
また、iOSの中ではひとつのアプリとして存在させることになってしまうので、どの環境に接続させに行くかによってTestFlightからアプリを切り替えないといけない点についてもやはり面倒だなと感じます。
リリースチャンネルごとに配信できることは便利な機能ではあるのですが、結局のところGithubやBitbucket側でCIを組んで個別に配信してしまうのが楽かも、と思っています。
React Nativeプロジェクトの場合、プロジェクトの情報はapp.jsonに記載するのですがapp.config.jsを利用することによって、app.jsonの内容を動的に切り替える事ができます。
参考
import merge from "deepmerge"
import productionConfig from "./app.production.json"
import stagingConfig from "./app.staging.json"
export default ({ config }) => {
if (process.env.MY_ENVIRONMENT === "staging") {
return merge(config, stagingConfig.expo)
}else if (process.env.MY_ENVIRONMENT === "production") {
return merge(config, productionConfig.expo)
}
return config
}
各設定ファイルの中はこんな感じ。iOSの場合、buildIdentifierを指定することで、特定のビルド向けのバイナリとなりますのでこの指定は必須。それだけだとiPhoneにインストールしたときにどれがdevでどれがstagingかわからなくあんっちゃうので、アイコンやスプラッシュスクリーンも差し替えておくとなお良いかもしれません。
{
"expo": {
"name": "アプリの名前",
"slug": "expo上でのアプリ名",
"icon": "./assets/icon.production.png",
"splash": {
"image": "./assets/splash.production.png"
},
"ios": {
"bundleIdentifier": "production.example.app"
}
}
}
これによって、ビルド時のコマンドとして下記のように指定することで、設定を切り替えてビルドを実施する事ができます。
MY_ENVIRONMENT=production expo build:ios
OTAでアップデートをかけるときはこんな感じです。
MY_ENVIRONMENT=production expo publish
なお、アプリの挙動(例えばどのAPIを叩きに行くとか)を変更するためには、app.jsonの中を見て判断することになります。設定情報を見るには、expo-constantsのmanifestを使います。manifestsのどの情報で判断するべきかは好みの問題もあるかと思いますが、私はios.bundleIdentifierを使うことが多いです。これはTestFlightのアプリごとに固有の情報だからですが、もっと良い方法があれば教えてください。
import Constants from "expo-constants";
console.info(Constants.manifest)
console.info(Constants.manifest.ios.bundleIdentifier)
また、これをbitbucket PipelinesやGithub Actionsと組合わせて、development/stagingブランチにマージしたら自動で該当する環境別アプリにOTA配信がかかるようにしております。例えばBitbucketだとこんな感じの設定ファイルになるかと思います。イメージを掴んで頂くために最低限の部分だけ抜き出しているので、各自の環境に読み替えてください。私の場合、devとstagingは自動で配信していますが本番だけは怖いので、事故を防ぐため手動配信にしています。
image: node:alpine
pipelines:
branches:
development:
- step:
script:
- npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
- MY_ENVIRONMENT=development npx expo publish --non-interactive
staging:
- step:
script:
- npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
- MY_ENVIRONMENT=staging npx expo publish --non-interactive
main:
- step:
trigger: manual
script:
- npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
- MY_ENVIRONMENT=production npx expo publish --non-interactive
おわりに
本記事ではプロトタイピングを繰り返しながらプロダクトを開発する状況におけるReact Native + Expoの利便性について説明させて頂きました。React NativeやExpo決して最先端のテクノロジーというわけではありませんが、開発するアプリの内容や用途次第では有力な選択肢となり得ると考えています。
Discussion