JavaScript
iOS
reactjs
React
reactnative

React Native をプロダクションで使ってわかった良かった点・悪かった点

こんにちは、株式会社ピケ CTO の中西(@nakanishy)です。
Dish というランチ検索アプリのフロントエンドとデザインを担当しています。

top.png

Dish では、React Native を採用して iOS アプリの開発をしています。iOS アプリを正式にリリースしたことがなく、また React Native も初めてだったので、苦戦する場面も多々ありましたが、良かった点もたくさんありました。

この記事では、Web 開発者の視点で React Native の良かった点と悪かった点を紹介したいと思います。

React Native の良かった点

高速に開発できる

sample copy.gif

React Native は、ライブリロードとホットリロードをデフォルトでサポートしているので、Web 開発をしているかの如く高速に開発が進められました。保存すると即座に変更が反映されます。また、ホットリロードの場合、状態を維持したまま変更分だけ反映されます。

Xcode の内容を変更したり、ネイティブモジュールのライブラリを入れない限り、再ビルドの必要がないので非常にスムーズに開発できました。

ライブリロードとホットリロードの違いはこちらの動画がわかりやすいです。

Web 開発における React の知見が活かせる

React での開発経験はあったので、余計な学習コストをかけずに開発できました。Redux でのステート管理、ESLint や Jest を使ったリントやテスト、Storybook を使ったコンポーネントの管理など、Web 開発で使っていた技術やノウハウが基本的にはそのまま使えます。

唯一、学習が必要だったのは Xcode 周りの知識です。例えば、実機でテストするための設定やアプリ公開時のビルド方法、ネイティブ(Swift や Obj-C)で書かれたモジュールのインストール方法、CocoaPods での依存管理などです。

iOS アプリの開発経験があれば問題ないと思いますが、私のように Web しかやってこなかった人は初めは戸惑うかもしれません。ただ、検索すれば日本語の記事がたくさん出てくるので大抵のことは解決できました。

豊富な UI コンポーネント

サードパーティの UI コンポーネントが非常に充実しています。コンポーネントを組み合わせるだけで簡易なアプリなら簡単に作れるでしょう。

Dish でも、主要な UI のほとんどにサードパーティのコンポーネントを使っています。以下に使用しているコンポーネントの一部を載せておきます。

複雑なアニメーションが簡単に作れる

React Native には Animated という組み込みのアニメーション API があり、それが非常に便利です。

React Native にはスタイルを当てるための StyleSheet がありますが、animation プロパティが存在しません。そこで React Native でアニメーションをつけたい場合、基本的には Animated を使うことになります。

はじめは「癖が強いなぁ」と思っていましたが、慣れると簡単に柔軟に複雑なアニメーションが作れるので、質の高いアプリを作る上では欠かせないなと思いました。

使い方を軽く紹介します。
まずは state で初期値を設定します。

state = {
  opacity: new Animated.Value(0)
}

アニメーションの対象となるコンポーネントを Animated.* に置き換え、stylestate の値を指定します。

<Animated.View
  style={{
    opacity: this.state.opacity
  }}
>
  ...
</Animated.View>

任意のタイミングでアニメーションをスタートさせます。

componentDidMount () {
  Animated.timing(this.state.opacity, {
    toValue: 1
  }).start()
}

このようにすると、コンポーネントがマウントされたとき、Animated.Viewopacity が 0 から 1 に変化し「ビューがフワッと出現するアニメーション」が作れます。

紹介したのは基本的なアニメーションですが、他にも以下の点が便利です。

  • Spring アニメーション
    • 名前の通りバネの動きを模したアニメーションです。これをつけておけば大抵のアニメーションはイイ感じになります。
  • アニメーションの並列・逐次実行
    • このあたり CSS では結構面倒くさいと思うんですが簡単に実現できます。

React Native の悪かった点

ビルドが不安定

ビルドやリリースする際のアーカイブが不安定でした。

例えば、シミュレータでは動くのに実機で動かそうとするとビルドが失敗したり、ビルドは通るけどアーカイブが失敗したり、タイミング次第でビルドが通るときと通らないときがあったり、自分の環境では動くのに他の環境で動かなかったり、様々ありました。

その原因の殆どは、サードパーティーのライブラリやコンポーネントによるものです。特にネイティブモジュールを使ったライブラリ関連でよく問題が起きました。具体的には、React Native とのバージョンの齟齬によるエラーや、ネイティブモジュールのインストール時にミス(?)がありエラーが出るときと出ないときがある、などです。

issue を読み漁って一つずつ解決しましたが、基本的に英語なのでトラブルシューティングは結構大変でした。

React Native 本体が悪いというより、周辺ライブラリとの兼ね合いで不安定になることが多いので、特にネイティブモジュールを入れる際は慎重に選定し、可能な限り使わないほうが問題はすくないかなと思いました。

標準のデバッグツールが使いづらい

React Native では標準の「検証ツール(Inspector)」が非常に使いづらかったです。

Artboard.png

特に以下の点で使いづらかったです。

  • 文字が小さくて読みづらい
  • スタイルの値がいじれない
  • React の stateprops が見れない
  • 「Network」で通信内容が見れない

解決策として、react-native-debugger を使いました。スタンドアロンのデバッグツールで、Chrome のインスペクタとほとんど同じ使い方ができます。

debugger.png

ライフサイクルが足りない

困ったのは「ビューが表示されたとき」に実行されるライフサイクルメソッドがないことです。マウントされたときには componentDidMount が実行しますが、表示されたときに実行されるライフサイクルがありません。具体的には、スクリーンを A → B → A と遷移したとき、最後のスクリーン A が表示されたことを検知できません。

