React Nativeの負債化でアプリのSwift化が決定

大門弘明氏:それでは「React Nativeで書かれたアプリをSwiftで書き直しています」の発表を始めます。

まずは自己紹介をします。名前は大門と申します。2014年に新卒で合同会社DMM.comに入社して、iOSエンジニアとしてオンラインサロン事業部でお仕事をしています。

本日お話しすることですが、つらい気持ちの話と、アプリ設計の紹介を少ししようと思っています。僕にとってReact Nativeはつらい。

そもそも何を作っているのかというと、オンラインサロン専用コミュニティを会員が利用するためのiOSアプリを作っています。主な機能として、投稿や返信などのタイムラインを確認できたり、ライブ配信の視聴、あとはオーナーの投稿などのプッシュ通知が受け取れたりするアプリです。

まずこのアプリが抱えている課題のお話をします。1つ目、プロジェクトのアップデートがとてもしんどいです。iOSプロジェクトは、毎年のアップデートなど、Appleの方針に従って変更を続けていかなければなりません。これはどのプロジェクトでも同じです。

それに加えて、React Nativeのアップデートはかなりしんどいものでした。React Native自体が公式のものではないので、常に最新のXcodeに対応しているわけでもなく、一筋縄ではいきません。

そもそも僕はこのプロジェクトで初めてReact Nativeに触れたので、React Nativeがわからなくて、見慣れない構文だらけのJavaScriptを使うのがかなりつらかったです。

2つ目ですが、アップデートのしんどさも相まってReact Nativeが負債化していました。イケてるアプリを作るためにはiOS、Swift、JavaScript、React Nativeあたりのすべての深い理解が必要です。

当アプリは当初フロントエンドエンジニアが開発していたという背景があり、ほぼすべてがReact Nativeで作られています。iOSの知見が足らず、開発する際の足枷になるなどの課題はあったかと思うんですが、iOSエンジニアがいなかった当時としては最適な選択だったかなと思います。

しかし時は流れて、品質や開発スピードの改善のためにアプリへ投資して、iOSエンジニアにきちんと作ってもらうという事業判断になりました。今は僕も含めてiOSエンジニアが3名いるのですが、今度はJavaScriptやReact Nativeの知見不足が足かせになり、あまり得意分野を活かせず、開発のスピードが出ないということになってしまいました。

また、React Nativeで書かれてはいるものの、現在対応プラットフォームはiOSのみで、あまりReact Nativeのメリットを活かせていません。React Nativeをこのまま使い続けていくとデメリットの部分に苦しめられ続けることになりそうです。

ここまでの話から、開発の足枷になってしまったReact Nativeを脱却して、Swift化しようという話になりました。次はそのお話しをします。

その1 XcodeとReact Nativeを最新の状態にアップデートする

React NativeからSwiftの移行はつらいが無理ではない。ということで、我々がReact NativeのアプリをSwift化した流れを簡単に紹介します。

その1、XcodeとReact Nativeを最新の状態にアップデートします。最新のXcode、React Nativeでビルドできるように、まずはひたすらエラーや問題を取り除いていきます。

その中で、一部のライブラリがアップデートできない場合があります。具体的には仕様が変わっていて、アップデートすると動作が保証できないものや、そもそも最新のXcodeやReact Nativeに対応していないもの、対応中のプルリク状態でマージされていないものなどです。

こうしたライブラリはpatch-packageを利用して修正しました。patch-packageはnpmでインストールしたライブラリにパッチを当ててくれるツールです。JSファイルだけではなく、ネイティブの.mファイルや、podspecファイルなどもこれで修正できます。

例えばXcode12に対応するためには、React Native用ライブラリのネイティブ部分がその依存関係を書き換えなければいけないんですね。その対応が行われているライブラリと行われていないライブラリがありました。

行われていないライブラリや、あえて古いバージョンを利用したいライブラリなどは手動でパッチを当てて対応をしています。地道な作業を繰り返して、まずはXcodeとReact Nativeのバージョンを最新のものに追いつかせます。

その2 Swift化の基盤をつくるアーキテクチャ

次はいよいよSwift化の基盤を作っていきます。今日はそのおおまかな設計の話をします。DMMには事業部などの縦割りの組織のほかに、横串のエンジニア支援の部署があって、社内推奨のアーキテクチャがあります。

iOSの場合はVIPERライクの設計になっているので、こちらをベースにして社内での技術の流動化を図ろうと思いました。また、いずれ消えゆくReact Nativeの部分は1つのモジュールとして、そのほかの機能もモジュールで分割するマルチモジュールな設計にしました。

スライドには出ていませんが、リアクティブプログラミングのフレームワークはRxSwiftではなく、純正のCombineを採用しています。

この図は、とある画面の構成を簡単に表したものです。一番上のViewControllerはUIの更新とPresenterへのイベントの通知のみを担当します。イベント通知はスライド上ではディスパッチとなっている部分です。

