1
/
5

React Native をプロダクションで採用した知見を共有します

このたび、スノウロビンでは React Native でのアプリ開発にチャレンジしました。

本記事では入門レベルを超えてプロダクションに採用してわかった知見を紹介します。

どんなアプリをつくったか

医者と患者に分かれて、ビデオ通話とチャットができる、医療系ビデオ通話アプリを開発しました。ビデオ通話とチャットにはTwilioの Programmable VideoProgrammable Chat というサービスを使っています。

以下の4種類のアプリを開発しました。

  • iOS 医者アプリ
  • Android 医者アプリ
  • iOS 患者アプリ
  • Android 患者アプリ

React Native を採用した理由

4種類のアプリでしたが、 iOS / Android ともにデザイン・基本的な挙動は同じということで、効率よく開発するためにクロスプラットフォーム技術の採用を検討しました。

React Native, Xamarin が候補に出ましたが、React Native を採用することになりました。

社内メンバーの技術スタックでは JavaScript のほうが得意だったということ、さらに採用当時国内でもチラホラと React Native 採用事例がでており興味を持っていたことが理由です。

使用技術

TypeScript

案件開始当初はプレーンな JavaScript で書いていました。しかし、アプリの肥大化が想定されたため、型チェック機構の必要性を感じ、TypeScript を採用しました。

TypeScript には本当に助けられており、複数箇所にわたる変更に臆することがなくなりました。また、型によるサポートは乱暴な言い方をすると「より頭を使わずコードを書くことができる」ように感じます。型の調べにのせてスムーズにコードを書けます。

個人的には1ファイルほどの書き捨てスクリプト以外は基本的に TypeScript を採用するようにしています。

MobX

mobxjs/mobx
Simple, scalable state management. Contribute to mobxjs/mobx development by creating an account on GitHub.
https://github.com/mobxjs/mobx

Redux と同じように状態管理を行うライブラリです(Redux と同じ Flux ライブラリではありません)。

使ってみて感じる MobX の特徴は以下です。

  • TypeScript で書かれているため、TypeScript と相性がよい
  • Redux と比べてグッとコード量が減る

今回の案件もスタートこそ Redux だったものの、TypeScript と組み合わせたときの記述量の多さがどうしても気になっていました。Action まわりで Action の型定義、Flux Standard Action における actionType を定義、各 Action を Pipe でつないだ各モジュールごとのアクション。以下の画像のようになります。

上が Redux の Action, Action Creator 定義に比べ MobX では Action, Action Creator, Reducer 相当の定義を以下の量で記述できます。

class YearVersionStore {
  @observable
  availableVersions: YearVersion[] = []
  
  @observable
  selectedVersion?: YearVersion

  @action
  loadYearVersions(availableVersions: YearVersion[], selectedVersion: YearVersion): void {
    this.availableVersions = availableVersions
    this.selectedVersion = selectedVersion
  }
}

react-native-router-flux

React Native アプリ開発におけるライブラリ選定でもっとも重要なのはナビゲーション管理ライブラリではないでしょうか。React Native パッケージには iOS, Android 両方をサポートしたナビゲーション機構が含まれておらず、なんらかのサードパーティライブラリを選定することになります。

本案件では react-native-router-flux を採用しました。

aksonov/react-native-router-flux
The first declarative React Native router. Contribute to aksonov/react-native-router-flux development by creating an account on GitHub.
https://github.com/aksonov/react-native-router-flux

react-native-router-flux の強みは「ナビゲーション構成を JSX で書けること」です。コードの見た目が react-router 採用時と似たようなものになり、とっつきやすいと考えました。

代表的なナビゲーション管理ライブラリに react-navigation があります。react-navigation と比較したときの react-native-router-flux のメリットとして、react-native-router-flux ではひとつの render 内でアプリ中のルーティングをすべて表現することができます。

以下が react-navigation のとき。

//
// コードは https://reactnavigation.org/docs/en/tab-based-navigation.html から抜粋
//

const HomeStack = createStackNavigator({
Home: HomeScreen,
Details: DetailsScreen,
});

const SettingsStack = createStackNavigator({
Settings: SettingsScreen,
Details: DetailsScreen,
});

export default createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack,
},
{
/* Other configuration remains unchanged */
}
);

以下が react-native-router-flux のときです。