具体的なシチュエーションとしては、Google Analytics のスクリーンビューをトラッキングする場面です。A → B → A と遷移したとき、最後の A がトラッキングできないと困ります。

解決策の一つとして、遷移した際にルーティング用の state を変化させ getDerivedStateFromPropscomponentWillReceiveProps で変更を検知する方法もありますが、強引かつ余計な実装がいるのであまりやりたくありません。

結局、この辺はナビゲーションライブラリに任せることにしました。react-native-navigation を入れると componentDidAppear というライフサイクルが追加されます。名前の通り、コンポーネントが「表示」されると発火するライフサイクルなので、上記のスクリーントラッキング問題が解決しました。

Dish が画面遷移の少ないシンプルなアプリなので、はじめは redux で独自でルーティングのステートを管理していたのですが、上記の問題に気づきこのままだと煩雑になると思ったのでライブラリに任せることにしました。

一部の CSS プロパティや値が使えない

StyleSheet では、一部の CSS プロパティや値がサポートされていません。以下に開発中に使えなくて不便だと思ったものを挙げます。

グラデーション

background: linear-gradient() が使えません。Dish ではサービスカラーがグラデーションなので、ボタンや背景にグラデーションを多用しているので困りました。

解決策として、react-native-linear-gradient を採用しました。以下のようなグラデーションを使ったボタンが作れます。

Screen Shot 2018-07-31 at 13.49.09.png

補足

React Native に ART という図形の表示ライブラリが含まれていて、これを使うとグラデーションが描画できるようです。ただし、ART はドキュメントにも載っていない API なので、使う際は注意が必要です。

メディアクエリが使えない

メディアクエリがないので、デバイスサイズに応じて余白やフォントサイズを変えることが難しいです。iPhone SE, iPhone 8, iPhone X の3サイズは最低限サポートしたかったのですが、デバイスサイズが違うのでどうしても崩れてしまいます。

候補はいくつかありますが、react-native-responsive で解決しました。

 import { MediaQueryStyleSheet } from 'react-native-responsive'

MediaQueryStyleSheet.create(
  {
    container: { padding: 20 }
  },
  {
    // iPhone SE
    '@media (max-device-width: 320)': {
      container: { padding: 10 }
    }
  }
})

グローバルスタイルが定義できない

React Native では、Web のように、ある要素のデフォルトのスタイルを定義しておくことができません。

代わりに公式のドキュメントでは、独自のコンポーネントを定義しアプリ全体に渡ってそれを使うことを推奨しています。例えば、デフォルトの文字色を定義したい場合、MyAppText のようなコンポーネントを作ってスタイルを当て、アプリ内のすべての TextMyAppText に置き換える、という具合です。

しかし、この作業が手間だったので、Ajackster/react-native-global-props を使いました。内部の処理を見ると、割と強引にスタイルを当てていたので、気持ちの良いものではありませんでしたが、ひとまずスピード重視で使っています。

import {  setCustomText } from 'react-native-global-props'

setCustomText({
  style: {
    color: '#333',
    fontSize: 16
  }
})

スプラッシュがフェードアウトしない

ezgif-4-0c7de3ce61.gif

左がフェードアウトしないもの、右がフェードアウトするものです。微妙な差ですが、実際にアプリを触ると違いがよくわかります。

iOS アプリでは、スプラッシュスクリーンを表示する場合、以下の画像のように Xcode で Launch Image または Launch Screen File を指定します。ただ、これだけだと左の画像のようにフェードアウトされずプツっと切れたような表示になってしまい、違和感が残ります。

launchimage.png

この問題は react-native-splash-screen で解決しました。このライブラリは、任意のタイミングでスプラッシュスクリーンを表示したり、隠したりできるものです。

白くなる原因は、おそらく JavaScript のコードのロードが走るためだと思います。そこで、一番最初に描画されるコンポーネントがマウントされるまでスプラッシュを表示しておくことで白い画面で途切れることなく、スムーズに繋がります。

import SplashScreen from 'react-native-splash-screen'

class FirstScreen extends Component {
  componentDidMount() {
    // スプラッシュスクリーンを非表示にする
    SplashScreen.hide()
  }
}

参考

英文と和文を混植するとズレる

Screen Shot 2018-08-23 at 21.35.41.png

英文と和文を混植すると若干ズレてしまいます。

この問題はまだ解決できていないので、ご存知の方がいたら教えていただけると嬉しいです。

追記

@sasurau4 さんからコメントで react-native の issue に上がっていると教えていただきました。
ありがとうございます!

まとめ

React Native をプロダクションで使ってみてわかった良かった点と悪かった点をお伝えしました。

React Native を選定して良かったか悪かったかで言うと、選んで良かったです。というよりはむしろ、他に選択肢がなかった、といったほうが正しいです。React は経験ありという前提で、以下のような制約条件にあてはまるのが React Native しかなかったです。

  • 短期間でアプリをリリースする必要があった
  • ネイティブ(iOS, Android)エンジニアがいない
  • 質の高いネイティブアプリ

不安定な部分はあったものの、実現したいクオリティのアプリを短期間で作ることができたので、満足しています。パフォーマンス面でもあまり気になるところはありませんでした。むしろ、Web の技術でここまで作れたら十分だなと思いました。

個人的には、次アプリを開発する機会があっても(もちろん要件次第ですが) React Native を採用すると思います。唯一の懸念点はパフォーマンス面ですが、大抵のアプリでは問題にならないのではないかなと感じました。実際に作る際には要検証ですね。


Dish のサーバーサイドの話は 「Dish」を支えるランチ推薦アルゴリズム にあるので、興味があればご覧ください。
Twitter もやってるので、よければフォローお願いします。