こんにちは、投稿開発部の @morishin127 です。React Native 新アプリシリーズ連載2日目ということで、この記事では React Native アプリの開発基盤の構築について書こうと思います。「クックパッド MYキッチン」というアプリは React Native 製で、iOS/Android 両プラットフォームでリリースされています。元々は一人の手で JavaScript (ES2017+) によって書かれていたアプリケーションでしたが、リリースまでの間に開発メンバーも増え、TypeScript の導入や CI の整備、また高速な検証のためにログ収集の仕組み作りや CodePush の導入などを行いました。それぞれ具体的にどのようなことをしたかを説明します。
セットアップスクリプト
npm-scripts を用いて npm run ios:setup
/ npm run android:setup
でそれぞれのプラットフォームでアプリケーションをビルドするための依存関係をインストールできるようにしています。npm-scripts は package.json に定義していて、それぞれの定義は次のようになっています。
{ "scripts": { "ios:setup": "cd ios && bundle install && bundle exec fastlane setup", "android:setup": "cd android && bundle install && bundle exec fastlane setup", ⋮ }, ⋮ }
ネイティブアプリのセットアップやバイナリ生成の処理には Fastlane を用いており、ここで行うセットアップの処理も ios/fastlane/Fastfile
/ android/fastlane/Fastfile
に次のように定義しています。Fastlane は Ruby 製のタスクランナーで、ビルドやストアへのサブミット、スクリーンショットの撮影など様々なタスクを定義して自動化するためのツールです。
▼ios/fastlane/Fastfile
react_native_root = File.absolute_path('../../') desc "Install dependencies" lane :setup do Dir.chdir react_native_root do sh "yarn install && yarn run build" end cocoapods(use_bundle_exec: true, try_repo_update_on_error: true) end
▼android/fastlane/Fastfile
react_native_root = File.absolute_path('../../') desc "Install dependencies" lane :setup do Dir.chdir react_native_root do sh "yarn install && yarn run build" end end
また次のような npm-scripts でシミュレータでの実行スクリプトを定義しておくと、開発者はリポジトリをクローンしてから yarn run ios:setup
と yarn run start
と yarn run ios:run:debug
を実行するだけで iOS アプリをシミュレータ上で実行することができて便利です。(Android も同様)
{ "scripts": { "ios:run:debug": "node node_modules/react-native/local-cli/cli.js run-ios --simulator 'iPhone SE'", "android:run:debug": "cd android && ./gradlew installStagingDebug", ⋮ }, ⋮ }
TypeScript 導入
開発当初は JavaScript のみでしたが途中で TypeScript を導入しました。元々の JavaScript コードと混在することになるため tsconfig.json
では "allowJs": true
を指定しています。React Native 周りのライブラリは型定義が充実していたため、TypeScript の恩恵を受けながら開発することができました。既存の React Native プロジェクトに TypeScript を導入した手順は morishin/ReactNativePractice の README.md にまとめたのでそちらをご参照ください。基本的には公式の Microsoft/TypeScript-React-Native-Starter に倣った手順を踏んでいます。
参考までに、「クックパッド MYキッチン」アプリの tsconfig.json
は執筆時点ではこのようになっています。ソースコードは src
ディレクトリに、tsc
によりトランスパイルされたコードが lib
ディレクトリに配置され、アプリは lib
以下のソースを読み込みます。
{ "compilerOptions": { "target": "es2017", "module": "es2015", "allowJs": true, "jsx": "react-native", "sourceMap": true, "outDir": "./lib", "strict": true, "skipLibCheck": true, "moduleResolution": "node" }, "include": ["./src/"] }
tsc
によるトランスパイルを自動化するために npm-scripts に build:watch
を定義し、開発中はこれを実行した状態でソースコードを編集しています。
{ "scripts": { "build": "tsc", "build:watch": "tsc --watch", ⋮ }, ⋮ }
フォーマッタ導入
複数人で開発する際にはフォーマッタがあると便利なので、Prettier を利用しています。フォーマットルールはほぼデフォルトのままです。エディタのプラグイン等で保存時に自動フォーマットがかかるようにしておくと便利かもしれません。
CodePush 導入
「クックパッド MYキッチン」ではサービスの高速な検証のために、バンドルの配信に Microsoft 製の CodePush という仕組みを利用しています。CodePush を利用すると React Native アプリの JS バンドルのみをユーザーの端末に配信することができ、App Store / Google Play Store でアップデートを配信することなくアプリを更新することができます。この特徴はとにかく高速に仮説を検証したいサービス開発者にとって魅力的で、React Native を採用する大きな理由のひとつになると思っています。
導入手順
CodePush の導入手順については公式の通りなので割愛します。
運用
CodePush は一つのプロジェクトに対して複数のデプロイ環境を作ることができ、「クックパッド MYキッチン」では Production
, Production-test
, Staging
の3つの環境を用意しています。Production
は App Store / Google Play Store で配信されている本番のアプリが JS バンドルを取得する環境、Production-test
は社内のみで配信しているアプリが参照している環境、Staging
はチーム内でデザインや動作を確認をするためのアプリが参照している環境です。Production-test
アプリと Staging
アプリの違いとして、前者は API サーバーや DB も本番環境を参照しているに対し、後者はバックエンドが本番環境とは切り離されているため、コンテンツの投稿・公開といった動作テストを行うことができるアプリになっています。それぞれの社内配信の方法とデプロイのタイミングについては CI の項で説明します。
画像の Beta 帯アイコンのものが Production-test
環境のアプリ、Staging 帯アイコンのものが Staging
環境のアプリ、無印が Production
環境のストア版アプリです。
ちなみにこのアイコンの帯は fastlane-plugin-badge を用いて社内配信の CI ジョブ内で付加しています。
iOS/Android アプリから CodePush のデプロイ環境を読み分ける
Production
, Production-test
, Staging
アプリで参照する CodePush の環境を分けていると述べましたが、iOS アプリでは Build Configuration 毎に CodePush の deployment key を切り替えることで、またAndroid アプリでは Build Type / Product Flavor 毎に deployment key を切り替えることでデプロイ環境を読み分けています。React Native アプリの Xcode プロジェクトに Build Configuration を追加するとビルドが通らなくなって苦戦したのでその解決の記録を貼っておきます。同じ問題に遭われた方の参考になれば幸いです。
CI 環境構築
CI マシンではプルリクエストを出したときに実行されるジョブと、プルリクエストを master にマージしたときに実行するジョブがあります。前者のジョブはアプリケーションのテストコードを実行しています。後者の master にマージしたときに実行されるジョブでは iOS/Android アプリのバイナリ生成と社内配信、CodePush の Production-test
環境への JS バンドルのデプロイを行っています。CodePush の Staging
環境へのデプロイは開発者が手元でビルドしたバンドルを手動でデプロイします。そうすることで master にマージする前のプルリクエスト段階のコードもチームメンバーの Staging
アプリに配信することができ、デザイナとのコミュニケーションに有用です。実際にユーザーさんが触れる CodePush の Production
環境へのデプロイは Production-test
環境にデプロイされたバンドルを Production
環境へコピーする(promote
)という形で行われます。promote
の実行はチャットボットを介して Rundeck 上のデプロイジョブを実行することで行っています。アプリバイナリのデプロイと CodePush のデプロイの構造をそれぞれ図1, 図2にしました。
▼図1: アプリバイナリのデプロイ
▼図2: CodePush のデプロイ
ログ収集
クックパッドには複数のモバイルアプリで共通して利用しているログ収集基盤があり、「クックパッド MYキッチン」のユーザーのイベントログ等の情報もそこへ送っています。ログ収集基盤というのはクックパッドのデータ活用基盤 - クックパッド開発者ブログで触れられている基盤のことで、モバイルアプリは Logend と呼ばれる社内のログ送信用エンドポイントへログを送り、Logend は fluentd を介して Amazon S3 にログを蓄積しています。S3 に蓄積されたログは社内のデータウェアハウスにロードされ、開発者はそこで分析を行います。データ活用の基盤に関して詳しくは上述の記事をご覧ください。
アプリからログを送信するに当たってバッファリングやリトライの機構が必要になりますが、これまでのクックパッドのモバイルアプリでは Puree というライブラリがその機構を担っていました。Puree には iOS/Android 両方のライブラリがありましたが、React Native から利用できる JavaScript 版は存在しなかったため、「クックパッド MYキッチン」の開発に際して作られました。
Puree に関して詳しくは過去の記事をご覧ください。
- モバイルアプリのログ収集ライブラリ「Puree」をリリースしました - クックパッド開発者ブログ
- 良い感じにログを収集するライブラリ、Puree-Swiftをリリースしました - クックパッド開発者ブログ
おわりに
いかがでしたでしょうか。これから React Native でやっていこうとしているやっていき手の皆さんの参考になれば幸いです。明日は@101kazさんから React Native プロジェクトの Android 環境整備のお話です、お楽しみに!
クックパッドでは毎日の料理を楽しみにするために、より良い技術を選択し、より速くユーザーさんに価値を届けられるサービス開発エンジニアを募集しています。興味を持っていただけましたら是非気軽にご連絡ください。話をしてみたいけど応募はちょっとという方は@morishin127にDMしていただいても大丈夫です🙆
👋