Layer型からFeature型へ 〜段階的に進めるディレクトリ構成の移行〜
はじめに
こんにちは!Recustomer の Ryohei です!
私はこれまで業務・個人開発でも、Atomic Design に基づいた Layer 型のディレクトリ構成を採用してきました。しかし、機能が増えていくにつれて、関連ファイルの散在や影響範囲の把握に課題を感じるようになりました。
この記事では、Layer 型で感じた課題を整理し、Feature 型のメリットを紹介します。また、弊社で取り組んでいる Feature 型への段階的な移行についても紹介します。
Layer 型と Feature 型とは
フロントエンドのディレクトリ構成には、大きく分けて 2 つのアプローチがあります。
Layer 型
技術的な役割やコンポーネントの粒度でディレクトリを分割する方法です。代表的な例として Atomic Design があります。
Atomic Design では、コンポーネントを以下の 5 つの階層に分類します。
- Atoms: ボタン、入力フィールドなど最小単位の UI 部品
- Molecules: Atoms を組み合わせた小さな機能単位(検索フォームなど)
- Organisms: Molecules を組み合わせた大きな機能単位(ヘッダー、サイドバーなど)
- Templates: ページのレイアウト構造
- Pages: 実際のコンテンツを流し込んだ画面
src/
├── components/
│ ├── atoms/ # Button, Input, Icon など
│ ├── molecules/ # SearchForm, UserCard など
│ ├── organisms/ # Header, Sidebar など
│ └── templates/ # PageLayout など
├── pages/ # UserListPage など
├── hooks/
├── utils/
└── api/
Feature 型
機能(ドメイン)単位でディレクトリを分割する方法です。代表的な例として bulletproof-react があります。
src/
├── features/ # 機能ごとのモジュール
│ ├── users/
│ │ ├── api/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── types/
│ └── posts/
│ ├── api/
│ ├── components/
│ ├── hooks/
│ └── types/
ポイントは features/ 配下に機能ごとのディレクトリを作り、その中に関連するコンポーネント、フック、API 呼び出しなどをまとめることです。
Layer 型で感じた課題
Layer 型を使っていて、以下のような場面で「少し大変だな」と感じることがありました。
1. 影響範囲の特定が難しい
例えば、ユーザー一覧画面を実装した場合を考えてみます。
src/
├── components/
│ ├── atoms/
│ │ └── Avatar.tsx # ユーザーアバター
│ ├── molecules/
│ │ └── UserCard.tsx # ユーザーカード
│ └── organisms/
│ └── UserTable.tsx # ユーザーテーブル
├── pages/
│ └── UserListPage.tsx # ページコンポーネント
├── hooks/
│ └── useUsers.ts # ユーザー取得フック
└── api/
└── fetchUsers.ts # API呼び出し
ユーザー一覧機能に関するコードが atoms/、molecules/、organisms/、pages/、hooks/、api/ と複数のディレクトリに分散しています。
例えば「ユーザー一覧のテーブル表示にバグがある」となったとき、以下のような疑問が浮かびます。
- データはどの API から取得しているのか?
- 取得したデータの加工はどこで行っているのか?
- テーブル表示しているのはどこなのか?
- UserCard は atoms? molecules? organisms?
これらを調べるために、複数のディレクトリを探し回る必要があります。
さらに厄介なのは、特定したコンポーネントが他の画面からもインポートされているケースです。
チーム内でコンポーネントの責務に関するルールが曖昧だと、本来 UI だけを担うはずのコンポーネントにロジックが混入していることもあります。そうなると「このコンポーネントを修正したら、どこに影響するのか?」の把握がさらに難しくなります。
2. コンポーネントの粒度が曖昧になりやすい
Atomic Design では atoms、molecules、organisms、templates、pages の 5 階層に分類しますが、「このコンポーネントはどの階層に属するのか?」の判断基準は人によって異なります。
例えば、以下のような UserCard コンポーネントを作ったとします。
const UserCard = ({ user }: { user: User }) => {
return (
<div className="user-card">
<Avatar src={user.avatar} />
<Text>{user.name}</Text>
<Button onClick={() => follow(user.id)}>フォロー</Button>
</div>
);
};
このコンポーネントはどこに置くべきでしょうか?
- A さんの意見: 「atoms(Avatar, Text, Button)を組み合わせただけだから molecules でしょ」
- B さんの意見: 「フォロー機能というビジネスロジックを持っているから organisms じゃない?」
チーム内で明確なルールが決まっていればよいですが、実際には判断が分かれることが多いです。
結果として、分類に悩む時間が増えたり、似たようなコンポーネントが別の階層に作られたりして、コンポーネント数だけが膨らんでいくことがあります。
Feature 型のメリット
Feature 型では、機能に関連するファイルが 1 つのディレクトリにまとまります。
src/
├── features/
│ └── userList/
│ ├── UserListPage.tsx
│ ├── UserCard.tsx
│ ├── UserTable.tsx
│ ├── useUsers.ts
│ └── fetchUsers.ts
1. 読むべき箇所の見当がつけやすい
ユーザー一覧にバグがあるとき、Layer 型では components/、hooks/、api/ など複数のディレクトリからファイルを探す必要がありました。
Feature 型なら features/userList/ を見ればほぼ十分です。読まなくてもよい箇所と読むべき箇所の見当がつけやすいのが最大の特長です。
2. 疎結合・高凝集になり、影響範囲が狭く済む
機能単位でディレクトリが分かれているため、変更の影響範囲がその機能内に閉じやすくなります。
Layer 型では components/molecules/UserCard.tsx を修正すると、どの画面に影響するか調べる必要がありました。Feature 型では features/userList/UserCard.tsx はユーザー一覧専用なので、影響範囲が明確です。
このように「関連するファイルを近くに置く」考え方をコロケーション(Co-location) と呼びます。
3. Feature 型は粒度の問題を軽減しやすい
Layer 型の課題として「階層ごとの定義が曖昧」という点を挙げました。molecules と organisms の境界はどこか?という問いに明確な答えはなく、人によって判断が分かれます。
一方、Feature 型では「この機能に関連するものはここに置く」というシンプルな基準になります。「UserCard は atoms? molecules?」という抽象的な判断ではなく、「ユーザー一覧に関連するものは features/userList/ に置く」という具体的な判断になります。
粒度の問題が完全になくなるわけではありませんが、Layer 型と比べて判断に迷う場面は減ると感じています。
Layer 型と Feature 型は組み合わせられる
ここまで Layer 型の課題と Feature 型のメリットを書いてきましたが、「Layer 型はダメで Feature 型に完全移行すべき」ということを強調したいわけではありません。
Layer 型には UI コンポーネントの再利用性が高く、デザインシステムとの相性が良いというメリットがあります。最近では、両者を組み合わせたハイブリッドなアプローチが増えていると感じています。
以下の記事がとても参考になりました。
弊社での取り組み
弊社でもフロントエンド開発において Next.js を使っており、ディレクトリ構成として Layer 型を採用していました。
しかしプロダクトが大きくなるにつれ、この記事で挙げたような課題を感じるようになりました。チームで話し合い、Layer 型と Feature 型を組み合わせる形で段階的に移行していくことにしました。
なぜ「段階的」なのか
一気に移行するのは現実的ではありませんでした。
- ディレクトリ構成の変更はプロダクトの成長に直接繋がらない
- 大規模な改修になるため、他の開発と並行して進める必要がある
- フロントエンド専任ではなくバックエンドも担当するメンバーが多く、全員に浸透させるのが難しい
そこで、まずコーディング規約を整備し、段階的に進めることにしました。
まずはコロケーションから
最初から完璧な Feature 型のディレクトリ構成を目指すのではなく、まずは依存関係が複雑なもの、粒度が曖昧なものをページ側にまとめることから始めています。
Atomic Design で言うと molecules や organisms に相当する共通コンポーネントは、依存関係が複雑で影響範囲の把握が難しいことが多いです。これらを直接変更せず、ページ側にコピーしてページ固有の差分のみを加えます。
// 変更前(共通コンポーネントを参照)
import { PriceCard } from "components/molecules/PriceCard";
// 変更後(ページ側にコピーして参照)
import { PriceCard } from "./_components/PriceCard";
共通定数も同様に、必要なものをページ側にコピーしてページ固有の値に変更します。参照がなくなったら元のファイルは削除します。
この方法だとページ配下のファイル数が増えていきますが、それはひとまず置いておきます。まずは影響範囲をページ単位に閉じることを優先しています。ページ内の整理は後からでもできますが、依存関係が複雑なまま放置すると、どんどん手がつけられなくなるからです。
おわりに
今回伝えたかったことは、Layer 型と Feature 型のどちらが優れているという話ではありません。
Layer 型には UI コンポーネントの再利用性が高いというメリットがあり、Feature 型には機能単位での開発がしやすく影響範囲の把握が容易というメリットがあります。大切なのは、プロジェクトの規模やチームの状況に合わせて、両者のメリットを活かすことだと思います。
100% を目指すのではなく、今ある状況で最適な選択をしていくことが、ディレクトリ構成を再考する際には重要なのではないかと思う今日この頃。
参考
Discussion