この記事は、Retty Advent Calender 2021 Part2の16日目の記事です。
はじめに
Rettyのアプリ開発チームでiOSアプリ/Androidアプリ/サーバーサイドエンジニアをしている山田です。 Rettyのアプリ開発チームでは、日々の施策実装の傍ら、安定した開発をより早く回していくために技術負債への対応も並行して行っています。 今年、その中でも特に大きな負債だったReactNativeの脱却をなんとか成し遂げることができました。
- そもそもなぜReactNativeを採用したのか
- なぜ脱却することになったのか
- どうやって脱却したのか
- 脱却して得られたものはあったのか
これらについて順を追って共有します。
そもそもなぜReactNativeを採用したのか
時は数年ほど遡ります。
2017年末ごろ
この時、iOSアプリの提出には必須であるAppleの審査には多大な時間が掛かっていたと言われています。これには1~2週間ほどの時間がかかり、よりスピーディにPDCAを回していきたい当時のアプリ開発チームとしては、この時間をできるだけ圧縮したいというニーズがありました。
但し、それが行える方法は限られています。AppStoreでアプリを公開する限りは、審査は必ず行う必要があります。動的に機能を切り替える仕組みも、あまり激しく行うと審査で弾かれる要因になります。WebViewで表示するコンテンツについてはある程度動的な変更に自由度がありましたが、Webコンテンツを表示する形式ではせっかくのネイティブのパフォーマンスの良さを活かす事ができません。
そこでRettyではApp Store Reviewガイドライン的に問題が無さそう*1だった、ReactNativeで機能を実装し、OTAにより細かい変更を素早くユーザーさんに届ける方法でこの問題を解決しました🛫
当時運用していたOTAの仕組みについては、以下に挙げた過去の記事で詳細にまとめられています。
これによりReactNativeを活用していた最盛期にはWebアプリケーションのように週に何度もリリースを行い、それによりPDCAを高速回転させ、より良いサービス作りに繋げることができました 🎉
なぜ脱却することになったのか
このような背景でReactNativeを導入し、実際にそのメリットも得られたRettyのiOSアプリですが、なぜこれを脱却することになったのでしょうか?
2021年ごろ
それから数年後、Appleの審査のプロセスも改善されました。2021年にもなってくると、審査を提出した翌日にはレビュー開始、さらに翌々日にはレビューが完了するようになり、以前ほどAppleの審査の速さに対して不満を感じる事は無くなりました。
ここまで迅速に行ってくれるとなると、過去に課題を感じていたようなPDCAを回していく難しさについても、特にチームでは課題と捉えるような声も無くなりました。
さらに、ReactNativeには
- ホットリロードが利用可能
- Reactを用いた生産性の高い宣言的なUIを記述できる
といった特徴もあり、前述したOTAを利用したいという側面以外にも開発に役立つメリットとなっていました。しかし、2019年に登場したSwiftUIがこの年になるとようやくRettyアプリでも利用できる環境が整いました。SwiftUIには前者に相当する機能としてXcode Previewsを利用したプレビュー機能が、後者についてはSwiftUI自体がReactの流れを汲んだ宣言的UIフレームワークの一種である、というところもあり、ReactNativeが実現していたことをSwiftUIでも実現することができました。
ReactNativeを導入したことによる問題
一方で、ReactNativeを導入したことによって発生した課題は残り続けます😢。ざっと以下のような課題が発生していました。
iOS開発とは別にReactNativeの知識を要求されてしまい、 開発・採用の壁に
Rettyの現在のアプリ開発エンジニアは基本的にはiOSネイティブ、Androidネイティブをメインのスキルセットとしているため、それらとは若干畑が違うReactNativeを理解でき、快適に開発できるメンバーが限られていました。当初導入を決定したチームのメンバーも数年も経過すると様々な理由からチームを離れられ、その結果数少ないReactNativeを理解できるメンバーが開発や保守運用を担当することとなり、属人化が発生していました。
また、採用の面から見ても、できればReactNativeを理解してくれる人だと嬉しい、と言う条件が追加されてしまったため、一緒に働いて頂けるメンバーを探す障壁になっている部分もありました。
ReactNativeのパフォーマンスissueが発生
主にListViewを利用する箇所について、アプリの挙動が大変重くなってしまう現象が発生しました。FlatListに移行することで概ね改善はされたものの、全体的にネイティブ実装の箇所よりも動作が重たい部分がある点は否めませんでした。これについてはReactNativeの問題というよりも、その使い方によるものである可能性も高かったものの、現実問題として前述のようにReactNativeを理解できるメンバーが少なかったことによりまとまったリソースを確保できなかったため、改善しようという動きはなかなか実を結びませんでした。
OSや言語の更新に伴うReactNative側の運用タスクが重い
例えば最近の例で言うと、ReactNativeがUIWebViewに依存しているため、更新を行わないと審査に通らなくなってしまう、と言う問題がありました。
上記の例以外にも、パフォーマンスやセキュリティの向上などが見込まれるため、継続的な更新は導入した以上行わなければなりません。更新にはもちろんコストがかかります。具体的にRettyの例では三ヶ月に一回ほどの頻度で三〜五人日ほどの工数がコンスタントにこのために消費されていました。
こう言ったあまり活用できていない機能はオミットしていき、運用にかかるコストを軽減し、より本質的な作業に集中したいです。
ReactNative自体が巨大なのでビルドに時間がかかりがち
ReactNative自体が巨大でビルドに時間がかかります。概ねReactNativeのビルド時間がアプリ全体のおおよそ30%を占めており、CIの実行時間が長くなってしまう原因の一つとなっていました。
クロスプラットフォームフレームワークの良さを活かせていない
ReactNativeはおそらく読者の皆さんの多くが認識している通りクロスプラットフォームフレームワークであり、iOS/Android双方のUIを実装することができます。しかし、RettyのiOSアプリがReactNativeを採用した一番の理由はOTAを使いたい、という所であり、リリースを大きくブロックしてしまう要素がなかったAndroidアプリではReactNativeを採用していませんでした。このため、クロスプラットフォームフレームワークならではのメリットを享受することができていませんでした。
Androidアプリに対してReactNativeを採用する方針も後から検討はしましたが、その当時にはReactNativeを理解できるメンバーが少ない状態になってしまっていたこと、そもそもiOSアプリで利用されることが前提で記述されたReactNativeのコードを共通化できるようにするためにコストがかかる、という問題があり、その方針で進むことはありませんでした。
総合的に考えて
ここまで記述した通り、導入から数年が経過し、導入当時はベターな選択だったものが現在の視点からするとそうともいえない状態になっていることがわかりました。プラットフォームは改善され、新しい選択肢が追加され、チームの形が変わり、現在のチームにとってはReactNativeを十分に活かせておらず、その運用が重たいコストとしてのしかかってしまっている状態です。
チームメンバーにこう言った課題を認識していることを共有すると、やはり同じような考えを持つ方が多く、あえてReactNativeを続けるよりはSwiftUIを採用する形にするのが良いのではないか?というフィードバックを得ることが出来ました。チームの総意としてReactNativeをやめた方が生産性が高まると考えているのであれば、これは脱却をやっていくしかないなと決意しました。
どうやって脱却したのか
やめたい!と思っても、それを実行に移しやり切ることはなかなか難しいものですが、Rettyアプリ開発チームでは以下のような手順を踏んでやり切ることができました。
1. 使用箇所をまとめ、規模感を推測する
何をどこまでやれば良いのかが曖昧な状態で進めるのは精神的にも大変ですし、何よりも先を見通すことができないので日々の施策開発と並行して進めるのはとても難しいです。そのため、こういった大掛かりな物事をやり切るには、規模感を把握してそれを元に計画を立てることが肝要です。Rettyアプリ開発チームでは、まずReactNativeがどの程度利用されており、どのくらいの置き換え作業を行うべきかを大雑把にまとめるところから脱却作業が開始しました。
2. ReactNativeを続けるデメリットをまとめ、プランナーさんにお伝えする
何をすれば良いのかがわかったら、次はその計画をプランナーさんに伝えます。あわせて、なぜReactNativeを脱却しなければならないのか、という点をエンジニアとしての前提知識抜きでわかりやすくお伝えします。例として、「ReactNativeを脱却しネイティブで作り直すことで、より早く施策開発を完了させることができる」という具体的なメリットを挙げて会話をした記憶があります。
3. 機能の整理を行う
これだけ大きな作業を行っていると、中には本当にこれは移行して再度実装する価値のある機能なんだろうか、と疑問に感じるものも発見されるかもしれません。そう言ったものについては積極的にチームメンバーやプランナーさんと対話をし、本当に必要なものなのかを再度確認しましょう。実際に該当機能が実装当初に想定していた効果を出せているのかを確認していただいたところ、あまり奮わなかったものもありました。こう言ったものは機能ごと削除してしまうという判断をしていただき、作業にかかる工数をうまく圧縮することができました。
4. 脱却を画面単位で部分部分で進める
一気に全てを終わらせようとするのはかなり大変です。普段の施策開発も平行で行いながら、脱却作業も進めることになるため、そのように進めるとなかなか終わりが見えなかったり、施策開発で行った実装がReactNative脱却を行うブランチと競合してしまったりと、余計な作業が発生することにも繋がってしまいます。いわゆるビッグバンリリースにまつわる問題ですね。こういった問題を避けるために
- 影響範囲は小さく
- 作業ブランチの寿命は短く
というルールを守って細かい単位で作業を進めていく必要があります。理想的にはコンポーネント単位で進められるのがベターではあり、今回の用件でも最初はそれを検討していたのですが、ReactNativeの一部分をネイティブで実装するのはかなり余計な手間が発生したり、サイズ計算周りがうまく動作しなかったりと課題が山積みだったため、ネイティブとReactNativeが混在しないように画面単位で作業を進めていきました。
なお、脱却後の画面は前述の通り当時評判の良かったSwiftUIを利用して実装しています。SwiftUIを取り扱うにあたっての諸々の事例はいくつか本ブログでも取り扱っているので、気になる方はこちらも参照してください。
5. ReactNativeを削除
画面単位でReactNativeを脱却していき、全ての画面が置き換わったら、最後にReactNative自体の削除を行います。ReactNativeへの依存を削除し、利用されていないJavaScriptコードの削除を行いました。 ここまでをやりきり、最盛期には1万行以上存在していたReactNative上のコードを全て削除することができました。
脱却して得られたものはあったのか
RettyではReactNativeを脱却し、現状の環境に即した技術を選定して移行することで
- 施策展開をより早く行えるように
- ビルド時間を三割ほど削減
- 運用コスト低減
- 基本的なiOS開発の知識だけで誰でも全機能を対応可能に
- クラッシュフリーレートが向上し、安定したアプリを提供可能に
- 属人化してしまっている領域が減ったため、バス係数が向上した
というメリットを得られました。これらは概ねReactNativeを運用していた際に感じていた課題であり想定通りではありました。しかし、元々置き換え前の状態では丸一日かかっていたような作業が半日以内ほどで完了したりするなど、得られた効果は予想以上でした。これは置き換え先として利用したSwiftUI導入による部分も大きかったと思います。
その他、これまで原因不明とされていたクラッシュが解消され、アプリの動作の安定性向上にも寄与しました。アプリ自体の品質を向上させつつ大変効率的に実装作業を行えるようになり、仕事が捗りとても良い結果を得ることができました 🚀
おわりに
Rettyでは過去の時点ではその当時の制約に基づいてベターであると判断した上でReactNativeを採用し、それを活用していましたが、採用を決定した時点から時間が経過し環境が変化した事により、多くの課題を感じることとなったため、これを辞めていく事にしました。 ReactNative自体は素晴らしい技術でしたが、残念ながらRettyの現状には適合しないものになってしまっていました。 現状の状態から考えてあるべき技術を選定し、計画を立て、ReactNativeからの脱却を実行することで、チーム全体の生産性の向上を達成することができました。
本記事ではReactNativeを事例として取り上げましたが、こう言った過去には良い選択だったものが果たして今でも本当にそうなのだろうか?と思うものは多かれ少なかれどのようなチームにも存在すると思います。そういった過去の選択を覆し、現状に即したより良い選択へ進んでいく道を選ぶ際に、この記事の内容がお役に立てれば幸いです。
最後にこれまでお世話になったReactNativeにお別れと感謝の言葉を持って締めさせていただきます。
さよならReactNative, ありがとうReactNative
*1:根拠についてはこちらのissueで議論がなされていました https://github.com/facebook/react-native/issues/12778