ReactNative製のアプリをExpoへ移行した時のメモ

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

# 🔰はじめに
弊社で運営しているReactNative製のアプリが年々運用・保守ともに厳しくなってきたので、Expoにリプレイスする形で移行してみました！その際、色々と考慮することが多かったので備忘として残したいと思います。

# 💻移行手順
## 1️⃣ Expoプロジェクトを新規作成して既存からソースコードを移植する
既存のプロジェクトにExpoを導入するのは結構きついので、まずは新規にExpoプロジェクトを作成しました。

```
npx create-expo-app@latest
```
まずはpackge.jsonの中身を、既存のReactNativeプロジェクトで使用していたパッケージを踏襲するように修正します。
この際、バージョンが古いパッケージに関しては、基本的に最新バージョンとなるように調整します。

次に、既存のReactNativeプロジェクトで使用しているファイル全てを、新生Expoプロジェクトの方にコピーします。
基本的にExpoも内部はReactNativeですので、直接コピーしても大きな破綻は生じないかと思います。
この際、ディレクトリ構成がバラバラになっている場合は、screensフォルダやcomponentsフォルダを作成して、それぞれ分けるのが後々見やすくなるかと思います。
自分が対応した際は、以下のようなディレクトリ構成になっておりました。
<details><summary>📌ディレクトリ構成例</summary>

```
Expo
 ┝ app // 後に説明するルーティング周り
 ┝ assets // 画像・アイコンなど
 ┝ components // コンポーネントファイル関連
 ┝ constants // 定数関連
 ┝ db // ローカルDB処理関連
 ┝ hooks // カスタムhooks関連
 ┝ pages // 全画面共通のレイアウト関連
 ┝ recoil // State関連
 ┝ screens // Screenコンポーネント関連
 ┝ service // サービス関連
 ┝ types // type関連
 └ utils // 共通関数関連
 
```

</details>

## 2️⃣ Expoの設定ファイルを整える
Expoの設定ファイルであるapp.jsを修正する必要があるのですが、後々環境変数などを埋め込む必要があるため、`app.config.js`にリネームして整えていきます。
アプリ固有のIDなどを入れる項目がありますので、既存のプロジェクトを参考にしつつ入力していきます。
<details><summary>📌app.config.js例</summary>

```js
export default {
  expo: {
    jsEngine: 'hermes',
    owner: 'viviON', // 所有している個人や会社名
    name: 'testApp', // アプリの名称
    slug: 'testApp', // 基本的にアプリの名称とイコールでOK
    version: '1.0.0',
    orientation: 'portrait',
    icon: './assets/images/icon.png',
    scheme: 'testApp', // アプリの名称
    userInterfaceStyle: 'automatic',
    androidStatusBar: {
      backgroundColor: '#000000',
    },
    backgroundColor: '#000000',
    ios: {
      ascAppId: 'xxxxxxxx', // 既存アプリのAppIDを入れる
      supportsTablet: true,
      bundleIdentifier: 'xxxxxxxx', // 既存アプリのBundleIdentifierを入れる
      associatedDomains: [], // Deeplinkなどを使用する場合はここにリンクを追加していく
      supportsTablet: true,
      infoPlist: {
        UIBackgroundModes: ['audio'], // バックグラウンド再生などは必要に応じて
      },
    },
    android: {
      softwareKeyboardLayoutMode: 'pan',
      package: 'xxxxxxxx', // 既存アプリのandroid package名を入れる
      intentFilters: [], // Deeplinkなどを使用する場合はここにリンクを追加していく
    },
    // 使用するパッケージに合わせて調整していく
    plugins: [
      'expo-router',
      [
        'expo-build-properties',
        {
          ios: {
            useFrameworks: 'static',
          },
        },
      ],
      [
        'expo-tracking-transparency', // トラックキング許可機能を利用するため
        {
          userTrackingPermission:
            'This identifier will be used to deliver personalized ads to you.',
        },
      ],
    ],
    experiments: {
      typedRoutes: true,
    },
    extra: {
      router: {
        origin: false,
      },
      eas: {
        projectId: '', // のちに作成するEASのprojectIdを入れる
      },
    },
  },
};

```
</details>


