React Native でアプリ開発をして良かったところ・ツラかったところ

こんにちは。ビズリーチの新卒事業部でフロントエンドエンジニアをやっている円谷(@___35d)です。以前ビズリーチのデザイナーブログにも登場して、通勤風景の写真でチーム内で相当いじられました。興味ある方はぜひそちらも読んでみてください。今日はエンジニアブログの方を書いていきたいと思います。

さっそく本題ですが、2018 年 11 月に ビズリーチ・キャンパス for OB/OG がフルリニューアルされました。いままで WebView ベースで構築されていたアプリをリデザインし、React Native でフルネイティブ化しました。この記事では React Native を採用するまでに至った背景と、導入してどうだったのかについてお話できればと思います。この記事が、これから React Native を使ったアプリ開発を検討している方の参考になれば幸いです。

リニューアルした ビズリーチ・キャンパス for OB/OG

リニューアルした ビズリーチ・キャンパス for OB/OG

目次

  • はじめに
  • 社内事情と技術選定について
  • React Native で良かったところ
    • 学習コストが少なくて済み、アプリのメンテナが増えた
    • Hot Reload によってスタイル修正がすばやくできた
    • React Native Debugger によってスムーズなデバッグが可能になった
  • React Native だとツラかったところ
    • Redux の初期学習コストが高くてツラい
    • アニメーションの実装の煩雑さ / パフォーマンスチューニングがツライ
    • バージョンアップについていくのがツライ
  • 結論:導入してよかった
  • ほかの開発者から一言

社内事情と技術選定について

React Native は、Facebook が開発したクロスプラットフォームアプリ開発用フレームワークです。React Native でアプリを作ることで、クロスプラットフォーム( iOS / Android どちらでも動く)アプリを作ることができます。 Facebook 社製なので Facebook はもちろんですが、Instagram / Facebook Messenger / Discord / Skype など、さまざまな大規模アプリで採用実績があり、世界的に注目を浴びつつあります。React Native という名前の通り、 React (JavaScript) が書ければ、アプリ開発をすることができることも特徴の一つです。

以上の特徴を踏まえて、私たちは React Native を導入することを決めました。その理由の 1 つとして、ビズリーチは Web に精通したエンジニアが多い一方、アプリエンジニアが少ないという状況が続いていることが挙げられます。私の所属する新卒事業部で見ても、Web エンジニアは 10 名程度在籍している一方で、アプリエンジニアは @kouchi67 の 1 人しかおらず、アプリのメンテナンスは彼にお任せといった状況でした。

私たちの事業部では、Web に精通しているエンジニアなら比較的学習コスト少なくコードが書けるであろう React Native をアプリの開発言語として採用し、アプリ開発に携われる人数を増やそうとしました。ビズリーチ・キャンパス for OB/OG は、アプリエンジニア 1 名と、私を含む他 3 名の Web エンジニアで開発を行いました。

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

😀 学習コストが少なくて済み、アプリのメンテナが増えた

一番の良かったことはこれだと思います。上記のような課題を抱えていた私たちの組織に React Native にはぴったりフィットしていました。一人に依存していたアプリのメンテナンス作業も、開発人員が増えたおかげで複数人で行うことができるようになりました。React Native を採用することで、組織の課題を解決することができました。また、スタイルの記述は CSS とほぼ同じなので、デザイナーでも微妙なスタイルの調整を行うことができたことも良かったことの一つでした。

せっかくの Tech Blog なので、技術の観点からも良かったことを何点か列挙したいと思います。React Native のさまざまな機能を駆使することで、生産性とコード品質を向上させることができました。

😀 Hot Reload によってスタイル修正がすばやくできた

私たちは、デザイナーが Sketch でデザインカンプを作成、エンジニアが実装、その後、デザイナーに実機で見た目を確認してもらう(デザイナーレビュー)、というフローで開発を行っていますが、デザイナーレビュー時の細かいマージンの調整などは Hot Reload (エディタ上で修正したものが即時に反映される) 機能を使うことでスピーディに行うことができました。デザインカンプとの微妙なズレは、デザイナーに実機で確認してもらいながらエンジニアがコードを修正する、という進め方でスタイルを調整することができ、スピーディな開発を実現することができました。

Hot Reload を使ってスタイル調整できる

Hot Reload を使ってスタイル調整できる

😀 React Native Debugger によってスムーズなデバッグが可能になった

複雑なアプリになればなるほどアプリの状態管理は複雑になります。React で状態管理をするには定番の Redux を私たちのチームでも採用しました。Redux を導入することで、Store と呼ばれる格納領域に、状態管理を一任することができます。

