
이전 글에서는 CDD(Component-Driven Development)를 통해 독립적이고 재사용 가능한 UI 블록을 만드는 방식에 대해 정리했다. CDD 패턴을 적용하면서 컴포넌트를 더 작은 단위로 나누고, UI의 일관성과 재사용성도 어느 정도 확보할 수 있었다.
하지만 프로젝트 규모가 커지고, 도메인별 비즈니스 로직이 복잡해지면서 새로운 고민이 생겼다. 페이지 수가 늘어나고 재사용 가능한 UI 블록도 많아졌을 때, 이 코드들을 어떤 기준으로 그룹화하고 배치해야 유지보수가 쉬워질까?
처음 FSD(Feature-Sliced Design)를 접했을 때는 CDD와 대립하는 개념처럼 느껴졌다. CDD가 컴포넌트를 잘게 나누는 방식이라면, FSD는 그와 다른 구조를 강제하는 아키텍처처럼 보였기 때문이다.
하지만 관련 자료를 찾아보고 개념을 정리하면서 생각이 바뀌었다. CDD와 FSD는 대립하는 개념이 아니라, 서로 다른 문제를 해결하는 상호보완적인 개념에 가깝다.
CDD는 “UI를 어떻게 독립적인 컴포넌트로 만들 것인가”에 초점을 둔다. 반면 FSD는 “그 컴포넌트와 비즈니스 로직을 프로젝트 안에서 어디에 배치하고, 어떤 방향으로 의존성을 허용할 것인가”에 초점을 둔다.
FSD는 대규모 프론트엔드 애플리케이션의 예측 가능성과 유지보수성을 높이기 위한 아키텍처 방법론이다.
프로젝트가 커질수록 흔히 발생하는 문제가 있다. “이 컴포넌트를 수정하면 어디까지 영향이 갈지 모르겠다”, “비슷한 로직이 여러 곳에 흩어져 있다”, “어떤 코드를 어디에 둬야 할지 애매하다” 같은 문제다.
FSD는 이런 문제를 해결하기 위해 코드를 단순히 기술적 기준, 예를 들면 components, hooks, services 같은 기준으로만 나누지 않는다. 대신 애플리케이션의 비즈니스 의미와 사용자 기능을 중심으로 코드를 나누고, 레이어 간 의존성 규칙을 통해 결합도를 통제한다.
즉, FSD의 핵심은 다음과 같이 정리할 수 있다.
“코드를 비즈니스 단위로 나누고, 의존성 방향을 제한해 변경의 영향을 예측 가능하게 만드는 것”
FSD는 크게 세 가지 축으로 구성된다.

