본문 바로가기
React

[아키텍처] FSD 아키텍처 : Feature-Sliced Design

by eunsoa 2025. 6. 12.

 

 

🔽 목표

'팀 프로젝트를 잘하려면 어떻게 구조를 설계/분리해야할까?'라는 궁금증이 생겼다. 

'잘 분리'하기 위해서는 재사용성이 좋게 역할과 책임을 분리해야하고, 다른사람이 이해하기 쉬운 코드가 가장 중요하다고 생각했다.

 

그래서 다른 개발자가 협업함에 있어서 가독성이 좋고, 역할과 책임이 잘 분리된 구조를 설계해보는걸 목표로 진행중이던 프로젝트를 수정해봤다.

 

 

 

🔽 현재 구조적 문제 상황

아래는 기존 프로젝트 구조로 페이지 단위로 분리되었다.

react-shopping-cart
├─ src
│  ├─ apis                             👈 api
│  │  ├─ cartItems
│  │  └─ httpClient.ts
│  ├─ pages                              👈 페이지별
│  │  ├─ cart
│  │  │  ├─ CartContent
│  │  │  │  ├─ CartList
│  │  │  │  ├─ EmptyCartContainer
│  │  │  │  └─ PriceContainer
│  │  │  └─ hooks
│  │  └─ order
│  ├─ shared                              👈 프로젝트 공용 로직
│  │  ├─ components
│  │  ├─ config
│  │  └─ hooks

 

요구사항이 크지 않았을 때는 하나의 페이지에서 ui와 상태가 관리되었지만, 프로젝트의 규모가 커지고 요구사항이 많아짐에 따라 어떻게 하면 중복을 줄이고, 각각의 도메인에 맞게 프로젝트 구조를 분리하고 설계할 수 있을지 궁금해졌다.

 

 

 

 

 

🔽 여러 대안 검토 및 선택 근거

 

1. 페이지별 분리

- 설명 :

  - 컴포넌트, 페이지, 서비스 등 기술적 타입별로 폴더를 구분하는 전통적인 구조.

- 장점 : 

  - 단순하고 빠르게 분리 가능.

  - 소규모 프로젝트, MVP, 단기 프로젝트에 적합.

- 단점 :

  - 요구사항이 커지고 페이지가 늘어나면 중복코드, 결합도 증가.

  - 상태 및 비즈니스 로직의 위치가 불분명해 유지보수와 협업이 어려워짐.

  - 기능 단위로 재사용 or 확장하기 힘듦.

 

 

2. Atomic Design

- 설명 :

  - UI를 원자(Atom) → 분자(Molecule) → 유기체(Organism) → 템플릿 → 페이지로 계층화하는 구조.

- 장점 : 

  - UI 컴포넌트 재사용성과 일관성 극대화

  - 디자인 시스템, UI중심 프로젝트에 적합

- 단점

  - UI레벨에 집중되어 있어 비즈니스 로직, 상태 관리, 도메인 분리에 한계

  - 프로젝트가 커질수록 폴더 구조가 복잡해지고, 실제 기능 단위 협업에는 제약

 

3. Clean Architecture

- 설명 : 

  - 도메인, 애플리케이션, 프레젠테이션 등 계층별로 명확히 분리하는 백엔드 지향 구조.

- 장점 :

  - 복잡한 비즈니스 규칙, 도메인 중심 개발에 적합.

  - 테스트하기 용이하고, 명확한 책임 분리가 가능.

- 단점 : 

  - 프론트엔드에서는 오버엔지니어링일 수 있음.

  - UI/UX 중심 프로젝트에서는 개발 속도가 저하되고 러닝커브가 존재함.

 

4. Feature-Sliced Design (FSD)

- 설명 : 

  - 기능(Feature) 단위로 폴더를 분리하고, 각 기능 내에 UI, 상태, API, 도메인 로직을 캡슐화.

- 장점 : 

  - 대규모, 협업 프로젝트에서 확장성과 유지보수성 뛰어남.

  - 명확한 역할·책임 분리, 중복 최소화, 팀 단위 병렬 개발 용이.

  - 코드베이스가 커져도 구조적 일관성 유지, 신규 기능 추가가 쉬움.

  - 의존성, 캡슐화, 계층적 흐름(하위 레이어는 상위 레이어를 모름)을 통한 견고한 설계.

- 단점 :

  - 초기 설계, 도입시 러닝커브 존재.

  - 소규모, 단기 프로젝트에는 과할 수 있음.

 

 

위의 4가지 선택지 비교를 정리하면 아래와 같다.

