FSD [Feature-Sliced Design] - 02

박기찬·2026년 1월 8일

Web tech

목록 보기
14/14
post-thumbnail

FSD? Feature-Sliced Design은 러시아 개발자 커뮤니티에서 시작된, 프론트엔드 아키텍처의 난제인 의존성 지옥을 해결하기 위해 나타난 프론트엔드 아키텍처 방법론이다. 2021년경부터 본격적으로 문서화되기 시작했으며, 대규모 프론트엔드 애플리케이션의 복잡도를 관리하기 위해 만들어졌다.

이전 아티클에 이어서 이번 포스트에서는 widgets 레이어와 pages 레이어, app 레이어를 마저 서술하고, 실제로 적용 시 주의할 점을 살펴보겠다.

Widgets (위젯 레이어)

  • 여러 Entities와 Features를 조합한 독립적인 UI 블록
    이름에서 알 수 있듯이, 여러 Entities와 Features를 조합하여 특정 비즈니스 컨텍스트를 표현하는 독립적인 UI 블록이다.

즉, Entity(무엇) + Feature(어떻게) 을 모아, 사용자에게 실제 서비스 공간을 제공하는 역할을 한다.

합치는 기준은 섹터별로 나뉘는데,

widgets/
  header/          # 헤더 (로고 + 검색 + 로그인버튼)
  sidebar/         # 사이드바
  product-list/    # 상품 목록 (필터 + 정렬 + 카드들)
  footer/

위 구조처럼, 헤더, 사이드바, 리스트, 푸터와 같은 범위로 나뉜다. 즉, 페이지에서 떼어낼 수 있는 단위이다.

재사용 가능하지만 특정 비즈니스 컨택스트가 존재하며, widget 안에서 여러 feature을 조합할 수 있다.

중요한건 자기완결성인데, 페이지에서 뚝 떼어내어 다른 페이지에 붙여도 그대로 동작해야한다. 즉, 데이터 패칭, 상태 관리, UI를 모두 처리한다는 말이다.

그러나, Widget이 반드시 여러 페이지에서 재사용되어야 하는 건 아니다. 한 페이지에서만 쓰이더라도, 독립적인 기능 븡록이라면 Widget으로 분리하는게 맞다.

1. Page에 의존하지 않고 동작해야 한다.

2. 위젯은 필요한 데이터를 스스로 요청하는 트리거 역할을 하지만 FSD 공식 문서에서는 위젯이 직접 API 호출 로직을 소유하는 것은 지양하고, Entities나 Features에서 제공하는 API/Hook을 가져다 쓰는 것을 권장한다.

3. Contextual : Shared 레이어와 달리, 특정 비즈니스 도메인에 종속된다. (ex. Header 컴포넌트의 경우, 내부에 User 정보와 Search 기능이 결합됨)

Widget은 다른 Widget을 직접 참조하지 않는 것을 원칙으로 하며, 필요한 조합은 Page 레이어에서 수행한다.


Pages (페이지 레이어)

  • 라우팅 단위의 페이지 컴포넌트
    페이지 레이어는 라우팅 단위로 나누어진, 실제 페이지 역할을 하는 컴포넌트 레이어다.

이 레이어에서는 단순히 위젯들을 나열하지 않고, 화면에 어디에 위치할지 등의 레이아웃 스켈레톤을 본격적으로 구성하는 공간이다. 즉, 하위 레이어들을 조합해 페이지별 UI 시나리오를 완성하는 역할을 한다.

pages/
  home/
  product-detail/
  cart/
  checkout/
  my-page/

계층적인 구조로 인해, 여러 Features, Entities나 Widgets 레이어가 결합되어 있는 모습이다.

// pages/product-detail/ui/ProductDetailPage.tsx
import { useParams } from 'react-router-dom';
import { Header } from '@/widgets/header';
import { ProductInfo } from '@/widgets/product-info';
import { AddToCartButton } from '@/features/product/add-to-cart';
import { ReviewList } from '@/widgets/review-list';

