🛠️

フロントエンドのリアーキテクトでチーム単位の改善サイクルを促進する

に公開

1 はじめに

弊社では、昨年9月からドメイン単位でチームが責任を持つ形へのフロントエンドリアーキテクトに取り組んでいます。これまでは複数のチームが一つのモノリシックなコードベースを共有していたため、変更の影響範囲が把握しづらく、各チームが自律的に改善を進めることが困難でした。このような課題に対して、日々の機能開発と並行して少しずつコードの分離を進めています。同じような課題を抱えるチームの参考になれば幸いです!

2 背景と課題

弊社メインプロダクトのフロントエンドは React を基盤に約 6 年間運用されており、コードベースは初期の延長線上でモノリシックな構成のままでした。その一方で、現在の開発組織はサービスの成長に合わせてドメインごとのチームへと分かれており、各チームが独立して機能開発の意思決定や開発を行うことが多くなってきました。

分離前の src ディレクトリは次のような構成です(わかりやすさのために単純化しています)。pages フォルダでは、ある程度のドメイン単位でディレクトリが分割されており、各ディレクトリ内にページコンポーネントとそれに関連するUIコンポーネント、ロジックが置かれていますが、ドメインの境界が曖昧で複数のチームが触るディレクトリもありました。

/
└─ src/
   ├─ components/   # 共通UIコンポーネント
   ├─ lib/          # 共通ロジック
   ├─ pages/        # ページ単位の画面コンポーネント
   │  ├─ domain_1/
   │  │  ├─ index.page.tsx
   │  │  ├─ hoge_component.tsx
   │  │  ├─ use_hoge.ts
   │  │  └─ ....
   │  ├─ domain_2/
   │  ├─ domain_3/
   │  └─ ...
   ├─ App.tsx
   ├─ router.tsx
   └─ etc...

このようなコードベースに対して、複数のチームで開発を行うことで次のような課題感がありました。

  • チームを跨いでコードが密に依存している
    • 特定の機能にバグが発生した場合、影響範囲が特定し辛く、迅速な対応が難しい
    • 責任範囲が曖昧なため、バグ発生時に担当チームを判断しにくい
  • 各チーム固有の挑戦(技術検証や設計の実験)を行いたくても、他チームに影響が出る懸念から手を出しづらい
  • 依存関係が複雑なファイルが散在し、使われ方を追うだけでも認知コストが高い

これらの課題を解決すべく、コード分離を進めていきました。

3 コード分離の実施

以下のステップでコード分離を進めていきました。

  1. コード分離の意義について整理と共有
  2. コード分離方針の作成
  3. 特定のチームでコードの移行を実施
  4. 各チームでのコード移行

弊社では、四半期ごとにチーム横断の少人数チームを組成し、テーマとして設定した課題に取り組む横断ワーキンググループ(以下、横断 WG)という枠組みがあります。上記のステップ1~3は、この横断 WGを活用しました。少人数で方針の作成からコード分離手順の作成を集中的に行い、それらを全体共有することで、各チームが普段の機能開発と並行してコード分離を進められるようにしました。

3-1 コード分離の意義について整理と共有

まずは、現状の課題感、コード分離の意義を整理し、関係者に共有しました。これにより関係者間でコード分離の重要性について共通認識を持つことができました。また、この段階で各エンジニアからフィードバックを得ることで、以降のステップの参考にすることができました。

3-2 コード分離方針の作成

方針の作成

次にコード分離方針を作成しました。いくつかの案が出た中で、今の課題感を解決でき、現状の組織体制にもマッチする「チームがオーナーになるドメイン群ごとにコードをまとめる」ことを採用しました。
この方針の下、src/packs(*)という新設したディレクトリ配下へ各チームの領域を切り出す計画を立てました。この方針の主なルールは次のとおりです。

  • src/packs/<team> をチームのホームディレクトリとし、その配下でページコンポーネントやロジックを完結させる
    • src/packs/<team> 内の構成や設計は各チームで好きに決めることができる
  • src/pages には各ページのエントリーポイント(例: *.page.tsx)だけを残し、実装は packs から import する
  • ドメイン固有のコンポーネントや ロジック は packs に移し、src/componentssrc/lib など共通レイヤーには本当に共有したいものだけを置く
  • packsから外部にエクスポートするものはindex.tsに集約し、他ディレクトリからの import は index 経由に限定する

(*)packsというディレクトリ名は一般的ではありませんが、弊社バックエンドでも進んでいるモジューラモノリス化で使われているディレクトリ名であり、社内で浸透していたため採用しました。

ファイル移動の判断基準は下表のように整理しました。