さらに、React Native Debugger を導入することで、この Store の状態を常に確認しながら開発を進めることができました。Action が発生するたびにログが流れ、Store にどのような変更が発生したのかを GUI で確認することができました。さようなら console.log(); ちなみにこの React Native Debugger は、Chrome などのブラウザと同じく、アプリ内の DOM の構造やサイズ、スタイルなども見ることもできます。原因不明のスタイル崩れなどもこれを使うことで素早く原因箇所を特定することができます。とても便利なツールでした。

React Native Debugger で Store の状態を随時確認できる

React Native Debugger で Store の状態を随時確認できる

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

ここまで良かったことをつらつらと書いてきましたが、つらみを感じた部分もたくさんありました。社内の他のエンジニアからもつらみの声がたくさん上がりました。その声の中から 3 つピックアップして紹介したいと思います。

 React Native つらみシート

React Native つらみシート ここにメンバーが感じたつらみが集約されていった

🤮 Redux の初期学習コストが高くてツラい

当初は Redux に熟練していないメンバーが多かったので、チーム皆で理解と共通認識を深めながらコードを書いていました。また、どのプロジェクトでもそうだと思いますが、業務ロジックへの理解も浅い状態からのスタートでした。

プロジェクトが進み Redux の習熟度と業務ロジックへの理解も深まるにつれて、書いている時点では最適と思ったコードが技術的負債になっていたことに気づくことが多々ありました。それに伴い Store も再設計していく必要がありましたが、Redux 自体の記述量が多く、修正 → 確認のイテレーションを回すのに時間がかかってしまうことが多々ありました。また、再設計の過程で新しいバグを生み出してしまう場合もあり、ここにつらみを感じることがありました。

最初から完全に理解した状態で作り上げることは不可能なので、技術的負債と常に向き合い続け再設計し続けるプロセスを踏む必要があります。これは私の主観ですが Redux は初期学習コストが決して低いものではないので、私たちのようなチーム体制の組織に導入する上では、上記のようなことに直面することへの覚悟がある程度必要だと思っています。

🤮 アニメーションの実装の煩雑さ / パフォーマンスチューニングがツライ

今回の開発では、アニメーションにも力を入れましたが、実装はなかなかのつらみを感じました。上記の Redux と同じですが、アニメーションに精通したメンバーがいたわけではなかったので、試行錯誤を繰り返しつつ実装を進めていきました。その過程で、コードの煩雑さとパフォーマンスチューニングで特につらみを感じました。

煩雑なアニメーションコードと戦い続ける

 話を聞きたいアニメーション

話を聞きたい アニメーション

こちらの例は、学生から OB に話を聞きたいリクエストが飛んだときに表示されるカードですが、このカードをタップすると以下のようなアニメーションが同時に発生します。

  • カード自体が伸縮
  • 画像が拡大
  • 閉じるボタンがフェードイン
  • 上からシャドー
  • 下からボタンが登場
  • 下からプロフィールが登場

同時に多くの要素がアニメーションすることで、実装がどんどん煩雑になっていき、render() が肥大化、アニメーションのデバッグはますます困難になっていきました。そこで以下のように、アニメーションさせる処理を分割しなるべく見通しのよい状態を維持し続けるように試みました。

renderAnimation = () => {
  const animationArray = []; // アニメーションさせたい要素を配列に格納
  animationArray.push(this.renderCloseButton());
  animationArray.push(this.renderBottomButtons());
  animationArray.push(this.renderProfile());
  animationArray.push(this.renderShadow());
  return animationArray;
};
renderCloseButton = () => (
  <Animated.View
    style={{
      position: 'absolute',
      top: Theme.getStatusBarHeight(true),
      left: 10,
      width: 40,
      height: 40,
      zIndex: 301, // シャドーより大きい必要がある
      justifyContent: 'center',
      alignItems: 'center',
      opacity: this.state.closeButtonOpacity,
    }}
    key="renderCloseButton"
  >
    <Animated.Image
      style={{
        width: 21,
        height: 21,
      }}
      source={{
        uri: 'closeButton',
        scale: Dimensions.get('window').scale,
      }}
    />
  </Animated.View>
);

アニメーションのパフォーマンス問題と戦い続ける

アニメーションが煩雑になると、どうしてもパフォーマンスが低下してしまうことがありました。 shouldComponentUpdate で余分な render() が走らないように細かに制御しました。初期描画時はアニメーションをしない、などアニメーションを実行するかどうかを state にフラグとして持たせていますが、 state が更新されるたびに render() が実行されるとパフォーマンスがどんどん落ちていきます。shouldComponentUpdate を適切に使用することで、render() の実行を必要最小限に抑えることができます。

