はじめに

Reactプロジェクトが大規模化するにつれて、「どこに何を置くべきか」「コンポーネント間の依存関係が複雑になりすぎた」といった課題に直面することが増えます。こうした問題を解決するために注目されているのが、Feature Sliced Design(FSD) というアーキテクチャ手法です。

Feature Sliced Designは、フロントエンドアプリケーションのディレクトリ構造を体系的に整理するための方法論です。Layer(層)、Slice(スライス)、Segment(セグメント)という3つの階層でコードを組織化し、ビジネスドメインごとにコードを分割することで、保守性とスケーラビリティを両立させます。

本記事では、ReactプロジェクトにFeature Sliced Designを導入するための実践的な知識を解説します。

実行環境・前提条件

前提知識

  • Reactの基本的なコンポーネント設計の理解
  • TypeScriptの基本的な型定義の知識
  • npmまたはyarnを使ったパッケージ管理の経験

対象読者

  • Reactプロジェクトのディレクトリ構造に課題を感じている開発者
  • チーム開発でのコード管理を改善したい方
  • 大規模アプリケーションの設計パターンを学びたい方

Feature Sliced Designの基本概念

Feature Sliced Design(FSD)は、フロントエンドプロジェクトを構造化するためのアーキテクチャ方法論です。コードを「ビジネスドメイン」と「技術的な役割」の2軸で整理することで、変更に強く理解しやすいコードベースを実現します。

FSDの3つの階層構造

FSDは以下の3つの階層でコードを組織化します。

階層 役割
Layer(層) 責務と依存関係のレベルを定義 app, pages, features, entities, shared
Slice(スライス) ビジネスドメインごとにコードを分割 user, post, comment, cart
Segment(セグメント) 技術的な役割でコードを分類 ui, model, api, lib

この階層構造により、「どのレイヤーに」「どのビジネスドメインの」「どんな種類のコードか」が一目でわかるようになります。

Layerの役割と設計思想

Feature Sliced Designでは、7つのLayerが定義されています。上位のLayerほど多くの責務を持ち、下位のLayerに依存できます。

Layerの一覧

src/
├── app/        # アプリ全体の設定・初期化
├── processes/  # 複数ページにまたがる処理(非推奨)
├── pages/      # ページ単位のコンポーネント
├── widgets/    # 自己完結した大きなUIブロック
├── features/   # ユーザーの操作・機能
├── entities/   # ビジネスエンティティ
└── shared/     # 再利用可能な共通機能

各Layerの詳細

Shared(共有層)

プロジェクト全体で再利用される基盤コードを配置します。ビジネスロジックを含まない純粋なユーティリティやUIコンポーネントが該当します。

src/shared/
├── api/       # APIクライアント、リクエスト関数
├── ui/        # ボタン、モーダルなど汎用コンポーネント
├── lib/       # ユーティリティ関数(日付操作、バリデーションなど)
├── config/    # 環境変数、設定ファイル
└── routes/    # ルート定義の定数

Entities(エンティティ層)

プロジェクトで扱うビジネスエンティティを表現します。User、Product、Orderなど、ビジネス用語で表現できる概念がSliceとなります。

src/entities/
├── user/
│   ├── ui/         # ユーザーアバター、ユーザーカードなど
│   ├── model/      # ユーザーの型定義、状態管理
│   └── api/        # ユーザー関連のAPI呼び出し
├── product/
│   ├── ui/
│   ├── model/
│   └── api/
└── order/
    ├── ui/
    ├── model/
    └── api/

Features(機能層)

ユーザーがアプリで行う操作や機能を表現します。「ログイン」「商品をカートに追加」「コメントを投稿」など、ビジネス価値を生み出すアクションが該当します。

src/features/
├── auth/
│   ├── ui/         # ログインフォーム、ログアウトボタン
│   ├── model/      # 認証状態の管理
│   └── api/        # 認証APIの呼び出し
├── add-to-cart/
│   ├── ui/         # カート追加ボタン
│   └── model/      # カート操作のロジック
└── post-comment/
    ├── ui/         # コメント投稿フォーム
    ├── model/
    └── api/

Widgets(ウィジェット層)

