MacがなくてもiOS開発ができる!expo-dev-clientについて
この記事はReact Native 全部俺 Advent Calendar 20目の記事です。
このアドベントカレンダーについて
このアドベントカレンダーは @itome が全て書いています。
基本的にReact NativeおよびExpoの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はXにお願いします。
Expo Buildは開発用/本番用のバイナリを簡単にビルドできるクラウドサービスです。開発時の基本はExpo Goを使用し、必要に応じてexpo-dev-clientを導入することで、カスタムネイティブコードを含むアプリの開発も可能になります。本番用のビルドはapp.jsonの設定でビルドプロファイルを切り替えて管理でき、ローカルに開発環境を用意する必要がないため、チーム全員が同じ環境でビルドを行えます。
まずはExpo Goから始める
Expoでアプリを開発する時、最初に使うのがExpo Goです。スマートフォンにExpo Goをインストールし、npx create-expo-app
で作成したプロジェクトをnpx expo start
で起動するだけで、すぐにアプリの開発が始められます。React Nativeの知識があれば、特別な環境構築なしにアプリ開発をスタートできます。
Expo Goを使った開発では、コードの変更がリアルタイムで反映され、UIの調整やロジックの実装を効率的に進められます。Expoが提供する様々なAPIも、追加の設定なしですぐに使えます。カメラやプッシュ通知、位置情報など、モバイルアプリでよく使われる機能の多くが、Expo Goの中に組み込まれています。
expo-dev-clientが必要になるケース
アプリ開発を進めていく中で、以下のような状況に遭遇することがあります:
- アプリで使いたい機能がExpo Goに組み込まれていない場合
- 既存のネイティブライブラリを使う必要がある場合
- アプリの起動時にカスタムの初期化処理を入れたい場合
- 特定のデバイスでしか使えない機能を実装したい場合
- プッシュ通知やBluetoothの挙動をカスタマイズしたい場合
このような場合、2つの選択肢があります。1つは従来のようにexpo prebuild
を実行してネイティブプロジェクトを作成し、XcodeやAndroid Studioでローカルビルドする方法です。もう1つはexpo-dev-clientを導入する方法です。
ローカルビルドは完全な自由度が得られる一方で、iOS/Androidそれぞれの開発環境を用意し、ネイティブアプリの開発知識も必要になります。対してexpo-dev-clientは、Expo Goの開発体験を維持しながら、カスタムネイティブコードの実行を可能にします。特にチーム開発においては、開発環境の統一が容易なexpo-dev-clientの導入を検討する価値があります。
基本的な考え方として、できるだけExpo Goで開発を続け、カスタムネイティブコードが必要になった時点でexpo-dev-clientの導入を検討します。ネイティブコードの自由度がより必要な場合や、既存のネイティブアプリのコードベースがある場合は、ローカルビルドを選択するのが適切でしょう。
expo-dev-clientを導入する
では実際にexpo-dev-clientを導入してみましょう:
# パッケージをインストール
$ npx expo install expo-dev-client
# 開発用バイナリをビルド
$ eas build --profile development
# iOSシミュレータ用のビルド
$ eas build --profile development --platform ios
expo-dev-clientを導入しても、開発時の体験はほとんど変わりません。ホットリロードは引き続き使えますし、エラー表示も見やすく、デバッグツールも充実しています。大きな違いは、カスタムネイティブコードが実行できるようになることと、開発用のバイナリを別途ビルドする必要が出てくることです。
EAS Buildでビルドを管理する
ビルドプロファイルの設定
開発やリリースのために、異なる設定のビルドを作り分けることができます。eas.jsonでビルドプロファイルを設定します:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true,
"resourceClass": "m1-medium"
},
"android": {
"buildType": "apk"
}
},
"preview": {
"distribution": "internal",
"ios": {
"resourceClass": "m1-medium",
"simulator": false
},
"android": {
"buildType": "apk"
}
},
"production": {
"ios": {
"resourceClass": "m1-medium",
"distribution": "store"
},
"android": {
"buildType": "app-bundle"
}
}
}
}
ビルドの実行
# 開発用ビルド(iOSシミュレータ用)
$ eas build --profile development --platform ios
# プレビュービルド(テスト用)
$ eas build --profile preview --platform all
# 本番用ビルド
$ eas build --profile production --platform all
ビルドが始まると、進行状況はWebコンソールで確認できます。エラーが発生した場合も、ログを見ながら原因を特定することができます。
環境変数の管理
開発環境と本番環境で異なる設定を使用する場合は、環境変数を活用します:
// app.config.js
export default {
expo: {
name: process.env.APP_VARIANT === "production"
? "MyApp"
: "MyApp (Dev)",
extra: {
apiKey: process.env.API_KEY,
environment: process.env.APP_VARIANT,
}
},
};
機密情報はEAS Secretsで管理することで、セキュアな運用が可能です:
# プロジェクト全体で使う環境変数を設定
$ eas secret:create
ビルドを効率化するためのポイント
ビルド時間を短縮するために、キャッシュの活用を推奨します:
{
"build": {
"development": {
"cache": {
"key": "developer-v1",
"paths": [
"~/.npm",
"~/.yarn",
"~/Library/Caches/CocoaPods",
"~/.gradle/caches"
]
}
}
}
}
まとめ
Expo Buildとexpo-dev-clientを使うことで、カスタムネイティブコードを含むアプリの開発からリリースまでをスムーズに行うことができます。特に複数の開発者が関わるプロジェクトでは、EASを使うことで環境差異による問題を防ぎ、効率的な開発フローを構築できます。
次回は、EAS Submitを使ったストアへのリリース方法について解説します。
Discussion
React Native をこれから触ろうと意気込んでいる React エンジニアです。とても有益なアドベントカレンダーをありがとうございます。毎日更新楽しみにしております。
ネイティブアプリ開発自体が初めてなのですが、例えば日記アプリなどのユーザーの「データ」を永続化する方法について、個人開発におけるベストプラクティスを記事化いただけると大変うれしいです。
開発者側が低コストでできようにするため、(Firebase等の)従量課金制のストレージを使用するのではなく、ユーザーの デバイスのローカルストレージを極力使用するような設計についてや、そもそもネイティブアプリにおけるデータの永続化におけるベストプラクティスを知れると嬉しいです。
ありがとうございます!励みになります!
そんなに長くならないはずなのでここに書いてしまいますが、アプリから使えるローカルのデータストアにはいくつか種類がありますが、React Nativeでよく使うのはAsyncStorageやSQLite、Realmあたりかなと思います。
日記アプリであればどれを選んでも作ることができますが、一長一短あって、たとえば一番手軽なAsyncStorageは単純なキーバリューストアなので検索などができません。
一般的な日記アプリであれば、SQLiteを使って十分実装可能です。まだアーリーアクセスの段階ですが、サーバーサイドではよく使われているPrismaというライブラリがReact Native用のラッパーを最近出したのでそれを使ってみるのも面白いと思います。
Realmに関しては、少し前までクラウドと自動同期する機能を提供していたので、初めはローカルのみで機能するアプリとして作って、後からオンライン同期機能を追加するようなことができたんですが、クラウドサービスを撤退してしまうようなので現時点ではあまりおすすめできないかなという感じです。(もちろんローカルストレージのみとして使うことは今後も問題なくできます。)
ベストプラクティスというほどのことかはわかりませんが、実装上の注意点として、ストレージのアクセスにかかるコストに気をつけて設計するといいと思います。
いくらローカルにあるとはいえ、ストレージへのアクセスは~数m秒かかってしまうので、レンダリングのたびにアクセスしてしまってはアプリの動作が重たくなってしまいます。
REST APIにアクセスするときと同じくらいの気持ちで、GETリクエストの代わりにローカルストレージからデータを読み出して、POSTリクエストの代わりにデータを書き込むような実装にすると書きやすいです。
このたびはとても丁寧な解説をいただき、ありがとうございます。いくつか選択肢を提示いただいて、それぞれの一長一短など具体的な使用例について参考になりました。
また、ストレージアクセスのコストや設計上の注意点に触れていただいたことで、ローカルストレージを扱う際の具体的なアプローチをイメージしやすくなりました。さらに、新しいツールやライブラリ(PrismaのReact Nativeラッパーなど)について教えていただいた点もとても興味深く、今後の実装に取り入れてみたいと思います。
大変貴重な情報を共有していただき、本当にありがとうございます。引き続きアドベントカレンダーの更新を楽しみにしております!