フロントエンドのディレクトリ設計を移行した話 | Atomic Design → Feature
はじめに
この記事は、業務でフロントエンドのディレクトリ設計思想を変更した際の作業をまとめた記事です📕
それなりの規模のプロジェクトでの移行作業のため、通期のリリースサイクルに影響が出にくいようにリファクタリングを進めてきました。そこでの進め方や感想も含めてお伝えできればと思います。
前提
6年ほど運用しているReact Nativeのプロジェクトでの移行作業のお話です。
従来のディレクトリ設計思想はAtomic Designを採用していました。しかし、organismsのコンポーネントが300個近くにも及び、プロジェクトの規模が大きくなるにつれて様々な課題が浮き彫りになりました。これを機に設計思想の移行を決めました。
Layer型のAtomic DesignからFeature型のディレクトリ構造に移行していこうと思います😌
参考: ソフトウェアアーキテクチャの基礎
なぜ移行したか
なぜ移行することにしたかは大きく分けて2つ理由があります。
- Atomic Designの定義の解釈が人それぞれで階層分けが難しい
- 依存管理が難しくロジックがコンポーネントに分散されているため影響範囲の把握が難しい
Atomic Design の定義の解釈が人それぞれで階層分けが難しい
(https://bradfrost.com/blog/post/extending-atomic-design/ から引用)
Atomic Designを使用したことがある人ならmoleculesの使い方で一度は悩んだことがあるかと思います。moleculesは状態はなるべく持たず、単独のatomsよりも具体的な機能を提供するコンポーネントです。しかしorganismsとの差別化がとても難しく、実装中に
「organismsとmoleculesどちらにコンポーネント作ったらいいのかな?」
と悩むこともあり、人によって定義が異なってしまっていました。そのため、いまいちmoleculesの使い方が統一されないままコンポーネントが多く作られたような状態になりやすいコンポーネント粒度でした。
※moleculesには150個ほどのコンポーネントがありました💧
依存管理が難しくロジックがコンポーネントに分散されている
Atomic DesignはLayer型なので「技術単位での分割」が基本になります。そしてorganismsには各サービスで使用される意味のある単位の塊で定義するため見た目の粒度だけでなく、ロジックの責務も明確にできるメリットがあります。
しかし規模が大きくなりコンポーネントの数が多くなってしまうと、画面やコンポーネントを新規で作成する際に、既存のロジックを持ったコンポーネントに気づかず重複してロジックを作成されることがありました。
その結果テストコードが重複したり、仕様の変更の際に抜け漏れてしまう懸念がありました。
Feature型に移行するメリット
- ディレクトリ単位で関心を分離するためコードリーディング、管理しやすい
- 影響範囲が明確にしやすく、変更容易性が高い
- 特定のページでしか使われないコンポーネントは、該当のページのディレクトリ内に配置するために横断的に利用する汎用的なコンポーネントがごちゃつかない
ディレクトリ単位で関心を分離するためコードリーディング、管理しやすい
各機能やページごとに関連するファイル(Components、Hooks、Providers)を1つのディレクトリ内でまとめます。規模が大きくなるにつれてHooksやlibsなどカスタムフックにしたファイルが多くなり複雑になってしまうケースが多くありますが、特定の機能に関連するすべてのコードを一箇所にまとめることで簡単に見つけることができます。
今回例に出しているのはHomeというドメインもとに例を出していますが、Home機能に関連するコンポーネントやロジックを修正する場合、/features/Home
ディレクトリ内のファイルのみを調査すれば良くなるので調査や修正がしやすいコードになります。
影響範囲が明確にしやすく、変更容易性が高い
変更時の影響範囲が明確になり、変更が容易になります。各機能が独立したディレクトリ内に閉じられているため、ある機能に加えた変更が他の機能に意図せず影響を与えるリスクが減り保守性が高いコードになります。
さらに、機能ごとにテストをグループ化することも容易になります。フロントエンドのテストとしてシナリオが重要な観点になりますが、Feature型では特定の機能に関連するテストの実行と管理がシンプルになります。
横断的に利用する汎用的なコンポーネントがごちゃつかない
特定のページや機能でのみ使用されるコンポーネントを、それぞれのディレクトリ内に配置します。一方で、複数の場所で再利用可能な汎用コンポーネントは、partsやlayoutなどの中央集約されたディレクトリに保持します。これにより、横断的に利用されるコンポーネントと特定の機能に固有のコンポーネントが明確に区別され、プロジェクトの構造が整理されます。
移行してみる
進め方
定期リリースに極力影響が出ないように進めていきたいので以下にように段階に分けて対応を進めていきました。
-
-
atoms → parts
- partsディレクトリを作成
- atomsディレクトリの中身を移動
-
organisms、molecules → layout
- layoutディレクトリを作成
- organisms, moleculesディレクトリの中身を移動
-
src/components/pages → src/pages
- src直下にpagesを先に移動した方がのちの作業でコンフリクトが起きにくいので先に移動
-
-
- featuresに移行完了した1つ大きいドメインで移行作業をする。(STEP3でメンバーに共有するための参考PRを作成する)
- featuresディレクトリを作成
- templatesディレクトリの中身を移動
- トップコンポーネントのindex.tsx以外のpagesディレクトリのを移動
- featuresに移行完了した1つ大きいドメインで移行作業をする。(STEP3でメンバーに共有するための参考PRを作成する)
-
- チームで新しい設計思想に慣れてもらうためにFeature型にリファクタリングするissueを作成しいつでも手が空いた際に着手できる状態にする
- 新規で画面を作成する際にはFeature型で書いてもらうように周知
- 事前共有と参考PR・ドキュメントを用意
STEP①とSTEP②(featuresディレクトリに移行までは完了したドメインを最低1つ作成するところまで)は一人でやった方が効率が良いので自分の方で行いました。
その後、STEP③ではチーム全体で進めていくことにしました。
新規の開発に影響が少ないように徐々に移行することを心がけ参考PRとドキュメントをもとにリファクタリングissueを作成し、進めています。
【before】 移行前のディレクトリ構造
移行前のディレクトリ構造は以下のようになっていました。
.
├── components/ # UIコンポーネント
│ ├── atoms/ # 最小単位のコンポーネント
│ │ └── Text/
│ │ ├── Text.tsx # テキストコンポーネント
│ │ └── Text.stories.tsx # Textコンポーネントのストーリー
│ │
│ ├── molecules/
│ │ └── Home/
│ │ ├── item.tsx # Home特有のitemコンポーネント
│ │ └── item.stories.tsx # itemコンポーネントのストーリー
│ │
│ ├── organisms/
│ │ └── Home/
│ │ ├── Assignment.tsx # 別画面でも利用されるAssignmentコンポーネント
│ │ ├── Assignment.stories.tsx # Assignmentコンポーネントのストーリー
│ │ ├── items.tsx # Home特有のitemsコンポーネント
│ │ └── items.stories.tsx # itemsコンポーネントのストーリー
│ │
│ └── templates/ # ページテンプレート
│ └── Home/
│ ├── Page.tsx # ページレイアウト
│ └── Page.stories.tsx # Pageコンポーネントのストーリー
│
├── hooks/ # アプリケーション全体のカスタムフック
│ |── app.tsx # 共通カスタムフック
│ └── useHome.tsx # Homeページ用のカスタムフック
│
├── providers/ # アプリケーション全体のコンテキストプロバイダー
│ |── app.tsx # 共通コンテキストプロバイダ
│ └── homeProvider.tsx # Homeページ用のコンテキストプロバイダー
│
└── pages/ # ページコンポーネント
└── Home/
├── __tests__/ # テストファイル
│ ├── index.test.tsx # Homeページのテスト
│ └── index.apollo.tsx # Apolloクライアントのテスト
├── Connected.tsx # APIアクセスやアクションハンドリング
├── Plain.tsx # データ整形やエラーハンドリング
└── index.tsx # ルーター登録、グローバルステート参照
【after】 移行後のディレクトリ構造
移行後のディレクトリ構造は以下のように改善しました💡
.
├── components/ # 共通コンポーネントのディレクトリ
│ ├── parts/ # 共通で利用するatomsのイメージ
│ │ ├── Text.tsx # テキストコンポーネント
│ │ └── Text.stories.tsx # テキストコンポーネントのストーリー
│ │
│ └── layout/ # 共通で利用するorganisms、moleculesのイメージ
│ ├── Assignment/ # Assignmentコンポーネント
│ │ ├── Assignment.tsx # 別画面でも利用されるAssignmentコンポーネント
│ │ └── Assignment.stories.tsx # Assignmentコンポーネントのストーリー
│ │
│ └── Order/ # Orderコンポーネント (別のドメイン)
│
├── features/ # 機能・ドメインごとのコンポーネント
│ └── Home/ # Home機能
│ ├── components/
│ │ ├── ListItem # ListItemコンポーネント群
│ │ │ ├── item.tsx # Home特有のitemコンポーネント
│ │ │ ├── item.stories.tsx # itemコンポーネントのストーリー
│ │ │ ├── items.tsx # Home特有のitemsコンポーネント
│ │ │ └── items.stories.tsx # itemsコンポーネントのストーリー
│ │ │
│ │ ├── Connected.tsx # APIアクセスやアクションハンドリング
│ │ ├── Plain.tsx # データ整形やエラーハンドリング
│ │ ├── Page.tsx # ページコンポーネント
│ │ └── Page.stories.tsx # ページコンポーネントのストーリー
│ │
│ ├── hooks/ # Home固有のカスタムフック
│ └── providers/ # Home固有のコンテキストプロバイダ
│
├── pages/ # ページのトップコンポーネント
│ └── Home/
│ └── index.tsx # Homeページのトップコンポーネント
│
├── hooks/ # アプリケーション全体のカスタムフック
│ └── app.tsx # 共通カスタムフック
│
└── providers/ # アプリケーション全体のコンテキストプロバイダー
└── app.tsx # 共通コンテキストプロバイダ
移行してみた感想と気づき
作業の進め方
他のメンバーが取りかかりやすいように「アサイン募集中」(これは弊社の概念ですが、誰でも自由に取り掛かれるようにラベルつける)にし、手が空いたタイミング等で対応していただいたので通常リリースを行いながら並行して進めることができました。
基本はスムーズに作業ができましたが、初期の「STEP① 汎用性の高いComponentsをまとめるディレクトリを作成する」の作業の際だけvscode(Visual Studio Code)のパス自動更新機能でorganisms、molecules → layout
に移行する際に、既存に同名のディレクトリが多数存在したため上書きができず大変でした😥(大きい移動作業は1人でやった方がいいと思います)
リリースへの影響
コンフリクトが起きやすい作業PRのマージは回数を減らすためにまとめたのが良かったかと思います👍
ディレクトリの移行作業は通期リリース対応とのコンフリクトが起きやすくコストがかかる作業のため、大きい移行はができるだけ個人で進めるのが良かったです。
メンバーで対応を進める際には1つのドメイン単位でのissueにタスクを切ればそこまでコンフリクトは起きにくかったです。1つ1つのタスク自体は簡単なものではあるので、1リリースに含めるissue数が増えてしまうのでテストの項目が多くなってしました🙇♂️今回の対応では基本ロジックは変えずパスの更新のみなのでテストは正常系のみでパスするように連携をしました。
ガイドライン化
今回の作業を進めるにあたって、事前にdocs/CODING_GUIDELINES.md
を作成しガイドラインを設けました。リポジトリ内に含め、気になった点があった場合、都度PRをもらってガイドラインを更新していくスタイルを採用。認識を擦り合わせながら対応を進めていけたと思います。
例)
横断的に使っているコンポーネントとFeaturesディレクトリー内で利用しているローカルのコンポーネントを1目でわかりやすいように相対パスを利用するように規約に追加。
// features/Home/components/Page.tsx
// 🙅♀️
import Dialog from 'src/features/Home/components/Work/Dialog';
import Items from 'src/features/Home/components/Work/Items';
import Footer from 'src/features/Home/components/Footer';
// 🙆♀️
import Dialog from '../Work/Dialog';
import Items from '../Work/Items';
import Footer from './Favorite';
新規で画面を作成する際には、思想に一致するコンポーネントを作りやすくするために hygen を使用してコンポーネントを自動生成できるようにしました。
Loaded templates: .hygen
added: src/features/NewPage/components/Connected.tsx
added: src/pages/NewPage/index.tsx
added: src/features/NewPage/components/Page.tsx
added: src/features/NewPage/components/Plain.tsx
added: src/features/NewPage/providers/NewPageProvider.tsx
added: src/features/NewPage/components/Page.stories.tsx
added: src/pages/NewPage/__tests__/index.test.tsx
added: src/features/NewPage/hooks/useQuery.tsx
最後に
Atomic Designを4年間使用してきた経験から、新しい設計思想に完全に慣れるにはまだ時間がかかると感じていますが、ページ・ディレクトリ単位でドメインが管理されている方が影響範囲が明確でわかりやすいと感じました。
規模が大きくなるにつれてどうしてもAtomic Designでは共通コンポーネントが紛れてしまいわかりにくくなるのが課題に感じていたのでそこが解消できるといいなと思っています🐶
Discussion