Feature-Sliced Design(FSD)をプロダクトに導入しての振り返り

現在開発しているプロダクトでは、フロントエンドのアーキテクチャに Feature-Sliced Design(FSD) を採用しています。

フロントエンドの開発を進めていくと、アプリの規模が大きくなるにつれてコンポーネントの肥大化や依存関係の複雑化に必ず直面すると思います。気づけばどこからでも何でも呼べるスパゲッティコードになってしまい、コードを1箇所いじると予期せぬ場所が壊れる……というのは、多くのチームが経験しているのではないでしょうか。

私たちのチームでも、こうしたディレクトリ迷子や循環依存を防ぎ、スケールしても壊れにくい基盤を作るためにFSDを導入しました。FSDについて紹介しつつ、実際に現場で運用してみてわかったFSDの構造や、具体的なメリット・現実的な課題について振り返ります。

Feature-Sliced Design(FSD)とは

FSDは、ReactやVue、Next.jsなどのコンポーネントベースのフロントエンド向けに作られたアーキテクチャモデルです。

一言でいうと、レイヤー(階層)スライス(機能)の2つの軸でコードを整理し、上から下への単方向の依存関係を絶対ルールとして強制する仕組みです。

かつて流行したAtomic Designなどは、コンポーネントの見た目の大きさや役割で分類していました。しかしこれだと、ユーザーや商品といった、特定の業務ドメインに紐づくロジックをどこに置くべきかが曖昧になりがちでした。

FSDは、技術的な関心事(UI、API、状態管理)だけでなく、ビジネスドメイン(機能)の単位でコードを切り出すことで、パーツごとの独立性を高めます。

FSDを形作る3つの概念:Layers, Slices, Segments

FSDのディレクトリは、上から順に以下の3つの階層で成り立っています。

 ① Layers(レイヤー)

アプリ全体を役割ごとに縦に分割したものです。FSDでは標準で7つのレイヤーが定義されており、上のレイヤーは、自分より下のレイヤーにしか依存してはいけないという厳格なルールがあります。

下に行くほど汎用的(技術的)になり、上に行くほど具体的(ビジネスルールに密着したもの)になります。

  • app: アプリ全体の初期化、グローバルスタイル、プロバイダーの設定など。
  • processes: 複数のページにまたがる複雑なビジネスプロセス(認証フローや段階的なフォーム入力など)※現在は設計をシンプルに保つため、非推奨とされるケースも増えているようです。私たちも今回は作成しませんでした。
  • pages: 画面(ルート)単位のコンポーネント。
  • widgets: 画面を構成する、独立した大きめのUIブロック(例:ヘッダー、サイドバーなど)
  • features: ユーザーに価値を提供する「アクション(機能)」の単位。ビジネスロジックを含み、何かを操作するもの(例:カートに追加する、お気に入り登録する)。
  • entities: ビジネス上の実体(ドメイン)。データモデルや、そのデータに紐づく最小限のUI(例:商品カードの見た目、ユーザー情報の表示)。
  • shared: 特定のドメインに依存しない、純粋な共通モジュール(例:UIコンポーネントライブラリ、APIクライアント、日時フォーマッター)。

 ② Slices(スライス)

pages、widgets、features、entities の中に作られる、ビジネスドメインごとのディレクトリです。例えば、entities レイヤーの中に user や productというスライスを作ります。

ここで最も重要なルールがあります。

同じレイヤー内のスライス同士は、お互いをインポートしてはいけない。

なじみ深いECサイトを例にすると、features/add-to-cart(カート追加機能)が features/wishlist(お気に入り機能)を直接参照することは禁止です。これによって、機能同士の結合度が下がります。

③ Segments(セグメント)

スライスの内部を、技術的な役割でさらに細分化したものです。通常は以下の4つに整理されます。

  • ui: コンポーネント(React/Vueファイルなど)
  • model: 状態管理(Reduxなど)、Hooks、ビジネスロジック
  • api: 外部APIとの通信処理
  • lib: そのスライス内だけで使うユーティリティ関数

ディレクトリ構造の具体例

Next.jsやViteプロジェクトでFSDを採用した場合、以下のような配置になります。

src/
├── app/                  # アプリ全体のグローバル設定
│   └── providers/
├── pages/                # 各画面のルーティング表現
│   └── catalog/
├── widgets/              # 複数機能をまとめる大きなUIブロック
│   └── product-grid/
├── features/             # アクションを伴う「機能」単位
│   └── add-to-cart/
│       ├── ui/           # カート追加ボタンなど
│       ├── model/        # カート追加時のロジックや状態
│       └── index.ts      # 外部に公開する窓口(Public API)
├── entities/             # ドメインモデルと最小限のUI
│   ├── product/
│   └── user/
└── shared/               # ドメインに依存しない共通部品
    ├── ui/               # 汎用Button, Inputなど
    └── api/              # 共通のAPIクライアント

Public API(index.ts)について

FSDの運用で外せないのが、各スライスのルートに置く index.ts です。これを Public API と呼びます。スライスの内部(ui や model の深い階層)にあるファイルを、外部から直接インポートすることは禁止です。必ずこの index.ts からエクスポートされたものだけを経由して使います。これによって内部の実装をいくらいじっても、外部への影響を最小限に抑え込めます。

プロダクトに導入して感じたメリット

コードを置く場所に迷わなくなり、レビューコストが下がる

ビジネスロジックがあってユーザーのアクションをトリガーするなら features、純粋にデータを表示するだけなら entities、というように基準が明確であるため、新規コンポーネント作成時に置き場所に迷うことが少なくなりました。レビュワーも設計の意図を掴みやすいです。

依存関係が綺麗になり、リファクタリングが楽になる

「上から下」という単方向の依存ルールと「同じレイヤー内の横の参照禁止」があるため、循環依存が構造的に起きなくなります。特定の機能が不要になったときは、そのディレクトリをごっそり削除するだけで安全にコードを整理できます。

FSD導入によりぶつかった課題

① features なのかentitiesなのか迷う

運用初期に最も多かったのが、この2つの境界線の判断です。

対策:私たちのチームでは、最初は entity にデータ表示用の薄いUIだけを作り、ボタンやAPI通信などのアクションが必要になったら features 側にロジックを切り離す(または外からボタンを流し込む)という方針にしました。

例えば、「商品画像や金額」は entities に作り、「カート追加ボタンとその処理」は features に作って、上のレイヤー(widgets など)で合体させるイメージです。

② ネストが深くなり、ファイル数が増える

すべての機能に ui, model などのセグメントと index.ts を用意するため、どうしてもファイル数やインポート文の手間が増えます。

まとめ

FSDについての紹介と、実際にプロダクトに導入して感じたPros/Consについて書かせていただきました。

ルールが厳格なので、最初は学習コストがかかりましたが、現状としてメリットを感じる瞬間が多いです。

また、今後プロダクトをスケールしていくなかで、そのうまみをより感じられることに期待しています。

参考文献

FSD公式ドキュメント:https://fsd.how/ja/docs/get-started/overview/

Feature-Sliced Design – スケーラブルなフロントエンドアーキテクチャを目指して: https://zenn.dev/akfm/articles/feature-sliced-design