render() {
  return (
    <Router>
      <Stack key="root">
        <Stack key="homeStack">
          <Scene key="home" component={HomeScreen} />
          <Scene key="details" component={DetailsScreen} />
        </Stack>
        <Stack key="settingsStack">
          <Scene key="settings" component={SettingsScreen} />
          <Scene key="details" component={DetailsScreen} />
        </Stack>
      </Stack>
    </Router>
  )
}

しかし Qiita や他社テックブログの React Native 記事を読むと React Navigation を採用するのが定番のようです。React Native 公式でも推されています。

実は react-native-router-flux では内部では React Navigation が使われています。そのため、バグが React Navigation に依存していた場合、React Navigation の修正後、react-native-router-flux に取り込んでリリースとなり、対応が遅れてしまいます。

本案件では react-native-router-flux を使いましたが、今後 React Native アプリを作成することがあれば、React Navigation の採用を検討するでしょう。

チーム編成

本案件のモバイルアプリ向け部分の開発者は4人です。内訳は以下。

  • React Native エンジニア 2人
  • モバイルアプリエンジニア 2人

React Native エンジニアは本開発のメインメンバーであり、基本的に JavaScript を書き、ネイティブモジュール導入時、ビルドまわりで Xcode, Android Studio をたまにさわる、という感じです。筆者もその1人です。

前述したように本案件では、医者アプリと患者アプリがあります。React Native エンジニアのうち 1人は医者アプリの開発、1人は患者アプリの開発を行いました。

モバイルアプリエンジニアは iOS / Android 担当を1人づつアサインしました。役割としては React Native と iOS / Android プラットフォームを結ぶブリッジ(ネイティブモジュール)の開発、および技術相談役です。

モバイルアプリエンジニアには医者・患者アプリをまたいでそれぞれ iOS / Android を担当してもらいました。

React Native を採用してよかったこと

それぞれのバージョン開発に専念できた

ここでいうバージョンとは iOS / Android ではありません。医者・患者アプリのことを指します。

医者・患者の2つのアプリをモバイルアプリエンジニアが作るとなると、 iOS エンジニアが iOS 向けに医者・患者アプリを、 Android エンジニアが Android 向けに医者・患者アプリを開発することになります。これは一人の開発者が医者・患者アプリ 両方の仕様について知る必要があり、困難です。

React Native を採用したことで医者・患者アプリ、どちらか片方のアプリの知識だけで開発することができました。

また、React Native の大きなメリットであるプラットフォーム間のコード共通化ですが、今回のアプリは iOS / Android とも基本的に見た目は同じだったため、ほとんどのコードを共通化させることができました。具体的には *.ios.tsx, *.android.tsx というファイルは存在せず、コンポーネントのプロパティで iOS / Android 特有の値を渡したり、パーミッションまわりで条件分岐が発生したくらいでしょうか。

React Native を採用して辛かったこと

バグが多い

React Native およびその周辺ライブラリについては本当にバグが多いです。開発期間中、GitHub の issue を見る機会がとても多くなりました。

例えばバグで iOS で日本語入力を行っても、変換対象とならずに確定されてしまう問題がありました(React Native v0.53.0 で発生していた。記事執筆時点の最新版 v0.57.0 で発生するかは未確認)。わかりやすく説明すると、「あ」と入力したとき、「あ」として確定されてしまい「亜」や「有」などに変換することが不可能となっていました。これは issue に「React Native パッケージ中の objective-c のコードを編集すれば解決する」と書いてあり、従うとたしかに修正されました。しかし React Native パッケージに未マージだったため、React Native をフォークしその修正を取り込んでいます。

記事執筆時点、2018年9月25日時点で React Native の最新バージョンは v0.57.0 である、つまり v1.0.0 を超えていないことからもうかがえるように、なかなか動作が安定しておりません。

ネイティブアプリエンジニアが必要

React Native は「Web フロントエンド技術だけでアプリが作れる」と紹介されることが多いですが、それをプロダクション採用できるかどうかは話が別です。

とくにビルドまわり(Xcode, Android Stuidio および Gradle )は基本的には避けて通ることができないかつ、未経験にはなかなか難しく、何度もモバイルアプリエンジニアの力を借りました。iOS, Android 環境に依存するライブラリを導入するとビルドがコケることが何度かありました。

パーミッションまわりも筆者は当初苦戦しました。Android のランタイムパーミッションや iOS でパーミッション文言を多言語化することについてなどです。

