viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方はこちらまで。
🔰はじめに
弊社で運営しているReactNative製のアプリが年々運用・保守ともに厳しくなってきたので、Expoにリプレイスする形で移行してみました!その際、色々と考慮することが多かったので備忘として残したいと思います。
💻移行手順
1️⃣ Expoプロジェクトを新規作成して既存からソースコードを移植する
既存のプロジェクトにExpoを導入するのは結構きついので、まずは新規にExpoプロジェクトを作成しました。
npx create-expo-app@latest
まずはpackge.jsonの中身を、既存のReactNativeプロジェクトで使用していたパッケージを踏襲するように修正します。
この際、バージョンが古いパッケージに関しては、基本的に最新バージョンとなるように調整します。
次に、既存のReactNativeプロジェクトで使用しているファイル全てを、新生Expoプロジェクトの方にコピーします。
基本的にExpoも内部はReactNativeですので、直接コピーしても大きな破綻は生じないかと思います。
この際、ディレクトリ構成がバラバラになっている場合は、screensフォルダやcomponentsフォルダを作成して、それぞれ分けるのが後々見やすくなるかと思います。
自分が対応した際は、以下のようなディレクトリ構成になっておりました。
📌ディレクトリ構成例
Expo
┝ app // 後に説明するルーティング周り
┝ assets // 画像・アイコンなど
┝ components // コンポーネントファイル関連
┝ constants // 定数関連
┝ db // ローカルDB処理関連
┝ hooks // カスタムhooks関連
┝ pages // 全画面共通のレイアウト関連
┝ recoil // State関連
┝ screens // Screenコンポーネント関連
┝ service // サービス関連
┝ types // type関連
└ utils // 共通関数関連
2️⃣ Expoの設定ファイルを整える
Expoの設定ファイルであるapp.jsを修正する必要があるのですが、後々環境変数などを埋め込む必要があるため、app.config.js
にリネームして整えていきます。
アプリ固有のIDなどを入れる項目がありますので、既存のプロジェクトを参考にしつつ入力していきます。
📌app.config.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を入れる
},
},
},
};
そして、以下のコマンドでexpo-doctor
を実行して、全体の構成が問題ないかチェックします!
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にリダイレクトするようにして画面の出し分けを行っております。
// 未ログインの時は認証画面へ
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
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
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
import HomeScreen from '@/screens/home/HomeScreen';
export default function HomePage() {
return <HomeScreen />;
}
また別画面への遷移はuseRouter
を使用し、パラメーターの取得はuseLocalSearchParams
を使用することで実現できるので書き換えていきました。
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個までという計算らしいです)
- まず、公式サイトにアクセスしてアカウントを発行します
- EASのcliツールをインストールします
npm install -g eas-cli
- 作成したアカウントで、EASにログインします
eas login
- EASのプロジェクトを作成します
eas init
プロジェクト名が聞かれると思うので、入力します。
プロジェクト作成後、EASのサイトを確認すると新規にプロジェクトが作成されていると思うので、画面上に表示されているProjectIDを2️⃣で作成したapp.config.js
に追記しておきます。
app.config.js
extra: {
router: {
origin: false,
},
eas: {
- projectId: '',
+ projectId: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx', <-- 作成したProjectのIDを入れる
},
}
- easの設定ファイルを作成します
eas build:configure
eas.jsonというファイルが作成されると思います。このファイル内で各環境のビルド設定を記載してきます。
開発中の動作確認方法として、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にアップしたい時
📌eas.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"
}
},
}
}
🛠️development buildに関して
基本的にパッケージの追加や、Expoの設定ファイルを修正したら都度development buildを実行して新しくバイナリを生成する必要があります。
まずは、expo-dev-clientというパッケージを導入します。
npx expo install expo-dev-client
その後、easのcliツールを使用して以下のコマンドでビルドすることが可能です。
eas build --profile development
ビルドが終わると、EASにインストール用のQAコードが生成されるので、動作確認したい実機端末で読み取ってインストールすることが可能です。
インストールしたアプリを立ち上げた状態で、Expoを起動するとdevelopmentモードでデバッグすることができます。
expo start --dev-client
📝 tunnelモードに関して
ちなみに、expo start時にtunnelモードを指定すると、異なるネットワーク間でも動作確認ができるようになります!(社外の端末とかで確認したい時にめっちゃ便利)
手順としては、--tunnel
オプションをつけて起動すると、
expo start --dev-client --tunnel
以下のように、コンソールにexp+
で始まるURLが発行されるので、このURLを上記でインストールしたアプリのEnter URL manually
のフォームに入力すると動作確認することができます。
› Metro waiting on
exp+testApp://expo-development-client/?url=testApp-8081.exp.direct
🛠️preview buildに関して
preview buildを利用することで、内部テスターなどにアップせずに実機でアプリの動作確認を行うことができます。
修正版のバイナリをプロジェクトメンバーなどに配布して確認してもらいたい時などに便利です。
まずは実機でテストする環境を整えるために、デバイスの登録が必要になります。
eas device:create
上記のコマンドを実行すると、デバイス登録用リンクのQRコードが発行されるので、リンクの手順に従って確認したいデバイスにprofile installしていきます。
その後、以下のコマンドでpreview用にビルドすると、developmentと同様、EAS側にQAコードが発行されますので、そこから実機端末にインストールすることができます。
eas build --profile preview
🛠️production buildに関して
各ストアにアップロードするためには、証明書やkeystoreの設定を行う必要がありますので、まずはそこから対応していきます。
🍎iOSの場合
以下のコマンド経由で、自動的にEAS側にcredentialの情報を登録することができます。
eas credentials
色々と聞かれるので、必要な項目に応じてポチって対応していきます。
完了後、EASのcredential画面でiOSの項目に追加した情報が表示されているかと思います。
🤖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:
の項目を選択する必要があります。
あとは必要に応じてポチっていけば、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としてアップします。
アップした後、app.config.js
の設定に追加した環境変数を入れていきます。(今回はreact-native-firebaseを継続して使用しております)
app.config.js
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では、フロントエンドエンジニアを募集しています。
また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。