フロントエンドを Vue.js から React にリプレイスしたお話 (前編)

はじめての方、はじめまして。久しぶりの方、お久しぶりです。 イノベーションセンターの何縫ねの。(@nenoMake)です。 普段の業務ではソフトウェアエンジニアとして Node-AI という WEB アプリケーションの開発をしています。 パブリックな活動としては、好きな言語である C# 関係の OSS 開発や技術ブログの投稿、登壇などをしています。 ですが、今回は C# ではなくフロントエンドのお話をします...!

この記事では今まで Vue.js 2.x で開発されていた Node-AI の WEB フロントを完全に捨て去りReact にリプレイスしたお話をつらつらとしていきます。 まずは前編ということで、リプレイスプロジェクト発足時の課題感からはじめ、プロジェクトの進め方や選定技術などについてお話しします。 後編には内部の設計などのより技術的なお話をしたいと思います。では前編スタート...!

Node-AI とは

Node-AI はノーコードでAIモデルを作成できる WEB アプリケーションです。以下の図のようにカードを直感的につなげるだけで時系列データの前処理からAIモデルの学習・評価までの一連のパイプラインを作成・実行できるようなものとなっています。

各データ処理(例えば正規化やデータ分割など)がそれぞれカードとして表現されており、それを繋げてパイプラインを作成します。 視覚的に処理の流れを追いやすく、データ分析者の(往々にしてカオスな)コードを解読する行為から解放されるため、業務が捗るでしょう。 また複数人での同時作業にも対応しているため、コラボレーションが活性化し、ステークホルダーを巻き込んだ効率的なデータ分析やAIモデル開発ができるようになるはずです。 現在 β 版として公開しているので、気になる方は是非使ってみてください。

今までのフロントエンドの課題

すこし歴史的経緯から。 私は新卒2年目なのですが、その入社より遥か以前、Node-AI の開発プロジェクトが爆誕したのは機械学習エンジニアたちがいろいろ案件を回していく中での課題感からだったそうです。 なのでいざ Node-AI というアプリケーションを開発するぞ!となった時、発案者たちである機械学習エンジニア達がアサインされるのは言うに及ばずなのですが、さてアプリケーションエンジニアないし WEB 系エンジニアはどう調達するか。 課題感が発生したチームには、そのような人材はいなかったそうです。機械学習専門のチームだからそれはそうという感じ。 このような場合、社内から適切な人間を引っ張ってきてどうにか解決するのがベストなわけですが、残念ながらそうはいかなかったそうです。 ではどうするか。そう、外注です。

そんなこんなでプロジェクト発足当初は Node-AI のフロントエンドは完全に外注していたそうです。 発注元にコード書ける人間がいない中で外注するとどうなるかは火を見るよりも明らかで、以下のような状態に陥ってしまいました。

  • 発注側にコードの品質を管理できる人間がいないため、コードが徐々にカオスへ
    • 指示された機能さえ出来てればいい、の積み重ね
  • コードがカオスになるにつれ、少しの修正に多くの時間と金がかかるように

このような状態が続いたわけですが、とあるタイミングで「全部内製開発するぞ!」という事になり、弊チームの人間がフロントエンドの開発をするようにもなったそうです。 内製開発に倒したはとても良い事だと思うのですが、結局今までのカオスの上に増改築をするような状況となるので、フロントエンドの機能開発に時間がかかるのなんの。

この時、フロントエンドが抱えていた技術的な課題は以下のようなものでした。

  • 問題ある DOM 構造や CSS
    • 手を加えると意図しない箇所の描画が壊れたりする砂上の楼閣
  • 問題あるコンポーネント設計
    • 単一責務になっていなかった
    • 結果として生まれる神コンポーネント
      • 具体的には 3000 行を超える SFC (.vue ファイル) が存在していた
    • むやみに複雑な props と emit のスパゲッティ
  • 厳しい開発体験
    • Vue.js 2.x の低い TypeScript ビリティ
    • 型が分からず、動かしてみないとデータ構造が分からないケース多数
    • にも関わらず、デバッガが使えない
    • VSCode の intellisense や Find All References 等の機能が効かない
  • 迫る Vue.js 2.x の EoL

この技術的な課題の結果として、度々ユーザからリクエストを受けるフロントエンドマターな機能が実装できないという問題や、デザイナーと開発者との連携、例えば Figma で提示された CSS が使えない等の問題も生じていました。

という事で、これらすべての問題を解消するため、フロントエンドをリプレイスするプロジェクトが動きました。リファクタリングじゃなくてリプレイスですからね、すべての解消を狙って然るべきでしょう...!

