60万行を超えるフロントエンドのリアーキテクチャとCI戦略

こんにちは。こんばんは。 開発生産性の可視化・分析をサポートする Findy Team+ 開発のフロントエンドリードをしている @shoota です。

ファインディのフロントエンドでは多くのプロダクトでNxを用いたモノレポを構築しています。

tech.findy.co.jp

Findy Team+のフロントエンドもNxを採用し、各パッケージ間の依存関係の管理やライブラリのマイグレーションなどの恩恵を受けています。 なかでも強力なキャッシュ機構をベースとしたCIの高速化はなくてはならない存在となっています。

以下は、このブログの執筆時点での Findy Team+のフロントエンドリポジトリが実行するCIの統計情報です。

Team+ の CI実行

直近30日間でCIの実行回数は2500回、平均時間は7分です。 そしてNxの機能によって削減された実行時間はなんと167日分(約4000時間)にもなります(キャッシュはネットワークで共有されるので開発者のローカルでも有効です)。

しかし我々はこのような強力かつ高速なCIの環境を常に維持し続けていたわけではありません! 過去には平均のCI実行時間が13分以上かかっており、開発速度に大きな影響を及ぼしていました。

そこで今回はNxでのキャッシュ率を高め、CI時間を短縮するためのNxをベースとしたリアーキテクチャの実例についてご紹介したいと思います。

CI高速化のためのリアーキテクチャ

2024年のはじめ頃から、Team+のフロントエンドのCI時間は伸び続ける傾向が見え始めてきました。 プロダクトの肥大化と複雑化、そして開発メンバー数の急増によって、コードベースの増加速度も急成長をしていました。

「開発速度が上がるほどCI時間が伸びる」というアンチパターンを打開すべく、1年以上をかけてモノレポの内部構造の再設計をしました。

  • ページレイアウトモジュールの分離、ライブラリ化
  • 共有モジュールの分割
  • 翻訳アーキテクチャの刷新
  • UI コンポーネントのモジュール分割

今回はこの内容を紹介していきたいと思います。

ページレイアウトモジュールの分離、ライブラリ化

まず初めに目をつけたのがページレイアウトに関連するモジュールでした。Team+のサイドナビゲーションやヘッダーUIなどのページ全体のレイアウトに関わるコードです。 これらはモノレポの共有モジュールの中に配置されていましたが、その役割ゆえにほぼすべての画面で利用します。

レイアウトを変更することは稀ですが、共有モジュール内にレイアウトが所属することでモジュール全体に依存関係を持ちます。このときレイアウト以外のコードを変更をすると、レイアウトを利用しているものすべてが依存関係にあるため、全画面のテストが発生してしまいます。

そこでレイアウト関連のコードを別モジュールに分離して、依存関係が集中しないようにしました。

図1. レイアウトモジュール分離

共有モジュールの分割

次にレイアウトを分離した後の共有モジュールを更に分割し、変更の影響範囲を効率化しました。

共有モジュールの中には古くからあり、成熟しており、そしてテストの重いものがいくつかありました。このような古参モジュールは多くの機能で利用します。一方で、最近の開発ドメインに向けて作られた新米モジュールも共有モジュールに存在します。

両者が同居していると日々のコード変更頻度と重いテストが複合し、全体効率を下げる要因になっていたため、別居計画を進めました。

図2. 共有モジュールの別居

変更頻度の低い古参モジュールを分離することで、開発を進めているドメインに関連した比較的軽いテストのみがよく回るような構造にできました。

翻訳アーキテクチャの刷新

これまでは通常のモノレポ構造の再設計でしたが、CI時間に最も影響を及ぼしていたのはここで紹介する翻訳システムでした。

Findy Team+ は日本語・英語・韓国語の3言語に対応していますが、これらの翻訳は1つのモジュールで集約管理していました。 しかし機能開発の高速化・大規模化に伴って、翻訳対象の文言も急増し、さらに韓国語対応を追加したことで爆発的な依存関係の連鎖が生まれ、CI時間を圧迫してしまいました。

図3. 翻訳辞書の一元管理

画面が追加されるごとに翻訳辞書が増え、かつ翻訳モジュールの変更が発生するので、全画面のテストが実行されます。 次の画面が追加されるとその前に追加された画面もテスト対象になり、さらに実行時間が伸びます。この繰り返しによってCI時間が指数的に長くなっていきました。

そこで画面個別の辞書を画面モジュールに分散し、それぞれの画面の開発で発生するコード変更が画面モジュールのなかで閉じるようにしました。分散した辞書は画面表示前に動的に読み込んで表示できるような修正も行いました。

図4. 翻訳辞書の分散

画面がどんどん追加されても全画面のテストが起きなくなり、開発速度・並行数が上がってもCI時間が伸びない構造に生まれかわることができました。 共有モジュールの分割とは異なり、依存関係は変更せずにコード変更の発生箇所を移動することで、平均の実行時間を大きく縮めることができました。

UI コンポーネントのモジュール分割

上記の3点の改善によってCI時間は概ね安定して短縮できていましたが、更にTeam+を構成するUI コンポーネントの分離を進めました。

ボタンやフォームなどのプリミティブなUI(HTMLに近いもの)とTeam+特有のセマンティックなUIに分類し、依存の親子関係となるように分けました。 これも先述のようにボタンコンポーネントやフォームなどのプリミティブなUIは変更頻度が少なく、かつ依存度が高いことを見据えた分離です。

UI分離

変更頻度の高いモジュールとそれに依存したモジュールのテストが実行されるので、Nxのキャッシュ率を向上させることができました。

コードベースとCI時間の比較

CI高速化のためのリアーキテクチャの内容をご紹介してきましたが、タイトルの「60万行」にも触れたいと思います。 今回のリアーキテクチャを開始した時点では、Team+のコード量は次のようになっていました。

Language files blank comment code
TypeScript 5422 42462 13430 353578
JSON 349 0 0 51694
以下略
SUM: 6212 46170 17230 419602

TypeScriptのコードとして約35万行、翻訳辞書を定義しているJSONを合わせると約41万行です。 これらのコードの平均のCI実行時間が13分以上かかっていました。

そして冒頭にも記載したとおり、現在では平均のCI実行時間が7分程度になっています。この改善の間にも多くの機能リリースをしてきました。 こちらが現在のTeam+のコード量です。

Language files blank comment code
TypeScript 9171 68280 22279 552539
JSON 672 6 0 66888
以下略
SUM: 10453 69658 22424 635517

TypeScriptのコードとして約55万行、翻訳辞書を定義しているJSONを合わせると約62万行です。

つまりコードベースとして約1.5倍の成長をしながら、CI時間を53%程度まで削減できたのです!!

まとめ

ここまで拝読いただきありがとうございます。

今回は Findy Team+の開発を支えるCIとその改善内容についてご紹介いたしました。内容を以下3点にまとめてます。

  • Nxベースのモノレポアーキテクチャを再設計することで変更に依存しない高速なCI環境をつくることができる。
  • モノレポ間の依存関係と、開発現場のコード変更の傾向をつかむことがCI速度改善のカギになる。
  • CI速度の改善は規模の大きなコード、組織にとって不可欠な改善のひとつと言える。

なお今回のリアーキテクチャでいちばん大変だったのは、変更対象のCI時間が基本的に長いことでした。 CI時間が長いモジュールを修正し続けているので当然なのですが、効果がでるまでの我慢期間が長かったことは胸に刻みたいと思います。


現在、ファインディでは一緒に働くメンバーを募集中です。 興味がある方はこちらから herp.careers