そして、以下のコマンドで`expo-doctor`を実行して、全体の構成が問題ないかチェックします！
```shell
npx expo-doctor
```
ここでfailedが出た場合は、メッセージを頼りにパッケージやconfigの中身を修正していく形になります。

## 3️⃣ ルーティング処理をexpo-routerに移行する
Expoでは`expo-router`というファイルベースルーティングシステムが採用されております。
既存のReactNativeプロジェクトは、`react-navigation`ベースで構築しておりましたので、`expo-router`ベースとなるように修正を行っていきます。
基本的にはNext.jsなどのルーティングシステムを踏襲する形になっているので、それほど迷いはしないかと思います。
`app`というディレクトリが既に存在しているので、この中に各ページのルーティングを構築していきます。
今回対応したアプリはログイン後の通常の画面と、ログイン前でも利用できるお試し画面が存在していたので、前者のログイン後のページ群を`(tabs)`というRoute Groupsに、後者のお試し画面を`（guest）`というRoute Groupsに分割する形で対応しております。
参考ではありますが、以下のようなディレクトリ構成で構築しておりました。

```
Expo
 ┝ app
 │  ┝ (guest)
 │  ┝ (tabs)
 │  └ auth
 ┝ _layout.tsx
 ┝ +html.tsx
 ┝ +not-found.tsx
 └ index.tsx
```
`auth`ディレクトリは認証用のtokenの取得などを行っておりますが、この辺はアプリの構成によって変わってきそうです。
以下のように、ログイン後の認証済みであれば(tabs)にリダイレクト、認証前であればauthにリダイレクトするようにして画面の出し分けを行っております。
```react
// 未ログインの時は認証画面へ
if (!isAuthenticated) {
  return <Redirect href={'/auth'} />;
}

// ログイン済みの時はホーム画面へ
return <Redirect href={'/(tabs)/home'} />;
```

そして、`(tabs)`ディレクトリの中は以下のようになっており、各タブ画面のディレクトリと`_layout.tsx`ファイルを配置してあります。
```
(tabs)
 ┝ home
 │  ┝ work
 │  ┝ grid
 │  ┝ list
 │  ┝ _layout.tsx
 │  └ index.tsx
 ┝ library
 │  ┝ work
 │  ┝ search
 │  ┝ _layout.tsx
 │  └ index.tsx
 ┝ playlist
 │  ┝ playlistDetail
 │  ┝ _layout.tsx
 │  └ index.tsx
 ┝ setting
 │  ┝ language
 │  ┝ ignore
 │  ┝ _layout.tsx
 │  └ index.tsx
 └ _layout.tsx
```
各階層にある`_layout.tsx`ファイルは基本的には直下のNavigation構成を配置していきます。
例えば、`(tabs)`のディレクトリは画面を横に並べるTab Navigationを実現したいので、`expo-router`の`Tabs`でTabレイアウトを構築しております。

**/(tabs)/_layout.tsx**
```react
export default function () {
  return (
    <Tabs>
      <Tabs.Screen
        name="home"
      />
      <Tabs.Screen
        name="library"
      />
      <Tabs.Screen
        name="playlist"
      />
      <Tabs.Screen
        name="setting"
      />
    </Tabs>
  );
}
```

また、`home`のディレクトリは画面を上に重ねるStack Navigationを実現したいので、Stackレイアウトを構築しております。

**/(tabs)/home/_layout.tsx**
```react
export default function HomeLayout() {
  return (
    <Stack
      screenOptions={{
        headerShown: false,
      }}>
      <Stack.Screen name="index" />
      <Stack.Screen name="work/index" />
      <Stack.Screen name="list/index" />
      <Stack.Screen name="grid/index" />
    </Stack>
  );
}
```
各画面の`index.tsx`は、既存のScreenファイルを再利用する形で対応しました。
**/(tabs)/home/index.tsx**

```react
import HomeScreen from '@/screens/home/HomeScreen';

export default function HomePage() {
  return <HomeScreen />;
}
```

また別画面への遷移は`useRouter`を使用し、パラメーターの取得は`useLocalSearchParams`を使用することで実現できるので書き換えていきました。
```react
import { useLocalSearchParams, useRouter } from 'expo-router';
const router = useRouter();
const params = useLocalSearchParams();

// router.pushで/home/workの画面に遷移
router.push({
  pathname: '/home/work',
  params: {
    workno,
  },
});

// worknoパラメーターを元にworkを取得
const work = getWork(params?.workno);
```

