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

こんにちは、株式会社ピケ CTO の中西（[@nakanishy](https://twitter.com/nakanishy)）です。
[Dish](https://itunes.apple.com/jp/app/dish-%E3%82%B5%E3%82%AF%E3%83%83%E3%81%A8%E3%83%A9%E3%83%B3%E3%83%81%E3%82%92%E6%B1%BA%E3%82%81%E3%82%88%E3%81%86/id1397367723?mt=8) というランチ検索アプリのフロントエンドとデザインを担当しています。

![top.png](https://qiita-image-store.s3.amazonaws.com/0/28504/4018a1d7-37c2-a435-dfd4-3ea2706cc936.png)

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

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

## React Native の良かった点

### 高速に開発できる

![sample copy.gif](https://qiita-image-store.s3.amazonaws.com/0/28504/db02e0a7-15f0-6570-2e4a-071432923c85.gif)

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

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

ライブリロードとホットリロードの違いは[こちらの動画](https://www.youtube.com/watch?v=2uQzVi-KFuc)がわかりやすいです。

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

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

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

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

### 豊富な UI コンポーネント

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

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

- [react-native-navigation](https://github.com/wix/react-native-navigation)
	- ルーティング
- [react-native-deck-swiper](https://github.com/alexbrillant/react-native-deck-swiper)
	- カードのフリック部分（Tinder UI）
- [react-native-snap-carousel](https://github.com/archriss/react-native-snap-carousel)
	- オンボーディングや条件選択のカルーセル
- [react-native-maps](https://github.com/react-community/react-native-maps)
	- 地図の表示
- [react-native-parallax-scroll-view](https://github.com/i6mi6/react-native-parallax-scroll-view)
	- スクロールすると伸びる画像
- [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons)
	- アイコン（Feather を使用）
- [photo-viewer](https://github.com/merryjs/photo-viewer)
	- フォトビューア

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

React Native には [Animated](https://facebook.github.io/react-native/docs/animated) という組み込みのアニメーション API があり、それが非常に便利です。

React Native にはスタイルを当てるための [StyleSheet](https://facebook.github.io/react-native/docs/stylesheet) がありますが、`animation` プロパティが存在しません。そこで React Native でアニメーションをつけたい場合、基本的には `Animated` を使うことになります。

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

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

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

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

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

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

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

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

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

-  [Spring](https://facebook.github.io/react-native/docs/animated#spring) アニメーション
    -  名前の通りバネの動きを模したアニメーションです。これをつけておけば大抵のアニメーションはイイ感じになります。
- アニメーションの[並列・逐次実行](https://facebook.github.io/react-native/docs/animated.html#sequence)
    - このあたり CSS では結構面倒くさいと思うんですが簡単に実現できます。

<!--
### Built-in コンポーネントが便利

React Native には組み込みの[コンポーネント](https://facebook.github.io/react-native/docs/components-and-apis)がありますが、よく使う便利な機能がデフォルトで付いていて便利でした。特に便利だったものをいくつか挙げます。

 `Text` コンポーネントには `numberOfLines` という prop があり、行数を指定すると「...」をつけて省略してくれます。Web においてはいくつかスタイルを書かないと実現できませんが、それが標準でサポートされています。

`Image` タグ。ただし SVG が使えないので注意です。
	- Image
	- Text
-->

## React Native の悪かった点

### ビルドが不安定

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

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

その原因の殆どは、サードパーティーのライブラリやコンポーネントによるものです。特に[ネイティブモジュール](https://facebook.github.io/react-native/docs/native-modules-ios)を使ったライブラリ関連でよく問題が起きました。具体的には、React Native とのバージョンの齟齬によるエラーや、ネイティブモジュールのインストール時にミス（？）がありエラーが出るときと出ないときがある、などです。

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

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

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

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

![Artboard.png](https://qiita-image-store.s3.amazonaws.com/0/28504/89b81b4c-66e0-41d5-cfca-15b38f008d97.png)


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

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

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

![debugger.png](https://qiita-image-store.s3.amazonaws.com/0/28504/91106d1e-ff33-4ff1-83f0-8d3190f8a9f9.png)

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

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

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

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

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

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

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

[StyleSheet ](https://facebook.github.io/react-native/docs/stylesheet) では、一部の CSS プロパティや値がサポートされていません。以下に開発中に使えなくて不便だと思ったものを挙げます。

#### グラデーション

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

解決策として、[react-native-linear-gradient](https://github.com/react-native-community/react-native-linear-gradient) を採用しました。以下のようなグラデーションを使ったボタンが作れます。

![Screen Shot 2018-07-31 at 13.49.09.png](https://qiita-image-store.s3.amazonaws.com/0/28504/b87a5ec7-ae29-a113-eb29-6c5484cd736c.png)

##### 補足

React Native に [ART](https://github.com/facebook/react-native/blob/master/Libraries/ART/ReactNativeART.js) という図形の表示ライブラリが含まれていて、これを使うとグラデーションが描画できるようです。ただし、ART は[ドキュメント](https://facebook.github.io/react-native/docs/components-and-apis.html)にも載っていない API なので、使う際は注意が必要です。


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

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

候補はいくつかありますが、[react-native-responsive](https://github.com/adbayb/react-native-responsive) で解決しました。

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

MediaQueryStyleSheet.create(
  {
    container: { padding: 20 }
  },
  {
    // iPhone SE
    '@media (max-device-width: 320)': {
      container: { padding: 10 }
    }
  }
})
```
### グローバルスタイルが定義できない

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

代わりに[公式のドキュメント](https://facebook.github.io/react-native/docs/text.html#limited-style-inheritance)では、独自のコンポーネントを定義しアプリ全体に渡ってそれを使うことを推奨しています。例えば、デフォルトの文字色を定義したい場合、`MyAppText` のようなコンポーネントを作ってスタイルを当て、アプリ内のすべての `Text` を `MyAppText` に置き換える、という具合です。

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

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

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

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

![ezgif-4-0c7de3ce61.gif](https://qiita-image-store.s3.amazonaws.com/0/28504/6b5d410c-8a2a-d91e-4154-4dfec980015c.gif)

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

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

![launchimage.png](https://qiita-image-store.s3.amazonaws.com/0/28504/fec48305-75e6-98a8-499d-7fd9953b0d9f.png)

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

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

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

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

#### 参考

- [React Native製アプリのクオリティを上げるために工夫した事 – 週休７日で働きたい](https://blog.craftz.dog/lessons-learned-from-creating-my-mobile-app-to-build-a-high-quality-react-native-app-dcf021ce37ef)

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

![Screen Shot 2018-08-23 at 21.35.41.png](https://qiita-image-store.s3.amazonaws.com/0/28504/a11fb3c3-e884-1a74-c2a6-7dcc9cd8f6d2.png)

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

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

#### 追記

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

- [Setting text contains combination of Chinese and English with lineHeight style cause characters not on same baseline](https://github.com/facebook/react-native/issues/19193)

## まとめ

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

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

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

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

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

---

Dish のサーバーサイドの話は [「Dish」を支えるランチ推薦アルゴリズム](https://note.mu/furuchin/n/neaaadbd60aee) にあるので、興味があればご覧ください。
[Twitter](https://twitter.com/nakanishy) もやってるので、よければフォローお願いします。