shouldComponentUpdate(nextProps: Props, nextState: State) {
  if (this.state.navBarImageOpacity !== nextState.navBarImageOpacity) {
    return true;
  }
  // 中略
  return false;
}

また、以下の例はローディングのアニメーションの実装コードですが、useNativeDriver を使用しないと、アニメーションの動作がカクカクになってしまい、ユーザー体験を損ねてしまいます。useNativeDriver を適切に使用し、軽快に動作するよう、細かい調整がアニメーションの実装には必要になってきます。

runAnimation(easing: Easing) {
  this.state.animation.setValue(90);
  this.state.opacity.setValue(0.15);
  Animated.parallel([
    Animated.timing(this.state.animation, {
      toValue: 270,
      duration: 680,
      easing,
      useNativeDriver: true,
    }),
    Animated.sequence([
      Animated.timing(this.state.opacity, {
        toValue: 1,
        duration: 340,
        easing,
        useNativeDriver: true,
      }),
      Animated.timing(this.state.opacity, {
        toValue: 0.15,
        duration: 340,
        easing,
        useNativeDriver: true,
      }),
    ]),
  ]).start(() => this.runAnimation());
}

つらみがたまってネガティブなことをたくさん書いてしまいましたが、一方で、シンプルなアニメーション(要素が移動するだけ等)は LayoutAnimation を使用することによって簡潔に記述することができるので、少しリッチにしたい、という場合には簡単な実装で実現することができます。

🤮 バージョンアップについていくのがツライ

React Native は 3 ヶ月に 1 度大きなバージョンアップがあります。まだメジャーバージョンが出ていない(執筆時点で最新バージョンが v0.57.7 )ということもあり、今後破壊的なアップデートが出てくる可能性も多いにあります。

メンテナンスしやすいアプリケーションを維持するためには、バージョンアップ作業は必要不可欠だと私たちは考えています。バージョンアップの詳しいやり方については他の記事に譲ることにしますが、完全に自動化はできない作業なため(手作業でコンフリクトを解消したりする必要がある)、少なからず作業工数がかかります。バージョンアップ作業そのもの以外にも、バージョンアップに伴うライブラリの動作確認、アプリ全体の動作確認などにも工数がかかります。

React Native 導入を検討している場合、この頻繁なバージョンアップを受け入れることができるかも事前に考慮しておくべきだと思います。

結論:導入してよかった

ここまで良かったこととつらかったことを書いてきました。開発途中で、Airbnb が React Native から撤退 というショッキングなニュースがあったりして社内がざわついたりしましたが(笑)、React Native 導入によってアプリのメンテナが少ないという課題を解決することができたのが私たちにとってはとにかく大きく、結論、React Native を導入して良かったなと考えています。

開発中私たちが感じたつらみも、私たちの技術力不足が原因だったことがほとんどで、レベルを上げて物理で殴れば解決することばかりだったかなと今では思っています。いま 0 から書き直したらもっと良い設計にできるはず。

テストが完全に導入できていなかったり、クロスプラットフォームといいつつ Android アプリはまだリリースできていないなど、まだまだやりきれてないところもありますが、今後も React Native と仲良くしていければ良いなと思っています。少なくとも僕は今回のプロジェクトを通して React Native のことがだいぶ好きになりました(笑) この記事がこれから React Native を導入しようと迷っている方々の判断材料の一つになればと思います。ここまで読んでいただきありがとうございました!

ほかの開発者からひとこと

若様

冒頭に登場しました @kouchi67 です。僕を救ってくれた React Native ですが、Swift にはないメリット・デメリットがあり、とても勉強になりました。辛い部分もありますが、チーム構成や目的によっては選択肢になりうるのが React Native だと思いますし、少なくとも私達には Fit してるかなと思いました。このブログを読んで興味を持った方がいたら是非チャレンジしてみてください!

ボギーさん

自分はモバイルアプリエンジニアではなかったのですが、React Native を採用することでスムーズに開発に入ることができたと思います。レイアウト周りで Web の知識を活かすことができたことと、Hot Reloading による変更の即時反映機能は、React Native で得られた大きなメリットでした。もちろん使っていくうちにつらみを感じることもありましたが、手軽にモバイルアプリ開発に手を出せるのは面白いと思います。

おのじゅん

Objective-C、Swift の遺産やツラミを差し置いても採用する魅力が React Native にはあると思います。React Native を通してモバイルアプリを作る楽しさが広がり、面白いアプリが増えたら素敵ですね。


ビズリーチや React Native に興味がある方はこちら 👇 まで

スモールチームでアジャイルに就活支援サービスを開発するエンジニアを募集! by 株式会社ビズリーチ