こんにちは、かつコーチです。
前回は「太らせないコンポーネント設計の考え方」というテーマで、1つのコンポーネントが責務を抱えすぎないようにする考え方を解説しました。
今回はその一歩手前、そもそもファイルをどこに置くかという話です。
コンポーネント単体をどれだけきれいに書いても、ディレクトリ構成が崩れていると、チーム開発では「このコンポーネントどこにあるんだっけ」という探索コストがどんどん積み重なっていきます。
今日は、Reactプロジェクトでよく使われるディレクトリ構成のパターンと、それぞれの向き不向きを整理していきます。
なぜディレクトリ構成が重要なのか
コードは書く時間より読む時間の方が長い
個人開発の小さなアプリなら、正直ディレクトリ構成はそこまで気にしなくても困りません。
ですが、チームで開発したり、プロジェクトが半年、1年と続いたりすると話は変わってきます。
新しく入ったメンバーが最初にぶつかる壁は、たいてい「どこに何があるか分からない」というものです。
ディレクトリ構成は、いわばコードベースの地図です。
地図が整っていれば、初見のメンバーでも迷わず目的のファイルにたどり着けます。
「とりあえずcomponents」の限界
Reactを学び始めた頃、多くの人は components/ フォルダに全部のコンポーネントを放り込むところからスタートします。
私自身も最初のプロジェクトでは、components/ の中にボタンもヘッダーもページ全体のコンポーネントも、全部フラットに並べていました。
最初の10個くらいまでは問題ありません。
ただ、これが50個、100個と増えていくと、エディタのファイル一覧を開くたびに目的のファイルを探すのに時間がかかるようになります。
「そろそろ構成を見直したいけど、どこから手をつければいいか分からない」——これは中規模以上のReactプロジェクトで非常によくある悩みです。
代表的な3つの構成パターン
パターン1:種類別構成(Type-based)
もっとも初心者に馴染みやすいのが、ファイルの種類ごとにフォルダを分ける構成です。
src/
├── components/
│ ├── Button.tsx
│ ├── Header.tsx
│ └── UserCard.tsx
├── hooks/
│ ├── useAuth.tsx
│ └── useFetch.tsx
├── pages/
│ ├── Home.tsx
│ └── UserProfile.tsx
└── utils/
└── formatDate.tsx
小規模なプロジェクトであれば、このシンプルさが強みになります。
「コンポーネントはcomponents/、フックはhooks/」というルールが明快で、新規メンバーも迷いにくいです。
ただし規模が大きくなると、1つの機能を追うために components/・hooks/・utils/ を何度も行き来する必要が出てきます。
「ユーザープロフィール機能を修正したい」というときに、関連ファイルが3つのフォルダに散らばっている状態です。
パターン2:機能別構成(Feature-based)
種類別構成の弱点を補うのが、機能(ドメイン)ごとにフォルダを分ける構成です。
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ └── LoginForm.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.tsx
│ │ └── api/
│ │ └── authApi.tsx
│ └── userProfile/
│ ├── components/
│ │ └── UserCard.tsx
│ ├── hooks/
│ │ └── useUserProfile.tsx
│ └── api/
│ └── userApi.tsx
└── shared/
├── components/
│ └── Button.tsx
└── utils/
└── formatDate.tsx
「認証機能を触りたいなら features/auth/ の中だけ見ればいい」という状態を作れるのが最大のメリットです。
機能を跨いで使う共通コンポーネントは shared/ にまとめておきます。
Next.jsのApp Routerと組み合わせる場合も、この機能別構成の考え方は非常に相性が良いです。
パターン3:Atomic Design構成
コンポーネントの「粒度」を軸に分類する構成もあります。
src/
├── components/
│ ├── atoms/
│ │ └── Button.tsx
│ ├── molecules/
│ │ └── SearchForm.tsx
│ ├── organisms/
│ │ └── Header.tsx
│ └── templates/
│ └── PageLayout.tsx
粒度ごとに責務が明確になる一方、「このコンポーネントはmoleculesかorganismsか」の判断に迷いやすいという難点もあります。
Atomic Designについては、次回の記事で詳しく掘り下げます。
比較表:どのパターンを選ぶべきか
判断軸を整理する
3つの構成の違いを、判断軸ごとに整理しておきます。
| 観点 | 種類別構成 | 機能別構成 | Atomic Design構成 |
|---|---|---|---|
| 向いている規模 | 小規模(〜数十コンポーネント) | 中〜大規模 | 中規模(デザインシステム重視) |
| 学習コスト | 低い | 中程度 | やや高い(粒度の判断が必要) |
| 機能追加のしやすさ | フォルダを跨ぐ作業が増える | 機能単位で完結しやすい | 粒度単位で完結しやすい |
| チーム開発との相性 | 小規模チーム向き | 複数チームでの並行開発に強い | デザイナーとの連携がある場合に強い |
迷ったときの選び方
初めてReactプロジェクトを立ち上げるなら、まずは種類別構成でシンプルに始めて問題ありません。
コンポーネント数が30〜50を超えてきたあたりで、機能別構成への移行を検討するのが現実的なタイミングです。
デザインチームと密に連携しながらデザインシステムを構築する場合は、Atomic Design構成が力を発揮します。
「最初から完璧な構成を目指す」より、「プロジェクトの成長に合わせて構成を見直す」という前提で考えるのがおすすめです。
つまずきやすいポイント:共通コンポーネントの置き場所
あいまいな common/ フォルダが生む混乱
機能別構成に移行する際、私が実際に苦労したのが「共通コンポーネントをどこに置くか」の判断です。
❌ Before:何でも common/ に放り込む
// src/common/components/UserAvatarWithBadge.tsx
// 「ユーザーのアバターに未読バッジを表示する」という
// 特定機能(通知機能)に強く依存したコンポーネントなのに
// なんとなく共通っぽいという理由でcommonに置いてしまっている
type Props = {
imageUrl: string;
unreadCount: number;
};
export const UserAvatarWithBadge = ({ imageUrl, unreadCount }: Props) => {
return (
<div className="avatar-wrapper">
<img src={imageUrl} alt="user avatar" />
{unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</div>
);
};
このコンポーネントは一見「アバター表示」という汎用的な役割に見えますが、実際は通知機能の未読件数に依存しています。
common/ に置いてしまうと、通知機能を削除・変更するときにこのファイルの存在を見落としやすくなります。
✅ After:本当に汎用的なものだけを共通化する
// src/shared/components/Avatar.tsx
// 「画像を丸く表示する」という純粋な見た目だけの責務に留める
type Props = {
imageUrl: string;
size?: number;
};
export const Avatar = ({ imageUrl, size = 40 }: Props) => {
return (
<img
src={imageUrl}
alt="avatar"
style={{ width: size, height: size, borderRadius: "50%" }}
/>
);
};
// src/features/notification/components/UserAvatarWithBadge.tsx
// バッジ表示のロジックは通知機能側に持たせ、汎用Avatarを利用する
import { Avatar } from "../../../shared/components/Avatar";
type Props = {
imageUrl: string;
unreadCount: number;
};
export const UserAvatarWithBadge = ({ imageUrl, unreadCount }: Props) => {
return (
<div className="avatar-wrapper">
<Avatar imageUrl={imageUrl} />
{unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</div>
);
};
判断の基準はシンプルで、「特定の機能・ドメイン知識に依存しているかどうか」です。
依存しているなら features/ の中に、依存していない純粋な見た目・処理だけなら shared/ に置く、というルールを決めておくと、後から見返しても迷いません。
応用:バレルファイルとパスエイリアスの活用
import文を短く保つ
機能別構成にすると、階層が深くなり import のパスが長くなりがちです。
// ❌ 相対パスが長くなりがち
import { useAuth } from "../../../features/auth/hooks/useAuth";
これを解決するのが、tsconfig.json で設定するパスエイリアスです。
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// ✅ エイリアスでどの階層からでも同じ書き方になる
import { useAuth } from "@/features/auth/hooks/useAuth";
バレルファイル(index.ts)の使いどころ
フォルダの入口に index.ts を置き、外部に公開するものだけをまとめてエクスポートする「バレルファイル」もよく使われます。
// src/features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { useAuth } from "./hooks/useAuth";
こうしておくと、他の機能からは features/auth の内部構造を意識せず、import { useAuth } from "@/features/auth" の1行でアクセスできます。
ただしバレルファイルを多用しすぎると、ビルド時の依存解決が複雑になり、ビルド速度が落ちるケースもあります。
大規模プロジェクトでは、必要な機能の境界にだけ絞って使うのがバランスの良い運用です。
まとめ
この記事のポイント
- ディレクトリ構成は「コードベースの地図」であり、規模が大きくなるほど重要性が増す
- 種類別構成はシンプルだが、規模が大きくなるとフォルダ間の行き来が増える
- 機能別構成は中〜大規模プロジェクトや複数チームでの開発に強い
- 共通コンポーネントは「特定機能に依存しているかどうか」で置き場所を判断する
- パスエイリアスやバレルファイルを使うと、import文の見通しが良くなる
次に読むべき記事
ディレクトリ構成の次は、コンポーネントの「粒度」の考え方を掘り下げていきます。
→ 次の記事:Atomic Design入門:コンポーネントを整理する考え方