そしてただのリプレイスじゃねぇぞ、ド級のリプレイスだ...! という事で、度々ユーザからリクエストを受けていた機能もこれを機会に実装しちゃうぞ!という形で進める事となりました。 GUI アプリケーションの開発を組んだ事ある開発者の方はいろいろ分かると思いますが、最初から考慮していないと厳しいものはいろいろありますよね。 後から入れるとコストが大爆発するやつ。

リプレイスプロジェクトの進め方

まず前提として、現在のNode-AI はスクラムを用いたアジャイル開発をしていて、開発チームはフロントエンドチーム、バックエンドチーム、インフラチームといったような技術領域での分割をしていません。 フロントエンドはできるがバックエンドは全くできない、あるいはバックエンドはできるがフロントエンドは全く出来ない、みたいなエンジニアは現在では弊チームに存在しません(採用を頑張っている事が伺えますね)。 まぁ、もちろん得手不得手はありますし、専門領域も異なるのですけどね。

そしてこのプロジェクトは主に2つのフェーズが存在しました。 それぞれどのような感じであったかを書いていきます。 予め断っておくと、いわゆる設計工程と開発工程とは異なったものです。

フェーズ1

期間としては 2022年6月~10月 です。

この期間は開発チームから私含めた4人を切り出し、主に以下のような事を行っていました。主には技術的な下地を作っていたといえるでしょう。

  1. 既存機能の整理
  2. 技術選定のためのサーベイ
  3. 不確実性の高い箇所についての設計やプロトタイプ実装
  4. 各既存機能の再実装にかかるコストの見積もり

技術選定の内容としては Vue.js 3.x/Nuxt.js, React/Next.js のどちらで行く?から始まり、UI ライブラリは?グラフライブラリは?とかとか。選定技術については後述します。

また技術的に不確実性が高い箇所については、実実装に入る前のこのフェーズで実際に手を動かしていろいろ試していました。 たとえば Node-AI のキャンバス画面 (カードを配置する画面) は素朴に DOM をくみ上げればいいというわけでもないので、その描画をどのように実現するか、具体的には html の <canvas> で行くのか <svg> で行くのかとか、フルスクラッチするかライブラリに乗っかるか等です。 他にも今までのフィードバックから需要が高い事は分かっているが今まで実装できなかった undo/redo や 複数人でのリアルタイム共同編集機能といった、さまざまな要求を実現するための技術的な不確実性を解消していました。 一個人のエンジニアとしては、とりあえず実装してみてそれをベースに開発メンバで頭突き合わせてあーだこーだと議論して、不確実性の解消し、完成度を高めていくというこのフェーズの活動はめちゃくちゃ楽しかったです。

そしてこれらの活動を通じて実装のおおよその感覚をつかみ、フェーズ2にて実装する全ての機能それぞれに実装コストの見積もりました。 とはいえ見積もり自体は大味ですが。

フェーズ2

期間としては 2022年11月~2023年12月 です。

まずはフェーズ1と同じ4人のエンジニアで実装をすすめ、2023年6月ごろから徐々に開発者の人数を増やし、12月にリプレイス完了!といった形です。

このフェーズの開発は Vue.js 2.x の EoL が 2023年12月であったことからも、とにかく開発速度が求められていました。 EoL までには何としてもリリースしたい。 そこでこのプロジェクトでは、これまで行っていた通常のスクラムとは一部異なる進め方をする事にしました。 これまでの進め方では、優先順位の高いプロダクトバックログアイテム(機能など)から順番に、複数の開発者で協力して開発してきました。 この進め方は必然的にチームの中で多くのコミュニケーションが発生するため、それが結果的に「コードをみんなのもの」にし、チームとしてのレジリエンスを高める事に寄与してくれていました。 しかし裏を返せば、コミュニケーションコストがそれなりにかかっているということですから、開発速度を最優先事項に据えた場合、最適ではなくなります。 そこでリプレイスプロジェクトでの開発においては「一人一殺」という標語の下に、特定の機能は特定の人が一気通貫で作るというスタイルで開発を進め、開発速度を高めていきました。 とはいえ当然レビューは行うので、実装者とレビュアーの間でのコードの共有はなされていましたし、誰かが悩んだりした際には随時メンバー間で相談や壁打ちは行っていたため(心理的安全性が確保されていると言えますね...!)、完全に一人しか知らないコード、というのはほぼないと思われます。 なお、インクリメントという名のアウトプットをステークホルダーにお見せしフィードバック貰ってプランニングして...、といったスクラム一般の一連の所作は今まで通りやっていました。