설계 구조 장점 단점
페이지별 분리 단순, 빠른 시작, 소규모에 적합 중복, 결합도↑, 유지보수 어려움, 재사용성↓
Atomic Design UI 재사용, 일관성, 디자인 시스템에 적합 비즈니스 로직/상태 분리에 한계, 대형 프로젝트엔 복잡
Clean Architecture 도메인 중심, 테스트 용이, 책임 분리 프론트엔드엔 오버엔지니어링, 러닝커브, UI/UX엔 비효율적
Feature-Sliced
Design (FSD)
확장성, 유지보수성↑, 역할·책임 명확, 협업/테스트/온보딩 용이 초기 러닝커브, 소규모엔 과할 수 있음

 

 

 

 

 

🔽 FSD(Feature-Sliced Design) 선택 이유

- 요구사항이 커지고, 여러 페이지에서 공통 UI/상태가 중복되는 문제를 해결하고 싶었음.
- 팀 프로젝트를 대비해 각 기능 단위로 역할과 책임이 명확하고, 협업에 유리한 구조를 공부해보고 싶었음.
- 기존 구조의 한계(중복, 결합도, 유지보수성 저하)를 극복하고, 코드 재사용성과 확장성을 높이고자 했음.
- FSD는 기능별로 캡슐화와 계층적 분리를 제공해 실무에서의 유지보수·협업·테스트·온보딩 등 모든 측면에서 장점이 명확했음.
- 실제 도입 후, 코드 중복이 줄고 신규 기능 추가·수정 시 영향 범위가 명확해졌음.

 

 

 

 

 

🔽 FSD 아키텍쳐 적용

src/
├── shared/          # 공유 유틸리티, 타입, 상수
├── entities/     # 비즈니스 엔티티 (cart, coupon)
├── features/     # 비즈니스 로직 (order, cart, coupon)
├── widget/        # 독립적인 위젯 컴포넌트
├── pages/       # 페이지 컴포넌트
└── app/      # 앱의 초기화, 전역 설정

- shared → entities → features → widgets → pages→ app 순으로 의존성이 흐르며, 상위 계층은 하위 계층에만 의존하도록 했어요.
각 계층은 아래 같이 각자의 목적에 맞게 분류했다.


  - shared
    : 재사용 가능한 컴포넌트, utils, 비즈니스 로직에 종속되지 않음.
  - entities
    : 도메인 데이터와 그자체의 로직으로 '무엇을'에 해당. 상태 관리, 타입 정의, api 통신이 해당함.
  - features
    : 여러 엔티티를 조합하여 사용자의 요구(비즈니스 플로우)를 구현하여 '어떻게'에 해당. 사용자 시나리오에 맞도록 entities를 조합하여 활용한 ui 컴포넌트, 상태관리, api 호출 등이 여기에 해당
  - widget
    : 페이지를 구성하는 큰 UI 블록으로 page에서 사용할수 있을 단위. entities와 feature를 조합해서 만든 의미있는 블록 조합.
  - page
    : widget을 조합해서 만든 하나의 페이지

 

 

 

🔽 적용 효과

예시로 ui를 그리는 컴포넌트의 경우에도 한번더 추상화하여서 shared 와 widget으로 분리했다.

 

UI 공통 로직을 분리하여 PriceInfoContainer는 순수하게 UI 렌더링만 담당하고, 도메인별 비즈니스 로직이 포함된 경우 아래와 같이 분리했어요.

// 1. UI 공통 로직 (@shared/components/PriceInfoContainer)
const PriceInfoContainer = ({ children, freeDeliveryLimit }) => {
  return (
    <Container>
      <Description description={`무료 배송 안내...`} />
      {children}
    </Container>
  );
};

// 2. 도메인별 컨테이너
// Cart  (@widget/cart/PriceContainer)
const CartPriceContainer = ({ orderPrice, deliveryFee, orderTotalPrice }) => {
  const orderItems = [
    { title: '주문 금액', price: orderPrice },
    { title: '배송비', price: deliveryFee },
  ];
  return (
    <PriceInfoContainer freeDeliveryLimit={FREE_DELIVERY_LIMIT}>
      <PriceBox items={orderItems} />
    </PriceInfoContainer>
  );
};

// Order (@widget/order/PriceContainer)
const OrderPriceContainer = ({ orderPrice, couponDiscountPrice, ... }) => {
  const orderItems = [
    { title: '주문 금액', price: orderPrice },
    { title: '쿠폰 할인 금액', price: -couponDiscountPrice },
  ];
  return (
    <PriceInfoContainer freeDeliveryLimit={FREE_DELIVERY_LIMIT}>
      <PriceBox items={orderItems} />
    </PriceInfoContainer>
  );
};