이 글은 2026년 8월 Feature-Sliced Design 2.1 기준으로 내용을 다시 확인하고 수정했어요.

React 프로젝트가 커지면 components, hooks, utils처럼 code 종류별로 나눈 폴더만으로는 변경 범위를 파악하기 어려워져요. 하나의 사용자 흐름을 수정하려고 여러 폴더를 오가고, 서로 관련 없는 code가 같은 폴더에 쌓이기 때문이에요.

Feature-Sliced Design(FSD)은 frontend application의 code를 layer, slice, segment로 나누는 architecture methodology예요. React에 종속된 library나 folder template은 아니며, business 변화가 생겨도 의존성 방향을 예측할 수 있도록 규칙을 제공해요.

FSD를 도입한다고 모든 layer를 처음부터 만들 필요는 없어요. 최신 FSD 2.1은 page에 code를 먼저 모으고, 실제 재사용과 분리 필요성이 생겼을 때 아래 layer로 추출하는 pages-first 접근을 권장해요.

FSD가 해결하려는 문제

code 종류를 기준으로만 나눈 구조는 작은 프로젝트에서 단순하고 편리해요.

text
src/
  components/
  hooks/
  services/
  utils/

규모가 커지면 components에 서로 다른 business 영역의 UI가 섞이고, utils에는 목적이 다른 함수가 계속 추가되기 쉬워요. 폴더 이름만으로는 어떤 사용자 흐름에 속하는지, 어디까지 함께 수정해야 하는지 알기 어려워져요.

FSD는 책임과 의존성의 크기에 따라 layer를 나누고, 각 layer 안에서는 product 의미에 따라 slice를 만들어요. 이를 통해 관련 code를 가까이 두고, 상위 수준의 화면이 하위 수준의 module을 조합하도록 의존성 방향을 제한해요.

FSD가 특정 architecture보다 항상 우월한 것은 아니에요. 현재 구조가 팀에 문제를 만들지 않는 작은 프로젝트라면 migration 비용이 더 클 수 있어요. 팀이 겪는 탐색 비용과 결합 문제를 먼저 확인한 뒤 도입하는 편이 좋아요.

Layer, slice, segment

FSD의 구조는 세 단계로 이해할 수 있어요.

  • Layer: code가 맡는 책임과 의존성의 범위를 나타내요.
  • Slice: product나 business에서 의미 있는 영역으로 code를 묶어요.
  • Segment: slice 내부를 기술적 목적에 따라 나눠요.

실제 프로젝트에서는 다음 여섯 layer를 주로 사용해요.

text
src/
  app/
  pages/
  widgets/
  features/
  entities/
  shared/

공식 사양에는 processes도 남아 있지만 deprecated 상태예요. 새 프로젝트에서는 사용하지 않고, 여러 page에 걸친 흐름은 featuresapp의 router 수준 logic으로 구성하는 것이 현재 권장 방식이에요.

모든 layer를 만들 필요도 없어요. FSD의 기본 예제도 app, pages, shared만으로 시작해요. widgets, features, entities는 분리했을 때 탐색과 재사용이 실제로 좋아지는 경우에 추가하면 돼요.

Layer import rule

FSD에서 가장 중요한 규칙은 상위 layer가 자신보다 아래에 있는 layer만 import하는 것이에요.

text
app

pages

widgets

features

entities

shared

예를 들어 features/add-to-cartentities/productshared/api를 사용할 수 있어요. 반대로 entities/productfeatures/add-to-cart를 import하거나, features/add-to-cart가 같은 layer의 features/remove-from-cart를 직접 import하면 규칙을 위반해요.

같은 slice 내부에서는 segment 사이를 자유롭게 참조할 수 있어요. appshared는 business slice를 두지 않고 segment로 바로 나누기 때문에 두 layer 내부의 segment끼리도 import할 수 있어요.

Slice와 segment

pages, widgets, features, entities에는 먼저 slice를 만들고 그 안을 segment로 나눠요.

text
features/
  add-to-cart/      # slice
    ui/             # segment
    model/          # segment
    api/            # segment
    index.ts

slice 이름은 product, add-to-cart, catalog-page처럼 application에서 어떤 의미인지 드러내야 해요. 서로 다른 slice는 같은 layer에서 직접 의존하지 않는 것이 원칙이에요.

