本エントリは3部作のPart3となっております。
モバイルエンジニアの@hotchemiです。
Part1、2では実際にインテグレーションを進めてきた中で得られた知見を公開してきましたが、今回は半年程の運用を経て我々は当初の目的を達成できているのか、という事に関しての振り返りと今後について共有できればと思います。
振り返り
現状を軽く復習しておくと私達は今年の初頭からハイブリッドスタイルの開発を初め、現在のコード比率はNative75%, React Native 25%程となっています。
Good
まず、Part1で宣言した3つの目標に関して振り返ってみます。
- モバイルエンジニア不足の解消(◎)
- 開発とリリースの高速化(△)
- Webフロントエンドとの設計の統一、コードの共用(○)
- ほぼ達成する事ができました。基本的にWebフロントエンドチームのReactプロジェクトと設計思想や利用しているライブラリを合わせている為、フロントエンドエンジニアが開発に参画しやすい土壌が整ったと思います。また、ActionCreatorやロジック層に関しては共通のコードがかなり存在しており現在どう共有していくのが良いか検討しています。
また、上記に挙げた点以外にも以下のメリットを享受する事ができています。
- テストの書きやすさ
- JavaScriptという言語自体のmockのやりやすさ、Reduxの設計が疎結合になっているのでActionCreator、Reducerのテストが書きやすい事、React+Enzyme+JestでUIロジックのテストやViewのsnapshotを網羅できしかもNode.js上で高速に実行できる事は非常に快適だと感じています。iOS/AndroidのNative実装でも頑張れば同じ事は勿論できますが手軽に高速に実行できるという点でテスタビリティには一日の長があると感じています。
- パフォーマンス(主にUI描画のFPS)
- 意外に思われるかもしれませんがReact Nativeはデフォルトで全ての動作をJS専用のバックグラウンドスレッドで実行しUI更新の命令だけをenqueueしメインスレッドに送りつけるというアーキテクチャになっている為メインスレッドでintensiveな処理を実行するという事は原則できないようになっています。私達のアプリには一部メインスレッドで重い処理を実行してしまっているレガシーコードがありそれと比べるとパフォーマンスがむしろ良いという事もあります。
- よく問題になるのはbundleされたJSを読み込む際の初期化時間ですが、こちらは私達のケースでは未だ問題になっていません。
Bad
私達は数多くのメリットをReact Nativeから享受してきましたが、当然うまくいかなかった事もあります。
一例を紹介します。
- Androidでのみ発生する不具合や問題に苦しめられた
- こちらについては後述します。
- クロスプラットフォーム開発自体が難しい
- React Nativeそれ自体とは関係なく、クロスプラットフォーム開発そのものが難しいという事に気付きました。具体的にはUIの変更を検証する為に常に複数のプラットフォームでテストをしなければならない事、各OSにとって最適なコンポーネントは何かを開発者が知っていなければならない事など、発生するコストは無視できない程に存在している、という結論に至りました。
- 両OS用のBridgeを書けるエンジニアは殆どいないのでBridgeエンジニアの作業がボトルネック化するという事態が発生しました。
- Reactを知らないNativeエンジニアにとっては学習コストがある
Androidで発生した問題
先程Androidでのみ発生する不具合や問題に苦しめられた、と書きましたが我々が実際に直面した問題に関して共有できればと思います。
- アーキテクチャが抱える根本的な問題
- Fragment上で発生する問題
- Part2で説明した通り、私達は部分的に導入を進めていった為iOSでは
UITabbarController
の1UIViewController、AndroidではBottomNavigation
上に乗っている1Fragmentという単位で導入していきました。その際に発生した問題は(実装の仕方に勿論依存しますが)Navigationの切り替え時にFragmentが再生成される度にReact Componentも再生成され、componentDidMount
が毎回呼ばれ無駄なAPI呼び出しが発生する、Componentが再生成される為ReactView自体の描画コストがかかるというものでした。 - またBottomNavigation上に乗っているFragmentの上でViewを描画した場合、
FlatList
が最後までスクロールしない、という問題がありました。こちらは恐らくReact Native本体のバグだと思いますが、根本原因解明まで至らず下記の様にスクロール領域を自前で計算するwrapperを利用していました。
- Part2で説明した通り、私達は部分的に導入を進めていった為iOSでは
import React, { PureComponent } from 'react'; import { Dimensions, View } from 'react-native'; interface Props { children: React.ReactNode; } interface State { height?: number; diff?: number; } // There're some cases cannot scroll down to bottom "0" only on Android. // It might be a bug of RN but we can deal with the problem by surrounding a view that has a tangible height. // this view leverages nativeEvent height and calculates diff and store it for device orientation. // // Got a hint from https://github.com/wix/react-native-navigation/issues/2214#issuecomment-347325418 // ref: https://github.com/facebook/react-native/issues/15707 // ref: https://facebook.github.io/react-native/docs/view.html#onlayout // onLayout: http://matthewsessions.com/2017/06/27/react-native-on-layout.html export default class AndroidScrollableWrapper extends PureComponent<Props, State> { private deviceOrientationChangeHandler = this.adjustHeight(); constructor(props) { super(props); this.state = { height: undefined, diff: undefined, }; } adjustHeight() { return () => { if (!this.state.diff) { return; } this.setState({ height: Dimensions.get('window').height - this.state.diff, }); }; } componentDidMount() { Dimensions.addEventListener('change', this.deviceOrientationChangeHandler); } componentWillUnmount() { Dimensions.removeEventListener( 'change', this.deviceOrientationChangeHandler, ); } render() { return ( <View onLayout={event => { const { height } = event.nativeEvent.layout; this.setState({ height, diff: Dimensions.get('window').height - height, }); }} style={{ width: Dimensions.get('window').width, height: this.state.height, }} > {this.props.children} </View> ); } }
今後
上記の問題を踏まえて、我々はiOSのみReact Nativeを使い続けていくという決定をしました。既にAndroidからはdependencyをrevertしています。
iOSのみReact Nativeを使っていくという手法はDiscordでも採用されており、我々のケースでもうまくworkしています。
React Nativeに限らずあらゆる技術は銀の弾丸ではないので、チームのスキルセットや状況に応じて適切に使い分けていく事が重要だと考えています。今回のケースでいうと、部分的に導入→振り返り→意思決定というサイクルを回す事でチームにとって最適なツールの使い方を最小限のコストで見つけ出す事ができました。また今回はReact Nativeの話でしたがKotlinやSwiftでの開発もしっかりやっていますしFlutter等新たなフレームワークに関しても積極的に調査、検討をしていきたいと考えています。
「React Nativeハイブリッドアプリへの挑戦」は以上で終了となります。長々とお付き合い頂きありがとうございました。