ウェブエンジニア問題集
第13章

ディレクトリ構成 — 規模と責務で決めるファイル配置

10
この章の目次開く

「このコンポーネント、どこに置けばいいんだろう」「あの処理を書いたファイル、どこだっけ」——ディレクトリ構成が設計されていないプロジェクトでは、この迷いが毎日発生します。

ディレクトリ構成はただの整理整頓ではありません。「どこに何があるか」の予測可能性と、「変更の影響がどこまで及ぶか」の見通しを決める、立派な設計対象です。この章では、代表的な2つの構成パターンと、規模に応じた使い分けを解説します。

学習者学習者

ディレクトリ構成って正解があるの?プロジェクトごとに全然違ってて、毎回どれが正しいのか分からなくなる…

先生先生

唯一の正解はないけど、「分類の軸」は2つしかないんだ。技術の種類で分けるか、機能で分けるか。世の中の構成パターンはほぼ全部、この2軸の組み合わせで説明できる。軸を理解すれば、初見のプロジェクトでも構成の意図が読めるようになるよ。

パターン1: レイヤー別(技術別)構成

ファイルを「それが何であるか(技術的な種類)」で分類する構成です。小規模なプロジェクトやチュートリアルでよく見る形で、MVCフレームワークの伝統的な構成もこの系統です。

src/
├── components/      # UIコンポーネント全部
│   ├── UserCard.tsx
│   ├── OrderList.tsx
│   └── ProductDetail.tsx
├── hooks/           # カスタムフック全部
│   ├── useUser.ts
│   └── useOrders.ts
├── services/        # API呼び出し・ビジネスロジック全部
│   ├── userService.ts
│   └── orderService.ts
├── types/           # 型定義全部
│   └── index.ts
└── utils/           # 汎用関数全部
    └── format.ts

レイヤー別の長所と短所

観点評価
置き場所の判断迷わない。「フックだから hooks/」と機械的に決まる
小規模での見通し良い。ファイル数が少ないうちは全体を把握しやすい
機能の変更つらい。1機能の修正で components/ hooks/ services/ を行き来する
機能の削除つらい。関連ファイルが散らばっているため消し漏れが出る
ファイル数の増加components/ に50ファイル並ぶと、もはや分類の意味がない

問題の本質は、実際の開発作業が「機能単位」で発生することです。「注文機能にキャンセルを追加する」という1つの変更が、レイヤー別構成では複数のディレクトリを横断します。

パターン2: 機能別(フィーチャー別)構成

ファイルを「どの機能のためのものか」で分類する構成です。

src/
├── features/
│   ├── user/
│   │   ├── components/UserCard.tsx
│   │   ├── hooks/useUser.ts
│   │   ├── api.ts
│   │   └── types.ts
│   ├── order/
│   │   ├── components/OrderList.tsx
│   │   ├── hooks/useOrders.ts
│   │   ├── api.ts
│   │   └── types.ts
│   └── product/
│       └── ...
└── shared/          # 複数の機能から使う共通部品
    ├── components/Button.tsx
    └── utils/format.ts

この構成の背景にあるのが**コロケーション(colocation)**という考え方です。

一緒に変更されるファイルは、近くに置く——変更の単位とディレクトリの単位を一致させることで、修正・削除の影響範囲がフォルダ1つに収まります。

「注文機能の修正」は features/order/ の中で完結し、「注文機能の廃止」は features/order/ の削除でほぼ終わります。レビューでも「このPRは features/order/ しか触っていないから、他の機能への影響はない」と判断できます。

機能別の長所と短所

観点評価
機能の変更・削除強い。影響範囲がフォルダ単位で閉じる
チーム分担機能ごとに担当を分けやすく、コンフリクトも減る
置き場所の判断迷う場面がある。「複数の機能で使うものはどこに置く?」問題が発生する
小規模での手間数ページのアプリでは大げさ。フォルダの階層だけ深くなる

どちらを選ぶか — 規模と変更の単位で決める

判断の目安は次のとおりです。

  • レイヤー別でよいケース — 画面数が少ない、機能同士の境界が曖昧、プロトタイプや学習用。分類のオーバーヘッドをかける価値がまだない段階
  • 機能別に移るサインcomponents/ の中を「機能名で検索」して探すようになった、1つの修正で触るディレクトリが常に3つ以上ある、機能の削除で消し漏れが出た