segment는 code 종류보다 목적을 표현해요. 공식 문서에서 자주 사용하는 이름은 다음과 같아요.

  • ui: 화면 표시와 관련된 component, style, formatter
  • api: backend 요청 함수, request/response type, mapper
  • model: schema, state, business logic
  • lib: 해당 slice에서 사용하는 작은 library
  • config: 설정과 feature flag

components, hooks, types처럼 code 형태만 나타내는 segment를 반복해서 만들면 관련 logic이 다시 흩어질 수 있어요. 예를 들어 장바구니 state와 관련 type, hook은 각각 별도 폴더로 보내기보다 model 안에서 함께 관리하는 편이 목적을 파악하기 쉬워요.

각 layer의 역할

app

app은 application을 실행하는 데 필요한 전역 설정을 담당해요. router, provider, global store 설정, global style, entrypoint가 여기에 들어갈 수 있어요.

text
app/
  routes/
  providers/
  styles/
  store/

특정 page나 feature에서만 사용하는 business logic을 app에 올리면 전역 code가 빠르게 커져요. application 전체를 연결하는 code만 두는 것이 좋아요.

pages

pages는 router에 연결할 수 있는 완성된 화면을 slice로 관리해요. FSD 2.1에서는 한 page에서만 사용하는 UI, request, 작은 state를 page slice에 그대로 두는 것을 권장해요.

text
pages/
  product-catalog/
    ui/
      ProductCatalogPage.tsx
      ProductFilter.tsx
    api/
      fetchProducts.ts
    model/
      filterSchema.ts
    index.ts

ProductFilter가 이 page에서만 사용된다면 서둘러 features로 옮기지 않아도 돼요. page가 충분히 탐색 가능하다면 많은 code를 포함해도 규칙 위반이 아니에요.

widgets

widgets는 data와 interaction을 포함할 수 있는 크고 독립적인 UI block이에요. 여러 page에서 재사용하거나, 한 page가 여러 개의 큰 독립 block으로 구성될 때 유용해요.

text
widgets/
  product-list/
    ui/
    api/
    model/
    index.ts

Header, pagination을 포함한 product list, 독립적인 sidebar처럼 멀리서 보아도 하나의 완성된 block으로 인식되는 요소가 후보예요. 반면 button, modal shell, dropdown 같은 범용 UI는 widgets가 아니라 shared/ui에 두는 편이 맞아요.

widget이 business logic이나 API를 가지면 안 된다는 규칙은 없어요. FSD 2.1에서는 해당 widget 밖에서 아직 재사용하지 않는 logic을 widget 안에 함께 두어 cohesion을 유지하도록 설명해요.

features

features는 사용자가 수행하고 싶어 하는 product interaction을 재사용 가능한 형태로 구현해요. 댓글 작성, 장바구니 추가, profile 수정처럼 business value가 있는 행동이 여기에 해당할 수 있어요.

text
features/
  add-to-cart/
    ui/
      AddToCartButton.tsx
    model/
      useAddToCart.ts
    api/
      addProductToCart.ts
    index.ts

사용자의 모든 행동을 feature로 만들 필요는 없어요. 한 page에서만 사용하는 form이나 interaction은 page에 두고, 여러 page나 widget에서 같은 행동을 재사용해야 할 때 feature로 추출하는 것이 좋은 판단 기준이에요.

entities

entities는 application이 다루는 business 개념을 표현해요. 사용자, 상품, 주문처럼 제품을 설명할 때 주어나 목적어로 등장하는 개념이 후보예요.

text
entities/
  product/
    ui/
      ProductCard.tsx
    model/
      productSchema.ts
    api/
      fetchProduct.ts
    index.ts

entity의 UI는 완성된 page block일 필요가 없어요. 여러 화면에서 같은 상품 표현을 재사용할 수 있도록 ProductCard처럼 시각적 표현만 제공하고, 장바구니 추가 같은 행동은 상위 layer에서 결합할 수 있어요.

같은 layer의 entity slice끼리는 기본적으로 직접 import하지 않아요. 실제 data 관계 때문에 cross-import가 필요하다면 공식 @x public API를 사용해 관계를 명시할 수 있지만, 먼저 interaction을 featurespages 같은 상위 layer에서 조합할 수 있는지 검토하는 편이 좋아요.

shared

