🏗️

Feature-Sliced Design を実践的に学ぶ:設計と実装のベストプラクティス

に公開

はじめに

フロントエンド開発において、コンポーネントの肥大化や責務の曖昧さに悩まされた経験はありませんか?
そんな課題を解決する設計手法が「Feature-Sliced Design(FSD)」です。
本記事では、FSDの基本概念から、レイヤー構造、設計指針、コード例、そしてよくある質問やアンチパターンまでを網羅的に解説します。

Feature-Sliced Design とは?

Feature-Sliced Design(以下、FSD)は、React(などのUIライブラリ)向けに設計されたスケーラブルなフロントエンドアーキテクチャです。

従来の「pages / components / utils」ベースの構造では難しかった以下のような問題にアプローチできます。

  • 機能ごとの責務が曖昧になる
  • コンポーネントの再利用性が低い
  • ドメイン知識が散らばって保守しづらい

FSDは、ドメイン中心・機能単位の分割を特徴とし、プロダクトのスケールに強い構成を可能にします。

FSDはいつ使うべき?──導入判断のヒント

Feature-Sliced Design(FSD)は、柔軟かつ強力なアーキテクチャパターンですが、すべてのプロジェクトに適しているわけではありません。
ここでは「FSDを採用すべきか?」を判断するためのヒントをいくつか紹介します。

向いているケース

  • 画面数や機能数が多い中〜大規模なアプリケーション
  • 複数人・複数チームでの共同開発
  • ビジネスロジックとUIの責務を分けたい
  • プロダクトのライフサイクルが長く、頻繁に機能追加や改修が発生する
  • クリーンアーキテクチャやDDDのような思想に関心がある

注意したいケース

  • ページ数の少ない簡易なツール・社内ツール
  • 一人で開発している個人プロジェクト
  • プロトタイピングやPoCなど、短期で完成させることが目的

FSDの構造と各Layerの役割

FSDは以下のレイヤーで構成されます:

src/
├── app/         # アプリ全体の初期化(Provider, Router 等)
├── pages/       # Next.jsなどのルーティングページ単位
├── widgets/     # 表示に特化した UI コンポーネントの集合
├── features/    # ユーザーアクションを伴う、ドメインに関わる機能単位
├── entities/    # アプリ内で共通して利用されるビジネスオブジェクト
├── shared/      # UIコンポーネント、ユーティリティなどの純粋共有モジュール

各レイヤーの解説とコード例

shared/

  • 目的:プロダクト全体で使い回す、表示専用またはユーティリティ的な要素
  • :Button, TextField, theme, hooks, libs, types
shared/ui/Button.tsx
export const Button = (props: ButtonProps) => {
  return <ChakraButton {...props} />;
};

entities/

  • 目的:ビジネスロジックを持つ、ドメイン中心の最小単位のオブジェクト
  • :User, Product, Article など
  • 含まれる要素
    • model/:状態管理ロジック(useUserなど)、ビジネスロジック、スキーマやインターフェース
    • ui/:純粋な表示コンポーネント(UserCardなど)、スタイルやフォーマッタ
    • lib/:ドメイン内で使われる汎用関数やユーティリティ
    • api/:外部API呼び出し
    • config/:ドメイン固有の設定、環境変数、機能フラグ
entities/user/ui/UserRow.tsx
import { User } from '../model/types';

export const UserRow = ({ user }: { user: User }) => (
  <tr>
    <td>{user.name}</td>
    <td>{user.email}</td>
  </tr>
);
entities/user/model/types.ts
export type User = {
  id: string;
  name: string;
  email: string;
};

features/

  • 目的:ユーザーアクションや振る舞いを伴う、1つの目的に特化した機能
  • :ログインフォーム、商品の削除、投稿の共有など
  • 含まれる要素
    • model/:状態管理ロジック(useUserなど)、ビジネスロジック、スキーマやインターフェース
    • ui/:純粋な表示コンポーネント(UserCardなど)、スタイルやフォーマッタ
    • lib/:機能固有の関数やユーティリティ
    • api/:外部API呼び出し
    • config/:機能固有の設定、環境変数、機能フラグ
features/user/list/ui/UserList.tsx
import { useEffect, useState } from 'react';
import { fetchUsers } from '../../api/getUsers';
import { User } from '../../../entities/user/model/types';
import { UserRow } from '../../../entities/user/ui/UserRow';
import { Table } from '../../../shared/ui/Table';