ところで一人一殺という進め方、技術的な視点で見ると、そのやり方で進めると実装取っ散らばらないか?と思ったりもするのではないでしょうか。 それについてはフェーズ1にて一定のレールを敷くことに成功し、基本的にはそれらのレールに沿って実装したため問題になりませんでした。 特に量産が必要なところ、例えばキャンバス上に配置されるカードなどのオブジェクトの操作や、各カード毎(=データ処理や機械学習に必要な処理毎)に対して作る必要があるカード詳細画面などは、それがよく機能していたと感じるところです。 また前述のとおり、2023年6月ごろから追加の開発者がこのプロジェクトに投入されたのですが、投入されたメンバーはいままで Vue.js で開発をしていたため、React には不慣れでした。 しかしながらメンバーそれぞれの React に対するモチベが高く優秀であった事に加え、コード上に一定のレールを敷くことが出来ていたため、スムーズにキャッチアップでき、最終的なゴールまで加速できたのではと思います。

その結果として2023年12月末、Vue.js 2.x の EoL というタイムリミットの前になんとかリリースできました、という感じです。 いやはやよかったよかった。

バックエンドにも手を入れる事を躊躇わない

フロントエンドのリプレイスは既存のフロントエンドの負債を解消する事と、今まで実現できなかった機能を実装する事を主だった目的としていました。 しかしそもそも、なぜ負債の解消をするのでしょうか?それは今後の継続的な機能開発を加速可能な状態にするためです。 せっかくリプレイスしても新機能の追加が大変なものが出来上がったのでは、コストを支払った意味がありません。

そういった視点で考えたとき、既存のフロントエンドが利用しているエンドポイントをそのまま利用する事がそれに寄与するのか?というのはリプレイスプロジェクトを進める上で当然議題にのります。 フロントエンドの実装を進める上でぶつかった既存のエンドポイントに関する課題は以下のようなものでした。

  • 複数のエンドポイントのレスポンスが闇鍋
  • いくつかのエンドポイントはレスポンスが極めて動的
    • エンドポイント単位で型を与えるのが困難

これは今後も新たに機能を追加する際、都度闇鍋 JSON に新機能由来のデータが追加され、それをデシリアライズした後に解釈するためのコードを加筆し続ける必要があるという事です。 これは継続的な開発及びバグ防止の観点から避けたいです。 そこでこの課題を解決するべく、バックエンド実装に対して以下の方策をとりました。

  • 新規に実装してもたいしたコストがかからないものについては新規実装
  • 新規実装にコストがかかりそうなものについても新規エンドポイント実装
    • ただし内部では BFF のような仕事をするレイヤを作成するだけに留め、ロジック自体は既存実装を使いまわす
    • これによりレスポンスを比較的シンプル化 & 強く型付け

もともとフロントエンドのリプレイスプロジェクトですから、コストをあまりかけないようにしました。 このようにバックエンドの実装も修正しながら進められた事は、フロントエンドだけでなくバックエンドも一定以上に書けるエンジニアでプロジェクトを進める事が出来た利点でしょう。

また上記の課題とは別軸に、以下に対応するためにもバックエンド実装は必ず通る道でした。

  • リプレイスで生まれる新機能向けバックエンド実装
    • リアルタイム通信系機能
    • ストレージ節約マイクロサービス

リアルタイム通信に纏わる機能は既存の実装にはほぼ無いものでしたから、必然的に新規実装する事になりました。 また Node-AI のキャンバス上に配置されたカードのデータ処理や機械学習の結果にまつわる生成物がバックエンド上では伴うのですが、フロントの新機能としてキャンバス内操作の undo/redo を実現すると、それらを即座に削除する訳にもいかなくなりました。 つまり定期的に不要になったそれらを片付けるマイクロサービスなどが必要でした。 このようなものもフロントエンドのリプレイスプロジェクトだからといって躊躇わずに実装しました。

選定技術

選定した技術はおおよそ以下のような感じです。

  • TypeScript
  • React/Next.js
  • Redux
  • Mantine
  • ECharts
  • SignalR
  • Tailwind CSS
  • Mock Service Worker
  • Storybook
  • React Testing Library
  • Playwright

もともと Vue.js 2.x を用いていた事から、Vue.js 3.x/Nuxt.js と React/Next.js のどちらを使うのかはプロジェクト発足時、当然議題にあがりました。 結果としては React/Next.js を選んでいるのですが、理由としては以下の通り。

  • TypeScript との親和性
  • 世界的な潮流
    • Vue.js 3.x が JSX を取り込んだことからも分かる通り。
  • メンバーのモチベ

個人的には React の「GUI は純粋関数として表現できる」という思想及び単方向データフローである点は極めて有益であると感じ、関数型コンポーネント + hooks 登場以降の React は大好きです。 もちろん実装上は全てのコンポーネントが純粋関数とはいかないのですが、Node-AI の実装では純粋なコンポーネントとそうでないところは徹底して分離する事で、読みやすさと保守性の向上に努めています。

