「スパゲッティコード」という悪夢と、転換点となった変化
50以上の画面と10人の開発者を抱えるSaaSプロジェクトを半年間運用した後、私は完全に行き詰まってしまいました。components/、hooks/、services/といった従来のディレクトリ構成(folder-by-type)は、最初は整っているように見えましたが、すぐに混沌とした状態に陥りました。何百ものファイルの中に隠れたロジックを探し出したり、他の部分を壊さずに機能を修正したりすることが、極めて困難な課題となったのです。
そこで私は、Feature-Sliced Design (FSD)に基づいて全体を再構築することに決めました。結果は驚くべきものでした。新人エンジニアのオンボーディング期間は2週間から4日間に短縮され、すべてが「あるべき場所」に配置されたことで、コードレビューのスピードも2倍になりました。FSDは単なるフォルダの命名規則ではありません。関心を科学的に分離するための「階層化(Layering)」の思考法なのです。
もしあなたが、ファイルAを修正した際、10階層も離れた場所にあるファイルBが突然エラーを吐いて発狂しそうになったことがあるなら、FSDこそが求めていた解決策です。
7つのレイヤー構造:FSD의 骨格
FSDではプロジェクトを7つの固定レイヤーに分割します。黄金律は極めてシンプルです。「**下位のレイヤーからのみインポート可能**」ということ。上位レイヤーからのインポートや、同じレイヤー内でのモジュール間のクロスインポートは厳禁です。
以下は、私が実際に導入して成功した構成図です:
src/
├── app/ # 全体設定 (Providers, Styles, Routing)
├── processes/ # (オプション) 複数のページにまたがる複雑なフロー
├── pages/ # ページ全体の構成 (Composition)
├── widgets/ # 大きなUIブロック (例:Header, Sidebar)
├── features/ # ユーザーアクション (Login, Search, AddToCart)
├── entities/ # コアとなるビジネスロジック (User, Product, Order)
├── shared/ # 共通コード (UI Kit, API client, Utils)
実践的な導入:焦らず一歩ずつ
最初からすべてを作り直す必要はありません。まずは最も低いレイヤーであるsharedから始めましょう。
- Shared: ビジネスロジックを持たない「純粋な」コンポーネントの集まりです。例:Button、Input、Axiosインスタンス。
- Entities: オブジェクトを定義する場所です。例えば、
entities/userにはUserCardのUI、ユーザーデータの処理関数、および対応するRedux sliceが含まれます。 - Features: ユーザーが得られる価値に焦点を当てます。例:
features/auth-by-email。
見分けるためのヒント:Entityは「これは何か?」(ユーザーデータ)という問いに答え、Featureは「ユーザーは何ができるか?」(ログイン)という問いに答えます。
Public API:厳格な門番
FSDでは、各サブディレクトリに**Public API**として機能するindex.tsを配置する必要があります。これは、外部からそのモジュール内部にアクセスするための唯一の窓口となります。
典型的なFeatureの構造は以下のようになります:
features/add-comment/
├── ui/ # 表示インターフェース
├── model/ # ロジック (States, Selectors)
├── lib/ # 内部ヘルパー関数
└── index.ts # 唯一のエクスポート窓口
index.tsでは、本当に必要なものだけをエクスポートします:
// features/add-comment/index.ts
export { AddCommentForm } from './ui/AddCommentForm';
export type { CommentSchema } from './model/types';
コードをクリーンでプロフェッショナルに保つために、tsconfig.jsonのパスエイリアス(path aliases)を活用しましょう。長い相対パスの代わりに@/を使用します:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
これにより、pages/article-detailsでのインポートは非常に簡潔になります:import { AddCommentForm } from '@/features/add-comment';。注意点として、Public APIをバイパスしてインポート(例:@/features/add-comment/ui/Form)してはいけません。カプセル化が壊れ、システムのメンテナンスが極めて困難になります。
ESLintをアーキテクチャの「警察」として活用する
理論は素晴らしいですが、チームが大きくなるとヒューマンエラーは避けられません。誰かが誤って2つのFeature間でクロスインポートをしてしまうかもしれません。これを防ぐために、私はeslint-plugin-boundariesを使用しています。
境界制御ツールのインストール:
npm install eslint-plugin-import eslint-plugin-boundaries --save-dev
この設定により、レイヤリング規則に違反した場合、IDE上で即座にエラーが通知されるようになります。例えば、entitiesがfeaturesに依存することを禁止します:
// 設定ルールの抜粋
"boundaries/element-types": [
2,
{
"default": "disallow",
"rules": [
{ "from": "entities", "allow": ["shared"] },
{ "from": "features", "allow": ["entities", "shared"] }
]
}
]
6ヶ月の実践から得られた教訓
FSDはすべてのプロジェクトにおける「銀の弾丸」ではありません。画面が2〜3個しかない小規模なアプリにFSDを適用するのは、牛刀をもって鶏を割くのと同じで、無駄な時間を費やすことになります(オーバーエンジニアリング)。
しかし、大規模プロジェクトでは、以下の3つのことを覚えておいてください。まず、土台となるsharedレイヤーには十分な投資をすること。次に、新しい開発者のために必ずARCHITECTURE.mdを用意すること。最後に、あまりに教条的になりすぎず、ビジネスの特性に合わせて柔軟にレイヤーを調整することです。
FSDを導入すると、最初はコードを書くスピードが少し落ちるかもしれません。しかし、信じてください。1年後にメンテナンスのためにコードを読み返したとき、このように整理整頓されたコードを書いた自分自身に感謝することになるでしょう。