複数のFeatureやEntityを組み合わせた、自己完結した大きなUIブロックを配置します。ヘッダー、サイドバー、フッターなどが典型例です。

src/widgets/
├── header/
│   └── ui/         # ナビゲーション、ユーザーメニューを含むヘッダー
├── sidebar/
│   └── ui/         # サイドバーナビゲーション
└── product-card/
    └── ui/         # 商品情報と購入ボタンを含むカード

Pages(ページ層)

ルーティングに対応するページコンポーネントを配置します。1ページ = 1スライスが基本ですが、類似したページはグループ化できます。

src/pages/
├── home/
│   ├── ui/         # ホームページのUI
│   └── api/        # ページ固有のデータ取得
├── product-detail/
│   ├── ui/
│   └── api/
└── checkout/
    ├── ui/
    └── api/

App(アプリ層)

アプリケーション全体の設定、プロバイダー、ルーティング設定などを配置します。このLayerはSliceを持たず、直接Segmentで構成されます。

src/app/
├── routes/        # ルーター設定
├── store/         # グローバルストア設定
├── styles/        # グローバルスタイル
└── providers/     # Context Providerのラッパー

Sliceの分割基準

Sliceはビジネスドメインごとにコードを分割する単位です。適切なSliceの設計がFSD成功の鍵となります。

Sliceを分ける判断基準

Sliceを分ける際は、以下の観点で判断します。

  1. ビジネス用語として独立しているか

    • 「ユーザー」「商品」「注文」など、ビジネス会話で使われる概念は独立したSliceにします
  2. 複数の場所で再利用されるか

    • 複数のページで使われる機能はFeatureとして切り出します
    • 1箇所でしか使わない場合はページ内に留めても問題ありません
  3. 独立して変更できるか

    • 他のSliceに影響を与えずに変更できる範囲でSliceを分割します

良いSlice設計の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// entities/user/model/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

// entities/user/ui/UserAvatar.tsx
import type { User } from '../model/types';

interface UserAvatarProps {
  user: User;
  size?: 'sm' | 'md' | 'lg';
}

export const UserAvatar = ({ user, size = 'md' }: UserAvatarProps) => {
  return (
    <img 
      src={user.avatar} 
      alt={user.name}
      className={`avatar avatar-${size}`}
    />
  );
};

避けるべきSlice設計

# 悪い例:技術的な分類でSliceを作っている
src/features/
├── hooks/          # 技術的な分類(NG)
├── components/     # 技術的な分類(NG)
└── utils/          # 技術的な分類(NG)

# 良い例:ビジネスドメインでSliceを作る
src/features/
├── authentication/
├── shopping-cart/
└── product-search/

Segmentの役割と命名規則

Segmentはコードの技術的な役割を表します。標準的なSegment名を使うことで、チーム内での認識を統一できます。

標準的なSegment

Segment 役割 含まれるもの
ui UI表示 Reactコンポーネント、スタイル
model データモデル 型定義、状態管理、ビジネスロジック
api バックエンド連携 APIリクエスト関数、データマッパー
lib ライブラリコード Slice内で使うヘルパー関数
config 設定 定数、フィーチャーフラグ

Segmentの配置例

src/features/authentication/
├── ui/
│   ├── LoginForm.tsx
│   ├── LoginForm.module.css
│   └── LogoutButton.tsx
├── model/
│   ├── types.ts
│   ├── useAuth.ts
│   └── authStore.ts
├── api/
│   ├── login.ts
│   └── logout.ts
└── index.ts          # Public API

インポートルールと依存関係の制御

FSDの最も重要なルールはインポートルールです。このルールにより、コードの依存関係を一方向に保ち、予測可能な変更を実現します。

Layer間のインポートルール

上位のLayerは下位のLayerのみをインポートできます。同一Layer間のインポートは禁止です。

App → Pages → Widgets → Features → Entities → Shared
 ↓      ↓        ↓         ↓          ↓         ↓
すべての下位Layerを参照可能  →  Sharedのみ参照可能

具体的なインポート例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// pages/product-detail/ui/ProductDetailPage.tsx

// OK: 下位LayerのWidgetsからインポート
import { ProductCard } from 'widgets/product-card';

// OK: 下位LayerのFeaturesからインポート
import { AddToCartButton } from 'features/add-to-cart';

