Expoで環境別にアプリを共存させる——ログインリセットからの解放

Expoでアプリ開発中、ローカル・ステージング・本番のように環境を分けるケースはよくあると思います。ただ、全環境を分けずに1つのアプリにした場合、 切り替えるたびにログインがリセット されてログインし直すのがめちゃくちゃ手間でした。

というわけで、 環境毎に別アプリとしてビルドして同じ端末内に共存 できるようにしました。このやり方を解説します。
ビルドだけでなく、Expo GoとDevelopment Clientでも検証しました。罠もあったのでそれも書いてます。

サンプルリポジトリ:eetann/expo-variant-example

## バージョン情報

  • Expo: 54.0.33
  • eas-cli: 16.28.0

今回はiOSでのみ試しました。

## ディレクトリ構成

先に関係あるファイルだけ列挙しておきます。

ディレクトリ構成
.
|-- app.config.ts
|-- build
| |-- build-1771035791620-local.ipa
| `-- build-1771036011464-staging.ipa
|-- eas.json
`-- package.json

## 切り替えの設定方法

今回はlocalstagingproductionの3環境の例です。環境変数APP_VARIANTで切り替えます。

### app.config.tsの設定

app.config.tsにて、bundleIdentifier・アプリ名・スキーマを環境毎に切り替えます。

bundleIdentifier で同一アプリなのか認識しています。これを切り替えれば別アプリとして認識されます。
アプリ名 は分かりやすさのためです。
スキーマ も設定してあげないとdevelopmentClientを使うときに正しい環境に切り替えられません(後述)。

app.config.ts
import type { ConfigContext, ExpoConfig } from "expo/config";
const IS_LOCAL = process.env.APP_VARIANT === "local";
const IS_STAGING = process.env.APP_VARIANT === "staging";
function getAppName(): string {
if (IS_LOCAL) return "Variant Example (Local)";
if (IS_STAGING) return "Variant Example (Staging)";
return "Variant Example";
}
function getBundleIdentifier(): string {
if (IS_LOCAL) return "com.example.expovariantexample.local";
if (IS_STAGING) return "com.example.expovariantexample.staging";
return "com.example.expovariantexample";
}
function getScheme(): string {
if (IS_LOCAL) return "expovariantexample-local";
if (IS_STAGING) return "expovariantexample-staging";
return "expovariantexample";
}
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
// 関係ない所は省略
name: getAppName(),
slug: "expo-variant-example",
scheme: getScheme(),
ios: {
bundleIdentifier: getBundleIdentifier(),
},
android: {
package: getBundleIdentifier(),
},
extra: {
APP_VARIANT: process.env.APP_VARIANT ?? "production",
},
});

### eas.jsonの設定

eas.jsonに環境変数を設定します。

eas.json
{
"build": {
"local": {
"developmentClient": true,
"distribution": "internal",
"env": {
"APP_VARIANT": "local"
}
},
"staging": {
"distribution": "internal",
"env": {
"APP_VARIANT": "staging"
}
},
"production": {
"env": {
"APP_VARIANT": "production"
}
}
}
}

## ビルド・起動方法

ビルドや起動方法によって指定方法が違うので分けて解説します。

### ビルド

まずビルド。profileの指定でOKです。

Terminal window
eas build --platform ios --profile local
eas build --platform ios --profile staging
eas build --platform ios --profile production

### Expo Go

Expo Goならexpo startするときに切り替え用の環境変数を書けばOKです。

package.json
{
"scripts": {
"start:go:local": "APP_VARIANT=local expo start --go",
"start:go:staging": "APP_VARIANT=staging expo start --go"
},
}

### developmentClient

ネイティブ機能を使うアプリの場合、Expo Goは使えないので必然的にdevelopmentClientを使うと思います。

developmentClientを使うならスキーマの指定が必須です。スキーマ未指定だとAPP_VARIANTを指定しても、expo startのQRを読み込んだときに別の環境のアプリが開かれてしまうことがあります(1時間溶かした)。

package.json
{
"scripts": {
"start:dev-client:local": "APP_VARIANT=local expo start --dev-client --scheme expovariantexample-local",
"start:dev-client:staging": "APP_VARIANT=staging expo start --dev-client --scheme expovariantexample-staging"
},
}

もちろんdevelopmentClientは事前に1回ビルドは必要です。ネイティブ機能のライブラリに変更があったら再ビルドをお忘れなく。

参考:QR code pointing to Prod instead of Dev build · Issue #20824 · expo/expo

## ビルド成果物のIPAファイルを管理するスクリプト集

筆者は、「ビルド時に作成されたipaファイル」に環境名をくっつけて分かりやすくしてます。

mkdir -p build
eas build --platform ios --profile local --local
LATEST_IPA=$(ls -t ./*.ipa 2>/dev/null | head -n 1)
BASENAME=$(basename "$LATEST_IPA" .ipa)
mv "$LATEST_IPA" "./build/${BASENAME}-local.ipa"

ipaファイルの端末へのインストールはfzfでやってます。

# xcrun devicectl list devices
# ↑でインストール先のDEVICE_IDを調べておく
ls -lt build/*.ipa \
| awk '{print $6, $7, $8, $9}' \
| fzf --header="Select IPA file to install" --no-select-1 \
| awk '{print $4}' \
| xargs -I {} \
xcrun devicectl device install app \
--device $DEVICE_ID {}

毎回こんなコマンドを手打ちするのは面倒なのでタスクランナーで実行してます。
筆者が使っているのはmiseです。サンプルリポジトリには実際のmise.tomlの例も置いてます。

## リンク集

筆者はよく間違えるんですが、公式ドキュメントはdocs.expo.devの方です。expo.devだと管理画面の方に飛んじゃいます。

公式ドキュメント:Expo Documentation
サンプルリポジトリ:eetann/expo-variant-example


以上、Expoでの環境別ビルド・起動方法でした。