## 4️⃣ ビルド環境を整える（EASの導入）
Expoプロジェクトのビルドや配信を自動化させるために、EASというサービスを導入していきます。
無料枠で一月当たりiOS15回、Android15回までビルド可能です。（実際はビルド回数ではなくて、残存しているビルドがそれぞれ15個までという計算らしいです）
- まず、[公式サイト](https://expo.dev/eas)にアクセスしてアカウントを発行します
- EASのcliツールをインストールします
```shell
npm install -g eas-cli
```
- 作成したアカウントで、EASにログインします

```shell
eas login
```
- EASのプロジェクトを作成します
```shell
eas init
```
プロジェクト名が聞かれると思うので、入力します。
プロジェクト作成後、EASのサイトを確認すると新規にプロジェクトが作成されていると思うので、画面上に表示されているProjectIDを2️⃣で作成した`app.config.js`に追記しておきます。
![スクリーンショット 2024-12-17 15.05.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/9dc65197-b67a-6746-5eb3-c183ffa58ca7.png)

**app.config.js**
```diff
extra: {
  router: {
    origin: false,
  },
  eas: {
-   projectId: '',
+   projectId: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx', <-- 作成したProjectのIDを入れる
  },
}
```
- easの設定ファイルを作成します
```shell
eas build:configure
```
eas.jsonというファイルが作成されると思います。このファイル内で各環境のビルド設定を記載してきます。

:::note info
開発中の動作確認方法として、Expoにはメインで2種類の確認方法があります。

Expo Go -> ビルド不要で手元の実機でExpoのプロジェクトを動作確認することができる機能。ただし、ExpoSDK以外のパッケージは動作対象外となっている。
EAS development build -> EAS上でbuildし、一度手元の実機にバイナリファイルをインストールすることで動作確認ができる機能。ExpoSDK以外のパッケージも動かすことができる。

今回は、ExpoSDK以外のパッケージも継続して使用していく予定なので、下のdevelopment buildを採用しました。
:::

ビルドの種類としては、以下の3種類をメインで使っていく形になります。
**development** -> 開発中に動作確認したい時
**preview** -> スマホに完成したアプリを入れて確認したい時(QRコードで配布可能)
**production** -> 本番ビルドしてApple Store Connect, Google Play Consoleにアップしたい時
<details><summary>📌eas.json例</summary>

```json
{
  "cli": {
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": {
        "APP_VARIANT": "development",
        "EXPO_NO_CAPABILITY_SYNC": "1"
      }
    },
    "preview": {
      "distribution": "internal",
      "autoIncrement": true,
      "env": {
        "APP_VARIANT": "preview"
      }
    },
    "production": {
      "autoIncrement": true,
      "env": {
        "APP_VARIANT": "production",
        "EXPO_NO_CAPABILITY_SYNC": "1"
      }
    },
  }
}

```
</details>

### 🛠️development buildに関して
基本的にパッケージの追加や、Expoの設定ファイルを修正したら都度development buildを実行して新しくバイナリを生成する必要があります。
まずは、expo-dev-clientというパッケージを導入します。
```
npx expo install expo-dev-client
```
その後、easのcliツールを使用して以下のコマンドでビルドすることが可能です。
```shell
eas build --profile development
```
ビルドが終わると、EASにインストール用のQAコードが生成されるので、動作確認したい実機端末で読み取ってインストールすることが可能です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/2567e224-ad26-adbf-984a-ef529b6da508.png" width="400" />
インストールしたアプリを立ち上げた状態で、Expoを起動するとdevelopmentモードでデバッグすることができます。
```shell
expo start --dev-client
```
📝 tunnelモードに関して
ちなみに、expo start時にtunnelモードを指定すると、異なるネットワーク間でも動作確認ができるようになります！（社外の端末とかで確認したい時にめっちゃ便利）
手順としては、`--tunnel`オプションをつけて起動すると、
```shell
expo start --dev-client --tunnel
```
以下のように、コンソールに`exp+`で始まるURLが発行されるので、このURLを上記でインストールしたアプリの`Enter URL manually`のフォームに入力すると動作確認することができます。
```shell
› Metro waiting on
exp+testApp://expo-development-client/?url=testApp-8081.exp.direct
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/53e53582-d447-353c-24a9-28958dc58d08.png" width="300" />

### 🛠️preview buildに関して
preview buildを利用することで、内部テスターなどにアップせずに実機でアプリの動作確認を行うことができます。
修正版のバイナリをプロジェクトメンバーなどに配布して確認してもらいたい時などに便利です。

まずは実機でテストする環境を整えるために、デバイスの登録が必要になります。
```shell
eas device:create
```
上記のコマンドを実行すると、デバイス登録用リンクのQRコードが発行されるので、リンクの手順に従って確認したいデバイスにprofile installしていきます。

その後、以下のコマンドでpreview用にビルドすると、developmentと同様、EAS側にQAコードが発行されますので、そこから実機端末にインストールすることができます。
```shell
eas build --profile preview
```
### 🛠️production buildに関して
各ストアにアップロードするためには、証明書やkeystoreの設定を行う必要がありますので、まずはそこから対応していきます。

**🍎iOSの場合**
以下のコマンド経由で、自動的にEAS側にcredentialの情報を登録することができます。
```
eas credentials
```
色々と聞かれるので、必要な項目に応じてポチって対応していきます。
完了後、EASのcredential画面でiOSの項目に追加した情報が表示されているかと思います。
![スクリーンショット 2024-12-18 12.43.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/f7d5ac53-a2e5-6646-6d21-bb34bf07ac3d.png)

**🤖Androidの場合**
Androidの場合、自分のプロジェクトだとiOSの様に自動設定ができなかったので、別途`credentials.json`というファイルを生成してアップロードする形で対応しました。
`credentials.json`自体は機密情報を扱っているため、gitignoreに設定しておく必要があります。
以下のようにプロジェクトに応じて、keystoreの情報を入力していきます。

**credentials.json**
```
{
  "android": {
    "keystore": {
      "keystorePath": "android/testApp.keystore", <-- 配置場所はandroidフォルダでなくても大丈夫です
      "keystorePassword": "xxxxxxx",
      "keyAlias": "xxxxxxx",
      "keyPassword": "xxxxxxx"
    }
  }
}
```
上記jsonを作成した後、`eas credentials`を実行するのですが、途中の`What do you want to do?`の項目で、以下のように`credentials.json:`の項目を選択する必要があります。

![スクリーンショット 2024-12-18 12.50.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/4fea73b1-c849-2694-001c-3df2137b5f59.png)
あとは必要に応じてポチっていけば、iOSと同じくAndroidのcredential情報も登録後できるかと思います。
登録後は以下のコマンドでproduction用のビルドを作成し、各ストアにアップロードするだけです！
```
eas build --profile production
```


## 5️⃣ Firebaseの設定（オプション）
Firebaseの設定もExpoを使用すればサクッと作れます。
Google-Service関連のファイルはAPI Key周りの機密情報が入っており、リポジトリ上で管理したくないので、EASの`Environment variables`で管理するように修正していきます。
環境変数の名前を分かりやすいように付けて、それぞれ`GoogleService-info.plist`と`google-service.json`をSecretとしてアップします。

![スクリーンショット 2024-12-18 10.55.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616804/00233339-487a-80ff-06e1-1d27822c69f5.png)

アップした後、`app.config.js`の設定に追加した環境変数を入れていきます。(今回はreact-native-firebaseを継続して使用しております)
**app.config.js**
```diff
export default {
  expo: {
    ios: {
      ...
+     googleServicesFile: process.env.GOOGLE_SERVICE_IOS,
      ...
    },
    android: {
      ...
+     googleServicesFile: process.env.GOOGLE_SERVICE_ANDROID,
    },
    plugins: [
+     '@react-native-firebase/app',
      ...
    ],
  },
};
```

# 💬最後に
Expo移行は以前から考えていて、なかなか重い腰を上げられないでいたのですが、今回対応してみてそれほど詰まったりすることはなく移行できたので良かったです！
素のReactNativeだとバージョンアップすると、他のパッケージが依存関係のせいで動作しなくなったりと辛みがかなりあったので、これを機に開発体験周りが改善できればと思っています！

# 🌈一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。