export const ProductDetailPage = () => {
  const { productId } = useParams();

  return (
    <div>
      <Header />
      <main>
        <ProductInfo productId={productId} />
        <AddToCartButton productId={productId} />
        <ReviewList productId={productId} />
      </main>
    </div>
  );
};

페이지 레이어는 로직을 최소화하고, 조립 기능만 하기를 지향한다.

내부에 여러 개의 useEffect나 복잡한 상태 관리 로직이 들어가는 순간, FSD의 의미가 퇴색되니 복잡한 로직은 Features/Widgets 레이어로 미뤄야한다.

Page 진입(라우트 활성화)은 데이터 패칭 사이클의 기준점이며 실제로 패칭을 하는건 Widget / Feature / Entity 내부에서 수행된다.

해당 레이어에서 Prefetching이나, 에러나 로딩 상태를 보여줄지(Suspense/Error Boundary)를 결정하기도 한다.

또한,

- SEO meta
- 페이지 통계
- layout 전용 정보
- 여러 위젯이 동일한 데이터 의존

위와 같은 경우에는 Page 레이어에서 직접 데이터 패칭이 발생하기도 한다. 하지만 이 경우에도 Page가 데이터 소유자가 되기보다는, 여러 하위 레이어에서 공통으로 사용하는 데이터를 조율하거나, prefetch/orchestration 목적으로 패칭을 수행하는 경우에 한정된다.


App (앱 레이어)

  • 애플리케이션 초기화, 전역 설정

가장 최상위 계층인 앱 레이어이다. 기존 App 컴포넌트와 역할이 크게 다르지 않고, 프로젝트의 모든 계층을 하나로 묶어주는 역할을 수행한다.

비즈니스 로직이 들어가는 것이 아니라, 애플리케이션이 실행되기 위한 환경설정을 완료하는 곳이다.

기존 프로젝트에서는 App.tsx 하나에 여러 개의 Provider와 Style, Router 로직이 뒤섞여 있는 경우가 있는데, FSD에서는 이를 세부 세그먼트로 나누어서 관리하게 된다.

  • providers/ : QueryClient, ThemeProvider, Redux Store, AuthContext
  • router/ : 모든 pages를 가져와 실제 경로와 연결하는 공간
  • styles/ : Css Reset, 디자인 시스템의 변수(Variable), 전역 폰트
app/
  providers/       # Provider 조합
  styles/          # 전역 스타일
  router/          # 라우팅 설정
  index.tsx        # 진입점

실제 코드 예시

// app/index.tsx
import { AppProvider } from './providers';
import { AppRouter } from './router';
import './styles/index.css';

export const App = () => {
  return (
    <AppProvider>
      <AppRouter />
    </AppProvider>
  );
};

지금까지가 전체적인 레이어 구조이다. 구조만 알면 FSD 아키텍쳐를 잘 사용할 수 있을까? 중요한 실무적 요소는 내 프로젝트에 잘 적용하는 방법, 우리 팀에 빠르고 정확하게 적용하는 것이다.

app
 └─ pages
     └─ widgets
         └─ features
             └─ entities
                 └─ shared

실무 폴더 구조 적용 시

1. 점진적으로 작게 시작하기

만약, 처음부터 fsd를 도입한다면

src/
  shared/ui/        # UI 킷만
  entities/user/    # 핵심 엔티티 하나
  features/auth/    # 주요 기능 하나
  pages/home/       # 메인 페이지
  app/              # 설정

위와 같은 기본적인 구조만 유지하고, 슬라이스(도메인), 세그먼트(역할별 분류)는 필요할 때만 만든다.

2. 마이그레이션은 점진적으로

기존 프로젝트를 FSD로 변경하는 경우에는 점진적으로 시행한다.

2.1 shared부터 시작

// 기존 components/Button.tsx
// 변경 shared/ui/Button/Button.tsx

// 모든 import 경로 변경
- import { Button } from '@/components/Button';
+ import { Button } from '@/shared/ui';

2.2 entities 분리