// OK: 下位LayerのEntitiesからインポート
import { ProductImage } from 'entities/product';

// OK: 下位LayerのSharedからインポート
import { Button } from 'shared/ui';

// NG: 同一Layerの他のSliceからインポート(禁止)
// import { CheckoutPage } from 'pages/checkout';

// NG: 上位Layerからインポート(禁止)
// import { AppProvider } from 'app/providers';

Public APIによるカプセル化

各Sliceはindex.tsでPublic APIを定義し、外部に公開するモジュールを明示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// features/authentication/index.ts

// 公開するもの
export { LoginForm } from './ui/LoginForm';
export { LogoutButton } from './ui/LogoutButton';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';

// 非公開(外部からアクセス不可)
// - ./api/login.ts
// - ./model/authStore.ts

ReactプロジェクトへのFSD導入例

実際のReactプロジェクトでFeature Sliced Designを適用した場合のディレクトリ構造を紹介します。

ECサイトの構造例

src/
├── app/
│   ├── routes/
│   │   └── AppRouter.tsx
│   ├── store/
│   │   └── store.ts
│   ├── styles/
│   │   └── global.css
│   └── providers/
│       └── AppProviders.tsx
│
├── pages/
│   ├── home/
│   │   ├── ui/
│   │   │   └── HomePage.tsx
│   │   └── index.ts
│   ├── product-list/
│   │   ├── ui/
│   │   │   └── ProductListPage.tsx
│   │   ├── api/
│   │   │   └── getProducts.ts
│   │   └── index.ts
│   ├── product-detail/
│   │   ├── ui/
│   │   │   └── ProductDetailPage.tsx
│   │   ├── api/
│   │   │   └── getProductById.ts
│   │   └── index.ts
│   └── checkout/
│       ├── ui/
│       │   └── CheckoutPage.tsx
│       └── index.ts
│
├── widgets/
│   ├── header/
│   │   ├── ui/
│   │   │   └── Header.tsx
│   │   └── index.ts
│   ├── footer/
│   │   ├── ui/
│   │   │   └── Footer.tsx
│   │   └── index.ts
│   └── product-card/
│       ├── ui/
│       │   └── ProductCard.tsx
│       └── index.ts
│
├── features/
│   ├── authentication/
│   │   ├── ui/
│   │   │   ├── LoginForm.tsx
│   │   │   └── LogoutButton.tsx
│   │   ├── model/
│   │   │   ├── useAuth.ts
│   │   │   └── types.ts
│   │   ├── api/
│   │   │   └── authApi.ts
│   │   └── index.ts
│   ├── add-to-cart/
│   │   ├── ui/
│   │   │   └── AddToCartButton.tsx
│   │   ├── model/
│   │   │   └── useCart.ts
│   │   └── index.ts
│   └── product-search/
│       ├── ui/
│       │   └── SearchBar.tsx
│       ├── model/
│       │   └── useSearch.ts
│       └── index.ts
│
├── entities/
│   ├── user/
│   │   ├── ui/
│   │   │   ├── UserAvatar.tsx
│   │   │   └── UserCard.tsx
│   │   ├── model/
│   │   │   └── types.ts
│   │   ├── api/
│   │   │   └── userApi.ts
│   │   └── index.ts
│   ├── product/
│   │   ├── ui/
│   │   │   ├── ProductImage.tsx
│   │   │   └── ProductPrice.tsx
│   │   ├── model/
│   │   │   └── types.ts
│   │   └── index.ts
│   └── cart/
│       ├── ui/
│       │   └── CartItem.tsx
│       ├── model/
│       │   ├── types.ts
│       │   └── cartStore.ts
│       └── index.ts
│
└── shared/
    ├── api/
    │   └── apiClient.ts
    ├── ui/
    │   ├── Button/
    │   │   ├── Button.tsx
    │   │   └── Button.module.css
    │   ├── Input/
    │   │   └── Input.tsx
    │   ├── Modal/
    │   │   └── Modal.tsx
    │   └── index.ts
    ├── lib/
    │   ├── formatPrice.ts
    │   └── formatDate.ts
    ├── config/
    │   └── constants.ts
    └── routes/
        └── paths.ts

パスエイリアスの設定

