기능 분할 설계 (Feature-Sliced Design, FSD)
기능 분할 설계(Feature-Sliced Design, FSD)는 프론트엔드 코드를 나누고 서로 어떤 방향으로 의존할 수 있는지 정하는 아키텍처 방법론입니다. 폴더 이름을 맞추는 것보다, 한 기능을 고칠 때 영향을 받는 범위를 줄이는 것이 목적이에요.
이 글은 2024년 소개를 바탕으로 현재 공식 문서의 설명을 반영했습니다. 오래된 구조도에 보이는 processes는 사용 중단된 레이어이며 새 프로젝트에 만들 필요가 없습니다.
레이어·슬라이스·세그먼트는 서로 다른 질문에 답한다
FSD 구조 개요. 그림에 포함된 processes는 현재 사용 중단된 레이어입니다.
features/add-to-cart/model을 예로 들어보면 구분이 명확해져요.
| 구분 | 답하는 질문 | 예시 |
|---|---|---|
| 레이어 | 어떤 책임을 지고 어디에 의존할 수 있는가? | features |
| 슬라이스 | 어떤 비즈니스 개념이나 사용자 행동에 속하는가? | add-to-cart |
| 세그먼트 | 그 안에서 어떤 목적을 수행하는 코드인가? | model |
슬라이스를 hooks, components처럼 기술 종류로 나누면 한 행동을 이해하려고 여러 곳을 오가게 됩니다. 세그먼트는 슬라이스 안에서 ui, model, api, lib, config 등 목적별로 나눕니다. 모든 세그먼트를 미리 만들 필요는 없어요. FSD 슬라이스와 세그먼트
레이어는 필요한 만큼만 사용한다
역할에 따라 나눈 레이어. 모든 레이어를 채워야 하는 템플릿은 아닙니다.
| 레이어 | 맡는 책임 | 쇼핑 앱의 예시 |
|---|---|---|
app |
앱 초기화와 전체 조합 | 라우터, provider, 전역 스타일 |
pages |
라우트에 해당하는 화면 | 상품 상세, 주문 내역 |
widgets |
독립적으로 의미 있는 큰 화면 블록 | 여러 페이지에서 쓰는 장바구니 요약 |
features |
사용자가 수행하는 주요 행동 | 장바구니 담기, 리뷰 작성 |
entities |
제품이 다루는 비즈니스 개념 | 상품, 주문, 사용자 |
shared |
비즈니스 영역에 종속되지 않는 기반 | HTTP 클라이언트, 버튼, 날짜 처리 |
현재 공식 문서도 보통 app, pages, shared부터 구성하고 가치가 있을 때 레이어를 더하도록 설명합니다. 페이지에서만 쓰는 큰 UI를 크다는 이유로 widgets로 옮길 필요는 없어요. FSD 레이어 정의
핵심 규칙은 import 경계다
다른 슬라이스를 참조할 때는 아래 레이어로 의존하는 것이 기본 규칙입니다.
일반적으로 한 슬라이스는 자신보다 아래 레이어의 슬라이스를 import할 수 있습니다. 같은 슬라이스 내부 파일끼리는 참조할 수 있지만, 같은 레이어의 다른 슬라이스를 직접 가져오지는 않습니다. app과 shared는 슬라이스 없이 세그먼트로 구성하는 예외입니다.
1
2
3
4
5
6
// features/add-to-cart/model/addToCart.ts
import { getProduct } from '@/entities/product'; // 허용: 아래 레이어
import { http } from '@/shared/api'; // 허용: 아래 레이어
// import { applyCoupon } from '@/features/apply-coupon';
// 기본 규칙 위반: 같은 레이어의 다른 슬라이스
이 규칙은 소스 코드의 의존 방향에 관한 것이지, 이벤트나 데이터가 런타임에서 한 방향으로만 흐른다는 뜻은 아닙니다. 콜백과 props를 통해 결과를 전달하는 것도 가능합니다.
그럼 장바구니 담기와 쿠폰 적용을 함께 처리해야 할 때는 어떻게 할까요? 둘을 조합하는 페이지나 위젯에 흐름을 두거나, 실제로 하나의 행동이라면 같은 슬라이스로 합칠 수 있습니다. 경고를 없애려고 비즈니스 로직을 무조건 shared로 내리면 경계가 더 흐려져요.
엔티티 사이의 불가피한 데이터 관계에는 공식 문서의 @x 교차 참조 API라는 명시적 예외도 있습니다. 모든 교차 import를 허용하는 통로가 아니라, 함께 바뀌는 관계를 드러내기 위한 장치입니다. 엔티티 관계와 import 규칙
Public API는 외부 사용자가 알아도 되는 범위다
1
2
3
4
5
// features/add-to-cart/index.ts
export { AddToCartButton } from './ui/AddToCartButton';
// pages/product-detail/ui/ProductDetailPage.tsx
import { AddToCartButton } from '@/features/add-to-cart';
외부에서는 내부의 model/useCartState.ts 같은 경로를 직접 알지 않게 합니다. 그러면 내부 파일을 나누거나 이름을 바꿀 때 호출부 전체를 고칠 필요가 줄어들어요.
다만 index.ts는 접근을 강제로 막아주는 장치가 아닙니다. 팀 규칙과 정적 검사가 함께 필요해요. 슬라이스 내부에서는 자기 index.ts를 거쳐 다시 자신을 불러오지 말고 상대 경로로 참조하면 순환 의존 위험을 줄일 수 있습니다. FSD Public API 문서
1
2
3
// features/add-to-cart/ui/AddToCartButton.tsx
import { useAddToCart } from '../model/useAddToCart';
// 자신의 공개 진입점 '@/features/add-to-cart'를 거치지 않음
shared 전체를 하나의 거대한 barrel로 묶을 필요도 없습니다. 관련 없는 무거운 의존성이 섞인다면 shared/ui/button, shared/lib/date처럼 목적별 공개 경계를 두는 편이 낫습니다.
실제로 시작한다면 이 정도로
작은 상품 앱을 가정한 구조 예시입니다. @/는 src/를 가리키도록 프로젝트에서 설정한 별칭이에요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
src/
app/
providers/
routes/
pages/
product-detail/
ui/ProductDetailPage.tsx
api/loadProductDetail.ts
index.ts
order-history/
ui/OrderHistoryPage.tsx
index.ts
features/
add-to-cart/
ui/AddToCartButton.tsx
model/useAddToCart.ts
index.ts
entities/
product/
model/product.ts
api/productApi.ts
index.ts
shared/
api/
ui/button/
이 예시에서 주문 내역이 한 페이지에만 필요하면 관련 로직을 그 페이지에 둡니다. 여러 곳에서 같은 주문 개념을 사용하기 시작했을 때 entities/order를 검토하면 돼요. 이름이 등장했다는 이유만으로 엔티티를 모두 선제적으로 추출할 필요는 없습니다.
도입 여부는 현재의 변경 비용으로 판단하기
FSD의 장점은 자동으로 생기지 않습니다. 슬라이스를 잘못 나누면 작은 변경에도 여러 폴더를 오가고, 같은 로직을 복제하거나 억지 추상화를 만들 수 있어요. 다음처럼 실제 문제와 대응을 연결하는 편이 유용합니다.
| 현재 문제 | 먼저 해볼 개입 | 확인할 결과 |
|---|---|---|
| 한 페이지 수정이 다른 페이지를 깨뜨림 | 페이지 내부 구현과 공개 API 분리 | 외부에서 내부 경로를 참조하는 곳이 줄었는가 |
| 기능 코드가 기술 폴더에 흩어짐 | 함께 바뀌는 코드를 한 슬라이스에 모음 | 변경을 이해하기 위해 열어야 하는 범위가 줄었는가 |
| 기능끼리 서로 순환 참조 | 상위 조합 지점으로 흐름 이동 | 순환 의존이 사라지고 책임이 명확해졌는가 |
| 빈 레이어와 이동만 늘어남 | 사용하지 않는 레이어 생략 | 구조 설명보다 기능 이해에 시간을 쓰는가 |
기존 앱이라면 한 화면을 골라 경계를 만들고 동작을 보존한 채 옮기는 것부터 시작할 수 있습니다. 새 구조와 기존 구조의 연결부를 명시하고, 다음 화면으로 넓히기 전에 변경 비용이 실제로 줄었는지 확인합니다. 공식 문서도 기존 커스텀 구조에서 점진적으로 이동하는 경로를 제공합니다. 커스텀 아키텍처에서 이전하기
제가 FSD에서 가져갈 핵심은 함께 바뀌는 코드를 가까이 두고, 외부 의존을 좁고 명시적으로 만드는 것입니다. 이 문제를 현재 구조로 충분히 풀고 있다면 레이어를 늘릴 이유는 없습니다. 반대로 경계가 무너져 변경 비용이 커지고 있다면, FSD는 팀이 공통 규칙을 정하는 데 도움이 될 수 있어요.
직접 구성한 예시는 FSD Todo 저장소에서 볼 수 있습니다.