また、どうしても React Native 標準で実現しづらい内容についてはネイティブモジュール(各プラットフォームと JavaScript を結ぶブリッジ)をモバイルアプリエンジニアに作ってもらいました。

以下、今回のアプリで作成されたネイティブモジュールです。

  • EventDispatcherModule
    • CallKit で着信を受け、 React Native アプリにイベントを通知する
  • TokenStorage
    • ネイティブモジュールから API リクエストする必要があり、User のトークンをネイティブモジュール / React Native 両方から参照できるようにストレージモジュールをつくった
  • VideoChatModule
    • Twilio SDK のラッパー

開発 Tips

以下では React Native アプリを開発する際の小ネタを紹介します。小ネタではありますがどれも重要です。

スプラッシュスクリーンがちらつく

React Native アプリにはスプラッシュスクリーンが一瞬だけホワイトアウトするよろしくない挙動があります。スプラッシュスクリーンの直後、一瞬だけチラつくのが確認できますでしょうか。

これがなぜかというと React Native アプリの仕組みに原因がります。React Native アプリの初期ロードは以下のような流れになっています。

  1. スプラッシュスクリーン表示( iOS 機構のロード )
  2. スプラッシュスクリーンが閉じる
  3. JavaScript ( React Native アプリのコード ) のロード
  4. React Native で記述された画面が描画される

ここで 3. のロードのとき、ビューの情報は JavaScript で書いているため何も描画するものがなく、真っ白な画面がチラつくことになります。上の Gif は、React Native プロジェクト作成直後で読み込む JS ファイルが小さいため、ホワイトアウトはほんの一瞬ですが開発が進むにつれその時間は長くなっていきます。

対策は crazycodeboy/react-native-splash-screen を採用することです。内容は以下が詳しいです。

React Native製アプリのクオリティを上げるために工夫した事 - 週休7日で働きたい
InkdropというMarkdownノートアプリを一人で作っているTAKUYAです。最近、React Nativeを使って、iOS版とAndroid版の新しいバージョンをリリースしました。React Nativeは、JavaScriptとReactを使ってクロスプラットフォームなモバイルアプリが開発できるフレームワークです。 どうすればReact...
https://blog.craftz.dog/lessons-learned-from-creating-my-mobile-app-to-build-a-high-quality-react-native-app-dcf021ce37ef#ad3a
How to Add a Splash Screen to a React Native App (iOS and Android)
Updated: February 27, 2018 I'm often asked about that last mile of developing a React Native app (actually getting it into the app store). There's more to it than just building your app and sending it off to Apple/Google - you've got to add icons, splash
https://medium.com/handlebar-labs/how-to-add-a-splash-screen-to-a-react-native-app-ios-and-android-30a3cec835ae

crazycodeboy/react-native-splash-screen を適用する前が以下。

crazycodeboy/react-native-splash-screen を適用したあとはこちらです。

デバッグ中だけ動作するコードがある

シミュレーターで動作確認したコードが実機になった途端、動かないということがありました。例えば String.prototype.padStart です。

というのも、React Native のデバッグモード中は Chrome の V8 エンジンで実行されます。通常モードは JavaScript Core と呼ばれる iOS / Android 内部のランタイムで実行されます。

つまり、デバッグ中の V8 エンジンでは動作して、通常モードのランタイムである JavaScript Core ではサポートされていないコードは、デバッグを切ると動作しなくなってしまうのです。

JavaScript Environment · React Native
JavaScript Runtime
https://facebook.github.io/react-native/docs/javascript-environment

import のパスを絶対パスにする

これは React Native 特有ではなく、 JavaScript / TypeScript について適用できる内容です。

import 時のファイルパスは相対パスで記述されますが、これはリファクタリングに弱いです。 例えばsrc/screens/HomeScreen/HomeScreenHeader.tsxsrc/components/HomeScreenHeader.tsx へと移動させる場合、 HomeScreenHeader の import 部分のパスが変わってしまいます。エイリアスを設定し、絶対パスにすることで import パスを変更する必要がなくなります。(もっとも最近では VSCode や WebStorm のリファクタリングで import のパスを変更してくれるようになっています)

このような相対パスでの記述から

import { HomeScreenHeader } from '../../screens/HomeScreen/HomeScreenHeader'