FSDを快適に使うために、絶対パスでインポートできるようパスエイリアスを設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      'app': path.resolve(__dirname, './src/app'),
      'pages': path.resolve(__dirname, './src/pages'),
      'widgets': path.resolve(__dirname, './src/widgets'),
      'features': path.resolve(__dirname, './src/features'),
      'entities': path.resolve(__dirname, './src/entities'),
      'shared': path.resolve(__dirname, './src/shared'),
    },
  },
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "app/*": ["src/app/*"],
      "pages/*": ["src/pages/*"],
      "widgets/*": ["src/widgets/*"],
      "features/*": ["src/features/*"],
      "entities/*": ["src/entities/*"],
      "shared/*": ["src/shared/*"]
    }
  }
}

Feature Sliced Designのメリット

FSDを導入することで、以下のメリットが得られます。

一貫性のあるプロジェクト構造

ディレクトリ構造が標準化されているため、プロジェクト間で統一した構成を保てます。新しいメンバーがチームに参加した際も、構造の学習コストを抑えられます。

変更に強い設計

Layerのインポートルールにより、変更の影響範囲が限定されます。下位Layerを修正しても、上位Layerに影響を与えにくい構造になっています。

コードの発見しやすさ

ビジネスドメインでSliceが分かれているため、「ユーザー関連のコードはentities/user」「ログイン機能はfeatures/authentication」と直感的にコードを探せます。

適切な再利用性のコントロール

SharedやEntitiesは積極的に再利用し、FeaturesやPagesは限定的に使うという方針が明確になります。過度なDRYによる複雑化を防げます。

Feature Sliced Designのデメリット

FSDにはいくつかの課題もあります。導入前に認識しておきましょう。

学習コスト

Layer、Slice、Segmentの概念や、インポートルールの理解には一定の学習が必要です。チーム全員が概念を理解していないと、ルールが形骸化する恐れがあります。

小規模プロジェクトではオーバーヘッド

5〜10画面程度の小規模アプリケーションでは、FSDの階層構造がかえって複雑に感じられることがあります。

Slice間の関係の設計が難しい

Entities間で関連がある場合(例:UserがPostを持つ)、クロスリファレンスの設計に悩むことがあります。公式では@x記法が提案されていますが、プロジェクトごとに最適解を探る必要があります。

既存プロジェクトへの導入コスト

すでに大規模になったプロジェクトをFSDに移行するには、段階的なリファクタリングが必要です。一度にすべてを移行することは現実的ではありません。

FSD導入時の注意点

Feature Sliced Designを成功させるためのポイントを紹介します。

すべてのLayerを使う必要はない

プロジェクトの規模に応じて、必要なLayerだけを使いましょう。小規模なプロジェクトではapppagessharedの3層だけでも十分です。

# 小規模プロジェクト向け
src/
├── app/
├── pages/
└── shared/

# 中規模プロジェクト向け
src/
├── app/
├── pages/
├── features/
├── entities/
└── shared/

段階的に導入する

既存プロジェクトに導入する場合は、以下の順序で段階的に進めます。

  1. sharedappを整備する
  2. 既存のUIをpageswidgetsに大まかに分類する
  3. 徐々にentitiesfeaturesを抽出する

Linterでルールを強制する

FSDのインポートルールを手動でチェックするのは困難です。公式のLinterツール「Steiger」を導入して、ルール違反を自動検出しましょう。

1
npm install -D @feature-sliced/eslint-config

チームでの合意形成

FSDの導入はチーム全体で合意を取ることが重要です。「なぜFSDを採用するのか」「どのLayerをどこまで使うのか」を明文化し、ドキュメントとして残しておきましょう。

まとめ

Feature Sliced Designは、Reactプロジェクトのディレクトリ構造を体系的に整理するための強力な方法論です。Layer、Slice、Segmentの3層構造と明確なインポートルールにより、保守性の高いコードベースを実現できます。

ただし、すべてのプロジェクトにFSDが適しているわけではありません。プロジェクトの規模やチームの状況を考慮し、必要に応じて段階的に導入することをおすすめします。

まずは小さなプロジェクトで試してみて、FSDの考え方に慣れてから本番プロジェクトへの導入を検討してみてください。

参考リンク