// User 관련 코드 모으기
entities/user/
  model/types.ts      ← types/user.ts 에서 이동
  model/store.ts      ← store/userStore.ts 에서 이동
  ui/UserCard.tsx     ← components/UserCard.tsx 에서 이동

2.3 features 분리

// 기능별로 하나씩
features/auth/login/1주차
features/auth/register/2주차
features/product/add-to-cart/3주차

2.4 pages, widgets 구성

위와 같은 순서를 지키되, 실무적으로 고려해야할 부분이 있다.

- 3번 이상 반복되면 Shared 레이어로 이동 (team convention)

- 컴포넌트가 500줄을 넘으면 분리 고려 (team convention)

- 단순히 줄 수 뿐 아니라, 하나의 컴포넌트가 두 개 이상의 비즈니스 도메인을 다루기 시작하면 분리 고려

- 슬라이스 간의 직접적인 참조는 금지. 슬라이스끼리 데이터가 필요하다면, 그들의 부모인 Widget이나 Page에서 데이터를 조립하여 전달.

앞선 아티클에서 설명한 Public API를 준수하도록 노력해야한다. FSD에서 Public API는 내부 구현을 감추는 캡슐화의 핵심이다.

// ❌ 나쁜 예
import { LoginForm } from '@/features/auth/login/ui/LoginForm';

// ✅ 좋은 예
import { LoginForm } from '@/features/auth/login';

// features/auth/login/index.ts
export { LoginForm } from './ui/LoginForm';
export type { LoginFormProps } from './ui/LoginForm';

또한 팀 내부적으로, 세그먼트의 사용 기준을 정해도 좋다.

ui/     - 항상 필수
model/  - 상태나 로직이 있을 때
api/    - API 호출이 있을 때
lib/    - 내부 유틸리티가 있을  (복수개)
config/ - 설정값이 필요할 때

아직 FSD에 대한 기준이 명확하지 않고, 이론적인 내용이 헷갈린다면 다음과 같은 질문을 답할 수 있는지를 생각해보자.

1. Entity와 Feature 구분이 애매한가?
2. Widget과 Feature 차이가 뭔가?
3. Shared가 너무 커질때는?
4. Public API를 적용해야 하는 이유는?
5. 의존성 방향은 어디로 흐르는가?
6. Next.js에서는 어떻게 적용되는가?
7. TypeScript의 타입은 어디에 위치하는가?

또한, FSD를 적용한 경우에 다음과 같은 지표를 완수하였는지를 체크해보자.

1. 평균 컴포넌트 크기가 감소하였는가?
2. 새 기능 추가 시 시간이 단축되었는가?
3. 버그 수정 시 시간이 단축되었는가?
4. 코드 리뷰 시 시간이 단축되었는가?
5. 새 팀원의 온보딩 시간이 단축되었는가?
6. 슬라이스 이름이 기술적 용어(ex. api-slice)가 아닌 비즈니스 용어(ex. order-management)인가?
7. 모든 슬라이스에 index.ts가 존재하며 외부 노출을 제어하고 있는가?
8. 하위 레이어가 상위 레이어를 참조하지 못하도록 강제했는가?

실제로 이러한 아키텍처를 적용할때는, 오버엔지니어링을 항상 조심해야한다. 겉보기엔 최적화가 잘되있고, 유지보수측면에서도 좋아보이지만, 모든 기술에는 트레이드오프가 존재하기 마련이다.

따라서, 중대형 프로젝트, 기능이 계속 추가되는 서비스나 명확히 확장이 될 비즈니스 도메인이 있을 경우 FSD 도입이 적합하며, 작은 프로젝트나 정적인 웹사이트에서는 과한 설계일 가능성이 높다. 물론 아키텍처에 대한 이해와 이론적인 숙지가 완벽하다면, 어떤 프로젝트에 적용하더라도 문제가 없겠지만 다른 팀원들의 러닝커브에 대한 비용, 필요없는 최적화가 아닌지에 대한 고민은 필요하다. 아키텍처는 목적이 아니라 수단이다.


관련문서

0개의 댓글