以下のような src ディレクトリから絶対パスでの記述にするということです。

import { HomeScreenHeader } from '@/screens/HomeScreen/HomeScreenHeader'

本案件の JavaScript ビルドまわりの構成は以下です。

  • `src/` にTypeScript ソースが入っている
  • tsc ( TypeScript Compiler ) が JavaScript を `build/` へと出力する
  • Metro Bundler が `build/` 中のコードをバンドルして React Native に提供する
  • 開発中は常に `tsc --watch` と Metro Bundler のタスクを実行している

このときエイリアスを実現するために以下の設定をしています。

tsconfig.json

// tsconfig.json

{
  // ...
  "paths": {
    "@/*": ["src/*"]
  },
  // ...
}

.babelrc

{
  "presets": ["react-native"],
  "plugins": [
    ["module-resolver", { "alias": { "@": "./build" } }]
  ]
}

TypeScript の path はあくまでパス解決のためだけに使われ import のパスを絶対パスから相対パスへと変換してくれません。tsc から出力されたコードは依然以下のようになっています。

import { Foo } from '@/utils/foo'

このとき以下のように変換するため、 babel で module-resolver を使っています。

import { Foo } from 'build/utils/foo'

export default は禁止

これまた React Native とは関係なく、JavaScript についてです。

よく争点となっている「export は default を使うか名前付き export のみにするか」ですが、個人的にはexport default を禁止にしています。

理由は以下です。

  • export default では export している名前と import 先で使用する名前が無関係
    • そのため、export default のファイル名を変更しても import 先の名前が変わってくれない
  • 異なるモジュールでも 1 ファイルにまとめておいたほうが分かりやすいことが多いため export default 限定はつらい
  • export default で複数のモジュールをまとめて 1 つのオブジェクトとすると Tree Shaking が効かない
  • export default と 名前つき export の混在を混在させる場合、ルール策定が難しい

詳しくは下のページがまとまっています。

Avoid Export Default · TypeScript Deep Dive
With default there is horrible experience for commonJS users who have to const {default} = require('module/foo'); instead of const {Foo} = require('module/foo'). You will most likely want to rename the default export to something else when you import it.
https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html

また、 import / export まわりのルールとして、 export するモジュール名はプロジェクト中で一意となるようにしています。

これはエディタと相性をよくするためです。モジュール名が一意であれば自動 import の候補が少なくなり、スムーズにコードを書くことができるからです。

ただこのルールと export default 禁止した場合に 1 つ問題があります。

たとえば react-redux で connect する FooComponent があったとします。

export const FooComponentComponent = (props) => {
  return (
    <Text>foo <Text>{props.text}</Text></Text>
  )
}

export const FooComponent = connect((state) => ({
  text: state.foo.text
}))(FooComponentComponent)

外からは connect しているかどうかは知りたくないので connect したコンポーネントを ConnectedFooComponent といったような名前にするのは嫌です。そのため、素朴な FooComponent が FooComponentComponent とったような苦し紛れの名前になっています。connect してないFooComponentComponent はコンポーネントカタログやテスティングで使用したいため、export する必要がありますし、 Component という名前で export するとエディタの自動 import に同様のモジュールが大量にひっかかってしまいます。

みなさんはどうされているのでしょうか。知見ある方は @karur4n までお願いします。

React Native を採用する基準

React Native は採用するかどうかの見極めが大変むずかしいです。React Native を採用する基準をミニマムにまとめると以下のようになるのではないでしょうか。

  • 案件をコントロールできることが必須
    • 「React Native だからここは妥協する」という決断ができること
  • 複雑なことはしない
    • Web アプリケーションの一歩先の体験を求めて、くらいがよい
    • ネイティブの機能(カメラ、マイクなど)をガンガン使う場合は厳しい
    • React Native パッケージがそれらネイティブの機能を提供していないため
    • サードパーティのネイティブモジュールだよりになるから

まとめ

以上、React Native でアプリを開発した知見を共有いたしました。

React Native はたしかに Web 技術でアプリを作ることができる面白さがある反面、まだ完成されていないことによるバグなどのつらさもある興味深いツールです。

株式会社スノウロビンでは、価値提供のため新しい技術に積極的にチャレンジしていくメンバーを募集しています!

株式会社スノウロビンでは一緒に働く仲間を募集しています
16 いいね!
16 いいね!
同じタグの記事
今週のランキング