またグラフライブラリには ECharts を利用しています。 Node-AI は時系列データを取り扱うアプリケーションなので、そこそこ大きいデータも問題なく描画できるグラフライブラリを選択する必要があります。 そこで10個弱のグラフライブラリでそれなりのサイズのデータを描画し、パフォーマンスバトルを行いつつ、GUI としての操作感や今後実現したいグラフが描画できるか等を検討しました。 結果的にパフォーマンス的にもグラフ表現の幅も広かった、ECharts を採用しました。

リプレイスを通して得たもの

  • 整理整頓された DOM 構造や CSS
  • 綺麗になったコンポーネント達
  • TypeScript による徹底した静的な型付け
  • Storybook による UI カタログ
  • 統一された開発環境

もともと課題になっていた、いわゆる汚いコードというやつは当然ながら全て払拭しました。

上記で「綺麗になったコンポーネント達」というふんわりした事を書いているのですが、ここでは以下のような事を徹底しました、という事です。

  • 単一責務
  • container と presenter の分離
  • message と service の分離
  • イベントハンドラ等の関数名による明示的な意味づけ

上2つについては特にいう事はないでしょう。 多くの人が頭で理解はしていても、なんやかんやで徹底されず、多くのコードではそうなっていないというだけで。

3つめの message と service の分離については、要はデータと処理の分離です。 データ指向プログラミングとも言いますね。 リプレイスされた TypeScript のコードの多くは純粋関数で出来ているため、これは自然と守られます。 とはいえそれが全てではなく、当然 class も使っています。 その場合でも、それらのインスタンスを「状態を持って振る舞うオブジェクト」として用いるのではなく、あくまで「immutable な message と service」といった形で分離し利用する事で、これを徹底してます。

そして最後のイベントハンドラ等の関数名による明示的な意味づけについてですが、これはたとえば useEffect の第一引数に処理をべた書きした無名関数を渡すのではなく、 コンポーネントの外で名前が与えられた関数を定義し、それを useEffect の引数内で用いるといったコードの書き方をしたという事です。 もう少し具体例をだすと onChange のような props があった時、そこに onChangeHandler みたいな情報量ゼロな関数オブジェクトを渡すのではなく、たとえば validateXxx のような何をする事を意図しているのか瞬時に判別できる名前のついた関数オブジェクトを渡すようにする、とかです。 こういった命名を積み重ねていくことで、初見でも渡されている関数が何を意図しているのかがすぐに分かります。 また同時に名前がついていることから、別の処理を追加しようとした際に心理的な抵抗感が生まれるため、1つのメソッドにさまざまな処理が追加され、結果的に単一責務からかけ離れた多重責務になってしまうといった状況が発生する事を防げます。 ラムダをべた書きしたり、情報量ゼロの名前を関数につけてしまった場合に発生しがちな複数の責務が押し寄せてくる事象も、小さな命名の積み重ねで防げると私は考えています。

そしてもう1つ、地味に大事な収穫の1つとして、統一された開発環境を提供できるようになりました。 現代の TypeScript によるフロントエンド開発する上で VSCode 上での開発体験は極めて重要です。 IntelliSense によるコード補間、Find All References や Go to Definition といったシンボルでコードを飛び回る機能、そしてデバッガ。 これらは生産性にダイレクトに響いてきます。せっかくゼロベースでやるのであれば、そのあたりは整備して全開発者にばら撒いてしまった方がなにかと良いです。 各々で開発環境をカスタムするのは止めませんが、標準的な開発環境は揃えた方が何かと幸せ。 という事で dev container で必要な設定や拡張が一式揃った開発環境を提供しています。 今までは標準的にはこれ使ってね、みたいのも無く、VSCode の便利機能を活用しきれていないケースが多々あったのですが、これにより全ての開発者はコストゼロで生産性の高い環境を手に入れられるようになりました。

まとめ

Node-AI のフロントエンドは Vue.js 2.x を捨て、React/Next.js に移行を果たしました。 この記事では主にプロジェクトの進め方や技術選定、結果として得られたものについてお話しました。 私個人としては入社してから殆どこのプロジェクトに付きっ切りでしたので、いろいろやり切った感があります。 頑張った偉い。 とはいえ React にリプレイスされた Node-AI のフロントエンドはこれで終わりではなく、むしろこれからの機能追加がドシドシ行われるフェーズこそが本番であり、設計したものの真価が問われるので、気が引き締まりますね。

そして EoL を迎えた Vue.js 2.x を捨てなければならない案件は各所で発生している問題かと思いますので、この記事が読者の皆さまの役に立てば幸いです。

後編予告

前編であるこの記事では、主にプロジェクトの進め方等について書いていきました。後編ではもうちょい技術よりのお話をする予定です。今のところ、以下の内容の設計だったり開発フロー等についてを予定しています。それではまた後編で会いましょう...!

  • ライブラリ依存無しでキャンバスを SVG で表現している話
  • キャンバスのリアルタイム共同編集機能の話
© NTT Communications Corporation All Rights Reserved.