shared는 application의 기반이 되는 code를 두는 layer예요. API client, UI kit, 환경 설정, route 상수, i18n 설정, 목적이 명확한 작은 library가 여기에 들어갈 수 있어요.

text
shared/
  api/
    client.ts
  ui/
    button/
    modal/
  lib/
    date/
    text/
  config/
  routes/
  i18n/

shared가 특정 application의 흔적을 전혀 가지면 안 되는 것은 아니에요. 회사 logo나 application route처럼 application을 아는 code도 둘 수 있어요. 다만 특정 상품을 구매할 수 있는지 판단하는 것과 같은 business logic은 포함하지 않는 것이 원칙이에요.

범용이라는 이유만으로 모든 type과 utility를 shared/types, shared/utils에 모으면 다시 dump folder가 되기 쉬워요. shared/lib/date처럼 책임이 분명한 단위로 나누고, business 의미가 있는 type은 해당 page나 entity 가까이에 두는 편이 좋아요.

Public API로 경계 만들기

각 slice는 외부에서 사용할 수 있는 항목을 public API로 명시해야 해요. appshared처럼 slice가 없는 layer에서는 각 segment가 public API를 제공해요. 일반적으로 slice나 segment root의 index.ts에서 필요한 항목만 다시 export해요.

ts
// features/add-to-cart/index.ts
export { AddToCartButton } from "./ui/AddToCartButton";

외부에서는 내부 파일을 직접 참조하지 않고 public API를 사용해요.

ts
// 권장
import { AddToCartButton } from "@/features/add-to-cart";
 
// 피해야 함: slice 내부 구조에 의존함
import { AddToCartButton } from "@/features/add-to-cart/ui/AddToCartButton";

export *로 내부 항목을 모두 노출하면 무엇이 계약인지 알기 어렵고 refactoring 범위가 넓어져요. 필요한 export를 명시하면 slice 내부 구조를 바꿔도 사용하는 쪽에 미치는 영향을 줄일 수 있어요.

Pages-first로 적용하기

새 React 프로젝트에 FSD를 적용할 때는 다음 순서가 단순해요.

  1. app, pages, shared만 만들어요.
  2. router와 provider는 app, route별 화면과 전용 logic은 pages에 둬요.
  3. API client와 UI kit처럼 business logic이 없는 기반 code만 shared로 옮겨요.
  4. 여러 page에서 재사용하는 큰 block이 생기면 widgets로 추출해요.
  5. 여러 page나 widget에서 반복되는 사용자 행동은 features로 추출해요.
  6. 여러 곳에서 같은 business 개념과 표현을 공유할 때 entities를 만들어요.
  7. 각 slice에 public API를 만들고 layer import rule을 검사해요.

이 순서는 먼저 entity와 feature를 모두 설계한 뒤 page를 조립하는 방식보다 관련 code를 가까이 유지하기 쉬워요. 필요가 확인되기 전에는 page에 code를 남겨두므로 불필요한 추상화도 줄일 수 있어요.

기존 프로젝트는 한 번에 이동하지 않아도 돼요. 공식 migration guide도 appshared의 기반을 정리하고, 기존 UI를 pageswidgets로 넓게 옮긴 다음 import 위반을 줄이면서 필요한 entitiesfeatures를 추출하는 점진적 방식을 제안해요.

적용할 때 자주 생기는 문제

  • 모든 click과 form을 features로 만들면 중요한 product 기능을 찾기 어려워져요.
  • button과 modal을 widgets에 넣으면 UI kit와 business block의 경계가 흐려져요.
  • 같은 layer의 slice를 직접 import하면 변경 영향이 옆 slice로 퍼져요.
  • hooks, types, utils 같은 폴더를 모든 slice에 기계적으로 만들면 code가 다시 종류별로 흩어져요.
  • public API에서 export *를 사용하면 내부 구현이 외부 계약으로 새어 나가기 쉬워요.
  • 현재 구조가 문제없이 작동하는 프로젝트를 이유 없이 전면 migration하면 학습과 이동 비용만 늘 수 있어요.

FSD의 핵심은 폴더 이름을 전부 만드는 데 있지 않아요. page에 관련 code를 모으고, 재사용이 확인될 때만 아래 layer로 추출하며, import 방향과 public API로 경계를 지키는 것이 더 중요해요.

참고 자료