対象ファイル チームがオーナーになる場合 チームがオーナーになれない場合
特定ページでのみ使われるもの src/packs/*/ src/pages/
複数ページで使われるコンポーネント src/packs/*/(原則管理チームを決める) src/components/
複数ページで使われるその他ファイル src/packs/*/(原則管理チームを決める) src/lib/ など

2 チーム以上で利用するファイルは、どのチームが責任を持つかを話し合って決め、決めたチームの packs に配置します。そして、運用してみて合わなければリファクタリングで調整します。また、どのチームもオーナーにならないようなドメインはファイル移動させず置いておく、という方針にしました。

ディレクトリ構成例

/
└─ src/
   ├─ packs
   │  ├─ team-a
   │  │  ├─ domain_1    # domain_1のコンポーネントやロジックを集約
   │  │  │  ├─ hoge_component.tsx
   │  │  │  ├─ use_hoge.ts
   │  │  │  └─ ...
   │  │  ├─ domain_2
   │  │  ├─ ...
   │  │  └─ index.ts    # packs/team-aからexportするものを集約
   │  ├─ team-b
   │  ├─ team-c
   │  └─ ...
   ├─ lib/
   ├─ components/
   ├─ pages/
   │  ├─ domain_1/
   │  │  └─ index.page.tsx #実装はteam-aのpacksからimportする
   │  ├─ domain_2/
   │  ├─ domain_3/
   │  └─ ...
   ├─ App.tsx
   ├─ router.tsx
   └─ etc...

eslint-plugin-importのno-restricted-pathsルールを使って、ファイル間の依存関係ルールを作成

また、リアーキテクトに並行して、以下のような各フォルダ間の依存関係のルールも作成し、eslint で warning が出るようにしました。これにより、ファイル間の依存関係が明確になり、コードの可読性向上を図りました。

依存関係ルールの例

  • src/pages以外からsrc/packsへの依存は禁止
  • src/packsからsrc/pagesへの依存は禁止
  • src/pagesからsrc/packsへ依存する場合、index.ts 経由の依存のみ許容する
  • src/packsから共通モジュール(src/componentssrc/libなどのルートディレクトリ)への参照は許可

期待するメリット

コードを分離することで以下のメリットを享受でき、前述の課題を解決できるのではないかと考えました。

  • 各チームがオーナーになるコードが明確になるので、自分たちのコードによりオーナーシップを持つことができる
  • 各コードに対して責任を持つチームが明確になるため、バグ発生時の原因箇所を迅速に特定し、早期対応が可能になる
  • packs 内で GraphQL 設定やディレクトリ構成を自由に試せるため、チーム単位で技術的な改善サイクルを回しやすい
  • コードの可読性・保守性向上
    • 外部に公開する API を index.ts に揃えることで依存関係が整理され、チーム外からの利用箇所が把握しやすくなる
    • 決まった単位でコードが分割されていることで、新メンバーのキャッチアップ速度向上やコードレビューの効率化が期待できる

懸念点

メリットの一方で、以下の懸念点も想定されました。

  1. 移行が中途半端に終わると、ディレクトリ構成が汚れてしまう
  2. チーム変更時にディレクトリ構成の変更が必要になる

1つ目の懸念については、移行が想定より進まなかった場合の切り戻しチェックポイントを設定し、中途半端な状態を避けるよう実施することにしました。
2つ目の懸念については、弊社の開発組織はドメイン軸でチームを構成しているため、ドメイン単位でのコード分離はチームの増減があっても破綻しにくいと考えました。また、チーム変更に伴うディレクトリ構成の変更は主にファイル移動とimport/exportの書き換えであり、移行コストが比較的低いことから、移行を進めることにしました。

3-3 特定のチームでコードの移行を実施

方針を作成したあとは、特定のチームでコードの移行を実施しました。コード移行時のtips をまとめた移行手順を作成しながら実施することで、他のチームでも移行がしやすくなるようにしました。

3-4 各チームでのコード移行

特定のチームでの移行作業が終わった後、移行手順をもとに他チームへのレクチャーを実施し、各チーム内でコードの移行をお願いしました。コンフリクトなど起きそうな場合は事前に影響範囲を話し合うなど、コミュニケーションをとりながら移行を進めています。

4 移行の現状とこれから

現在、チームがオーナーになれるドメインとしては6~7割程度の移行が完了しています。移行が想定より進まなかった場合の切り戻しチェックポイントを設定していましたが、各チームが主体的に動いてくれたおかげで、ロールバックせずに移行を続けられています。

まだ移行途中ですが、チームがオーナーになるドメイン群ごとにコードを分離したことで、次のような効果を感じています。

  • チーム単位での、ディレクトリ構成ルールの作成、 GraphQL の設定など各チームが packs 内で独立して改善できるようになった
  • ファイルの依存関係が整理されたことでコードの認知コストが下がった
  • デッドコードなどの不要なコードを整理できた
  • 同じファイルを複数チームで編集する機会が減り、日々のコンフリクトが減少した

もちろん移行の難しさもあります。例えば、複数チームが関わるドメインの境界線は今も悩ましく、どちらの packs に置くべきか相談しながら慎重に進めています。
日々の機能開発に並行してコード分離を進めるのは大変ですが、チーム間の定期的な進捗や課題感を共有しながら、コード移行の完了を目指したいと思います。

5 参考資料

コード分離方針作成時に以下の資料を参考にさせていただきました。ありがとうございます。

https://blog.cybozu.io/entry/2022/02/04/171154

https://zenn.dev/cybozu_frontend/articles/module-share-pattern

https://buildersbox.corp-sansan.com/entry/2024/11/20/120000

Linc'well, inc.

Discussion