Layer는 애플리케이션의 최상위 구조를 나타낸다. 각 레이어는 코드의 책임과 의존성 방향을 결정한다.
일반적으로 다음과 같은 레이어를 사용한다.
app
pages
widgets
features
entities
shared
상위 레이어는 하위 레이어를 사용할 수 있지만, 하위 레이어는 상위 레이어를 사용할 수 없다. 예를 들어 features는 entities와 shared를 사용할 수 있지만, widgets나 pages를 직접 참조해서는 안 된다.
Slice는 레이어 내부에서 비즈니스 도메인이나 기능 단위로 코드를 나누는 기준이다.
예를 들어 features/AddToCart, entities/Product, entities/User 같은 구조가 Slice에 해당한다.
Slice는 독립성과 응집도를 갖는 것이 중요하다. 같은 레이어에 있는 다른 Slice에 직접 의존하기 시작하면, 기능 간 결합도가 높아지고 변경의 영향 범위를 예측하기 어려워진다.
Segment는 Slice 내부에서 코드의 역할에 따라 나누는 기준이다.
대표적으로 다음과 같은 Segment를 사용할 수 있다.
ui # 화면을 그리는 컴포넌트
model # 상태, 비즈니스 로직, store, hook
api # 서버 요청, API 함수
lib # 해당 slice 내부에서 사용하는 보조 로직
config # 설정값, feature flag 등
즉 FSD는 다음과 같은 흐름으로 코드를 찾게 만든다.
Layer → Slice → Segment
예를 들어 아래 경로를 보면,
src/features/AddToCart/ui/
각각 다음과 같이 해석할 수 있다.
features → 사용자 행동 중심의 기능 레이어
AddToCart → 장바구니 담기 기능 Slice
ui → 해당 기능의 UI Segment
이처럼 FSD는 큰 구조에서 작은 역할로 내려가며 코드를 배치하기 때문에, 프로젝트가 커져도 코드가 있어야 할 위치를 비교적 명확하게 판단할 수 있다.
전자상거래나 대형 관리 시스템을 예로 들면 다음과 같은 구조를 생각해볼 수 있다.
src/
├── app/
│ └── providers/
│
├── pages/
│ └── order/
│
├── widgets/
│ └── ProductSection/
│
├── features/
│ └── AddToCart/
│ ├── ui/
│ ├── model/
│ ├── api/
│ └── index.ts
│
├── entities/
│ ├── Product/
│ │ ├── ui/
│ │ ├── model/
│ │ ├── api/
│ │ └── index.ts
│ │
│ └── User/
│ ├── ui/
│ ├── model/
│ └── index.ts
│
└── shared/
├── ui/
├── api/
├── lib/
└── config/
여기서 shared/ui는 CDD로 만들어진 Button, Modal, Input 같은 공통 UI 컴포넌트가 위치하기 좋은 영역이다. 단, 이 컴포넌트들은 특정 비즈니스 도메인에 강하게 의존하지 않아야 한다.
반면 features/AddToCart/ui에 있는 UI는 장바구니 담기라는 구체적인 사용자 행동에 묶여 있다. 따라서 단순히 버튼처럼 생겼다고 해서 무조건 shared/ui로 빼기보다는, 그 컴포넌트가 비즈니스 의미를 가지는지 먼저 판단해야 한다.
하나의 비즈니스 로직에서 여러 UI가 나와야 할 때는 어떻게 해야 할까?
예를 들어 features/AddToCart라는 Slice가 있고, 이 안의 model이 장바구니 담기 상태와 로직을 담당한다고 해보자.
이때 같은 로직을 사용하는 UI가 여러 개 있을 수 있다.
features/AddToCart/
├── ui/
│ ├── DesktopAddToCartButton.tsx
│ ├── MobileAddToCartButton.tsx
│ └── HeaderMiniCartButton.tsx
├── model/
│ └── useAddToCart.ts
└── index.ts
이 경우 굳이 Feature를 여러 개로 쪼갤 필요는 없다. 같은 비즈니스 목적을 가진 UI라면 하나의 Slice 안에서 여러 UI 컴포넌트가 같은 model을 바라보도록 구성할 수 있다.
중요한 기준은 “UI가 여러 개인가?”가 아니라 “비즈니스 목적이 같은가?”이다.
비즈니스 목적이 같다면 하나의 Slice 안에 두는 것이 응집도를 높인다. 반대로 목적이 달라졌다면 별도의 Feature로 분리하는 것을 고려할 수 있다.
FSD에서 가장 조심해야 할 부분은 Feature 간 직접 참조다.
예를 들어 features/A가 features/B를 직접 import하기 시작하면, 같은 레이어의 Slice끼리 결합이 생긴다. 이런 구조가 반복되면 순환 참조와 스파게티 코드가 생기기 쉽다.
이 문제는 크게 두 가지 방향으로 해결할 수 있다.
두 기능이 화면상에서 함께 배치될 뿐, 내부 비즈니스 로직이 강하게 얽혀 있지 않다면 widgets나 pages 레이어에서 조합하는 것이 좋다.
예를 들어 상품 정보와 장바구니 담기 기능을 함께 보여주는 영역이 필요하다면 다음과 같이 구성할 수 있다.
widgets/ProductSection/
├── ui/
│ └── ProductSection.tsx
└── index.ts
import { ProductCard } from '@/entities/Product';
import { AddToCartButton } from '@/features/AddToCart';
export function ProductSection() {
return (
<section>
<ProductCard />
<AddToCartButton />
</section>
);
}
이 방식의 장점은 features/AddToCart와 entities/Product가 서로를 직접 알 필요가 없다는 점이다. 조합의 책임은 더 상위 레이어인 widgets가 담당하고, 각 Feature와 Entity는 자기 책임만 유지할 수 있다.
이 방식은 두 기능이 화면에서는 함께 등장하지만, 내부 로직까지 강하게 결합되어 있지 않을 때 가장 적합하다.
두 기능의 비즈니스 로직이 실제로 강하게 연결되어 있다면, 단순히 UI 조합으로 해결하기 어렵다.
예를 들어 “자재 등록”과 “바코드 발행”이 합쳐져 하나의 “입고 처리” 흐름을 만든다고 해보자. 이때 기존 Feature끼리 직접 import해서 연결하면 결합도가 높아진다.
대신 공통으로 필요한 비즈니스 주체를 하위 레이어로 내려야 한다.
entities/
├── Material/
├── Barcode/
└── GoodsReceipt/
그리고 실제 사용자 행동은 Feature로 분리할 수 있다.
features/
├── RegisterMaterial/
├── IssueBarcode/
└── SubmitGoodsReceipt/
마지막으로 이 기능들을 하나의 화면 흐름으로 조합하는 책임은 widgets나 pages에서 담당할 수 있다.
widgets/GoodsReceiptForm/
이렇게 하면 Feature끼리 직접 의존하지 않고, 공통 도메인 모델은 entities에서 관리할 수 있다. 결과적으로 중복 모델을 줄이면서도 의존성 방향을 유지할 수 있다.
CDD와 FSD는 서로 경쟁하는 개념이 아니다.
CDD는 재사용 가능한 UI 컴포넌트를 만들기 위한 개발 방식이고, FSD는 그 컴포넌트와 비즈니스 로직을 애플리케이션 안에서 어떻게 배치하고 의존시킬지 정하는 아키텍처 방법론이다.
작은 프로젝트에서는 CDD만으로도 충분할 수 있다. 하지만 프로젝트가 커지고, 페이지와 기능, 도메인 로직이 늘어나기 시작하면 단순히 컴포넌트를 잘게 나누는 것만으로는 부족해진다.
이때 FSD는 코드의 위치와 의존성 방향에 대한 기준을 제공한다.
결국 중요한 것은 “무조건 FSD 구조를 따라야 한다”가 아니라, 프로젝트가 커졌을 때 변경의 영향 범위를 예측 가능하게 만들고, 기능 간 결합도를 낮추는 것이다.
그런 의미에서 CDD는 UI의 재사용성을 높이고, FSD는 애플리케이션 구조의 예측 가능성을 높이는 방식으로 함께 사용할 수 있다.