Presenterは、受け取ったイベントをもとにInteracterにビジネスロジックを依頼して、結果に応じてViewControllerのあるべき状態を通知、もしくはルーターに画面遷移を依頼します。画面の構成は以上です。VIPERライクな画面の作り方になっています。

次にマルチモジュールの全体像です。Featureと呼ばれる部分が、画面や機能のまとまりごとにモジュールとして分割されます。React Nativeの部分も1つのFeatureとしてモジュール化しています。メインターゲットはいわゆるInfrastructure層の実装や、DI、各種Featureの呼び出しなどを行います。

先ほどの図では依存関係がちょっとわからないので、依存関係を簡単に表した図も用意しました。このCoreとCommonって何ぞや? と思った方がいるんじゃないかと思うので説明します。

Commonは、Swift本体の拡張などほかのプロジェクトにソースコードをそのままコピー&ペーストしても動くコードを置いています。例えば、文字列をスネークケースに変換するとか、Stringクラスの拡張とかですね。Coreは、アプリケーション固有のグローバルオブジェクトや、VIPER関連の基底クラスなどを置いています。

次に、React NativeとSwift間のやりとりをどのようにして行っているかを紹介します。これはReact Nativeを利用したアプリのユニークな部分だと思います。

アプリでは、1個1個の処理に対してネイティブ側にインターフェイスを持たせるのではなく、Swift側から見たときにイベントを受信するためのSubjectと、送信するためのメソッドのみを用意して、enumで定義したイベントオブジェクトが流れるPub/Subのような仕組みを作りました。

図にするとこんな感じです。真ん中のBridgeModuleはイベントの送受信以外の振る舞いを持たず、ハブのような役割に徹します。React Nativeからイベントを受信したい場合は、Swift側からBridgeModuleが提供しているSubjectを購読して、流れてきたイベントに応じて処理を実行します。

React Native側にメソッドを公開できるRCT_EXTERN_METHODというマクロを利用して、BridgeModuleのreceiveFromRNというメソッドをReact Native側から呼び出します。React Nativeにイベントを送信する場合はこれを呼び出すだけです。

React Native側は、初期化時にNativeEventEmitterというところにaddListenerしてイベントが流れてくるのを待つだけです。

このNativeEventEmitterは、React Nativeで標準で搭載されているネイティブとの連携の仕組みです。Swift側はnotifyToRNというメソッドにイベントを載せて呼び出して、React Nativeはそのイベントを受け取ります。

簡単にですがコードも載せてみました。Swift側はreceivedEventというSubjectとnotifyToRNメソッドだけを意識すれば、React Native側とやりとりが行えます。

Swiftファイルとは別にObjective-Cのファイルがあるのは、SwiftではRCT_EXTERN_METHODやRCT_EXTERN_MOJULEなどのマクロを利用できないからです。この部分だけObjective-Cで書いています。

その3 影響範囲の少ない末端の画面からSwift化する

推奨アーキテクチャと、マルチモジュールの設計と、Pub/Sub上でReact NativeとSwiftがメッセージをやりとりする仕組みが整ったら、あとは段階的にSwift化していきます。

具体的には影響範囲の少ない末端の画面からSwift化していきます。なぜならSwift化の途中で新規機能の開発などが入る可能性があるからです。そのためSwift化が完全に終わらなくてもアプリをリリースできる状態にしておく必要があります。

また、末端の画面以外からSwift化をしてしまうと、SwiftからReact Nativeに遷移して、またそこからSwiftの画面に遷移して、さらにそこからReact Nativeの画面に遷移するという状況が生まれてしまいます。その場合、単純にコントロールが難しくなり、依存関係も複雑になってしまいます。このような理由から末端の画面から順番に移行していきます。

末端の画面からSwift化しているイメージを説明します。この矢印は単に画面遷移のおおまかな順番を表しています。我々はこの青い部分を通称で第1弾と呼んでいます。先ほど紹介したSwift化の基盤部分と、どうしても先にSwift化しておかないとスムーズに移行が進められないと判断した画面などのことです。

これらを先にSwift化して、この時点でいったんリリースが可能な状態になります。そのあとに各末端画面や、中間の画面を順番にSwift化していくフェーズを第2弾、第3弾と呼んでいます。

ここまで、我々がどのような流れでReact NativeのアプリをSwift化しているのかを紹介しました。現状はまだリリースされていないですが、第1弾まで作業が完了しています。これから多くの課題が出てくるんじゃないかと思ってはいるので、機会があればそういったことも紹介したいと思います。

今日のまとめです。僕にとってReact Nativeはつらいという気持ちの表明と、Swift化の流れと設計を紹介しました。React NativeからSwiftへ移行するのは楽ではないですが、一つひとつの課題をクリアして着実に前へ進んでいます。

以上です。ご清聴ありがとうございました。