export const UserList = () => {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    fetchUsers().then(setUsers);
  }, []);

  return (
    <Table>
      <tbody>
        {users.map((user) => (
          <UserRow key={user.id} user={user} />
        ))}
      </tbody>
    </Table>
  );
};
features/user/list/api/getUsers.ts
export const fetchUsers = async () => {
  const res = await fetch('/api/users');
  return res.json();
};

widgets/

  • 目的:UI部品の組み合わせにより、構造を持った画面構成要素を形成
  • :Header, Footerなど
  • 特徴
    • アクションを持たず、表示に専念
    • 再利用性が高い
widgets/UserSection.tsx
import { UserList } from '../features/user/list/ui/UserList';
import { Button } from '../shared/ui/Button';

export const UserSection = () => (
  <section>
    <h2>ユーザー一覧</h2>
    <UserList />
    <Button onClick={() => alert('追加')}>新規ユーザー追加</Button>
  </section>
);

pages/

  • 目的:アプリケーションの各画面(ルーティング単位)を構成するエントリーポイント
  • :ログインページ、ユーザープロフィール、ダッシュボードなど
  • 特徴
    • URL に対応する単位で構成される(例:/login, /profile
    • ページ単位で widgetsfeatures を組み合わせて構築する
    • 通常、Next.js や Remix のようなフレームワークのルールに従う
pages/profile.tsx
import { Header } from '@/widgets/header';
import { UserSection } from '@/widgets/user-section';

export default function ProfilePage() {
  return (
    <>
      <Header />
      <UserSection />
    </>
  );
}

よくある質問(FAQ)

Q1. Formはwidgetsではないの?

Formは単なる表示ではなく「送信」というユーザーアクションを扱うため、features に分類されます。

Q2. HeaderやFooterは再利用していないけどsharedに置くべき?

HeaderやFooterのような表示専用UIコンポーネントで、ドメインロジックやユーザーアクションがないものは widgets が適切です。

Q3. modelセグメントはどのように分類・配置すればよいですか?

FSDでは、Layer > Slice > Segment という構造になっており、
model はSlice内のSegmentの一つとして、状態管理やビジネスロジックを担当します。

どのSliceのmodelに配置するか判断するためのフローチャートの例を以下に示します。

アンチパターンと注意点

以下は、FSD を導入した際によくあるアンチパターンです。構造が崩れてしまう原因になるため、注意が必要です。

features にデータ取得(query)をすべて入れてしまう

例:画面ロード時に使う初期データ取得のクエリを features に記述してしまう

なぜダメ?
Query(読み取り系のロジック)は、ドメインに属する「情報の持ち主」である entities に配置するのが原則です。
features はあくまで「振る舞い(ユースケース)」の単位であり、読み取りだけの責務とは切り分けることで、責任範囲が明確になります。

shared にドメイン固有の型や関数を置いてしまう

例:ユーザー情報に特化したバリデーション関数を shared に置いてしまう

なぜダメ?
shared は「アプリケーション全体で使える汎用的なロジック」や「共通UI」を置く場所です。
一方、特定のドメイン(例:ユーザー・商品など)に強く依存するものは entities に配置すべきです。
これを混在させると、shared が「なんでも置ける箱」になってしまい、再利用性のある構造が崩れていきます。

widgets にビジネスロジックを混ぜてしまう

例:ヘッダーコンポーネント内で API を呼び出して状態管理を行っている

なぜダメ?
widgets は「表示専用コンポーネント」の層であり、UI部品の組み合わせにフォーカスします。
ビジネスロジック(API呼び出し・状態変更など)は featuresentities に委ねるべきです。
UIとロジックを混ぜると、再利用しづらくなり、テストや保守性も低下します。


こうしたアンチパターンを避けることで、レイヤーごとの責務が明確になり、開発規模が大きくなっても構造が破綻しにくくなります。

まとめ

Feature-Sliced Design は、大規模・中規模フロントエンド開発において強力な設計指針を与えてくれます。

レイヤー 主な役割 特徴
app 初期化やルーティング 最上位の構成
pages ページ単位のエントリーポイント フレームワーク依存
widgets UI部品の組み合わせ 表示専用・再利用しやすい
features 機能単位・アクションを伴う ユーザーアクション中心
entities ドメインモデルの最小単位 データ中心の設計
shared 汎用的な部品やロジック 全体共通で使える

おわりに

この記事では、Feature-Sliced Design の基本から、実際の実装・判断フロー・アンチパターンまで体系的にまとめました。

設計の正解は1つではありませんが、FSDの考え方はチーム開発の共通言語として非常に有効です。
あなたのプロジェクトにもぜひ取り入れてみてください。

参考リンク・ドキュメント

Discussion