構成を選ぶ基準は「見た目の綺麗さ」ではなく「よくある変更が、いくつのディレクトリにまたがるか」です。
道を歩いて進む人
構成は一度決めたら終わりではなく、規模の成長に合わせて移行していく

なお、Next.jsのApp Routerのようにフレームワークがルーティング用のディレクトリ規約を持つ場合は、その規約(app/ 配下)はフレームワークに従い、それ以外のコード(features/shared/ など)を自分たちの方針で設計する、という組み合わせになります。

依存の方向を設計する — 構成はimportルールとセットで

ディレクトリを分けただけでは、構成は簡単に崩れます。崩壊を防ぐ本体は、「どこからどこへのimportを許すか」というルールです。

機能別構成での典型的なルールは次の3つです。

⭕ 許可される依存
features/order → shared          # 機能は共通部品を使ってよい
features/user  → shared

❌ 禁止される依存
features/order → features/user   # 機能同士の直接import禁止
shared         → features/order  # 共通部品が特定機能に依存するのは禁止
  • feature同士は直接importしないorderuser の内部ファイルに依存すると、user の変更が order を壊すようになり、フォルダを分けた意味が消えます。複数の機能で必要になったものは shared へ昇格させます
  • sharedはfeatureに依存しない — 依存の方向は常に features → shared の一方通行にします。これはSOLID原則で扱った「安定したものに依存する」という考え方のディレクトリ版です
  • feature内部への直接アクセスを制限する — 各featureの入り口となる index.ts で公開するものを明示し、外部からは features/order 経由でのみimportさせる形にすると、機能の内部構造を自由に変更できます

よくあるハマりどころ

utils/common/ のゴミ箱化

「どこに置くか迷ったら utils/ へ」を繰り返すと、utils/ は分類不能なコードの吹き溜まりになります。対策はシンプルで、置くときに具体的な名前を付けることです。utils/format.ts より shared/date/formatDate.ts。「共通っぽいから」ではなく「2つ以上の機能で実際に使われているから」shared に置く、という基準も有効です。

学習者学習者

とりあえず utils に置いて、あとで整理しよう…っていつも思うんだけど、その「あとで」が来たことがない気がする。

先生先生

みんなそう(笑)。だから「置くとき」が唯一の整理チャンスだと思った方がいい。迷ったら、そのファイルを使う側の機能のフォルダに置くのがおすすめ。共通化は2回目に使いたくなったときにやれば遅くないよ。

早すぎる共通化・早すぎる細分化

1つの機能でしか使っていないコンポーネントを、将来を見越して shared/ に置くと、「どの機能から使われているか分からないから変更が怖い」コードが増えます。また、ファイルが3つしかない段階で components/hooks/api/types/constants の5フォルダを切るのも、階層が深くなるだけです。構造は必要になってから増やす方が、結果的に綺麗に保てます。

「1ファイルが大きくなったら分割」の基準がない

ディレクトリ構成を決めても、1ファイルに全部書かれたら意味がありません。「コンポーネントとデータ取得ロジックは分ける」「1ファイルは目安〇行まで」のような分割の目安も、構成ルールとセットでREADMEなどに明文化しておくと、構成が長持ちします。

構成ルールがドキュメント化されていない

構成は「わかっている人」には自明でも、新しく入ったメンバーには見えません。どこに何を置くか・何が禁止かを README や CONTRIBUTING に数行でよいので書いておきます。これは伝わるコードとドキュメントで扱った「読者を意識したドキュメント」の実践でもあります。

ちゃんと使うためのポイント

  • ディレクトリ構成の分類軸は「技術の種類」か「機能」の2つ。どちらで分けるかを意識的に選ぶ
  • 小規模はレイヤー別で十分。変更が複数ディレクトリにまたがり始めたら機能別へ
  • 機能別構成の核はコロケーション——一緒に変わるものを近くに置き、変更・削除をフォルダ単位で閉じる
  • 構成はimportルール(features同士の直接import禁止、shared は features に依存しない)とセットで初めて機能する。ルールはESLintで強制する
  • utils/ をゴミ箱にしない。迷ったら使う側の機能フォルダに置き、共通化は2回目から
  • 構成ルールは明文化する。新メンバーが「どこに置くか」を質問しなくて済む状態がゴール

次の章では、ディレクトリに配置する中身の側——実務で頻出するデザインパターン(Repository・Service・Factory)を解説します。

参考リンク