들어가며
프론트엔드 프로젝트가 커질수록 가장 어려워지는 것은 변경의 파급력입니다.
한 기능을 수정했는데 예기치 못한 다른 부분이 깨지거나, 팀 간 코드 충돌로 인해 개발 속도가 점점 느려지는 경험을 누구나 해봤을 겁니다.
실제로 많은 팀들이 이런 문제를 겪고 있습니다:
- 버그 수정 하나가 3개의 다른 페이지를 망가뜨림
- 새 기능 추가 시 기존 코드를 건드려야 해서 배포가 두려움
- 코드 리뷰에서 "이 변경이 다른 곳에 영향 없나요?" 같은 질문이 반복됨
이 문제를 해결하기 위해 최근 주목받는 아키텍처가 Feature-Sliced Design(FSD)입니다.
FSD의 핵심은 단순히 폴더 구조를 바꾸는 게 아니라, 의존성과 캡슐화를 관리해 모듈의 영향 범위를 제한한다는 데 있습니다.
전통적 폴더 구조의 구체적인 문제점
전통적인 구조는 보통 이렇게 시작합니다:
src/
components/
Header.tsx
UserCard.tsx
ProductList.tsx
hooks/
useAuth.ts
useProducts.ts
utils/
api.ts
validators.ts
pages/
HomePage.tsx
ProductPage.tsx
처음에는 단순하고 직관적입니다. 하지만 프로젝트가 커질수록 이런 문제가 발생합니다:
1. 의존성 방향이 뒤섞임
// ❌ 이런 의존성 체인이 만들어짐
// pages/HomePage.tsx → components/ProductList.tsx → hooks/useProducts.ts → utils/api.ts
// 하지만 동시에 utils/validators.ts → components/UserCard.tsx 같은 역방향도 존재
// ProductList.tsx
import { useAuth } from '../hooks/useAuth'; // hooks에 의존
import { validateProduct } from '../utils/validators'; // utils에 의존
// 하지만 validators.ts에서는
import { UserCard } from '../components/UserCard'; // 다시 components에 의존!
2. 캡슐화 부족
특정 컴포넌트를 구현하기 위해 사용된 내부 컴포넌트(private module)을 다른 외부 컴포넌트에서 참조하게 되면서 변경의 영향 범위를 예측하기 어려워집니다.
// ❌ 다른 컴포넌트에서 내부 구현에 직접 접근
// pages/ProductPage.tsx
import { ProductListItem } from '../components/ProductList/ProductListItem'; // 내부 컴포넌트 직접 참조
import { productListReducer } from '../components/ProductList/reducer'; // 내부 상태 로직 직접 참조
3. 변경 파급 범위 예측 불가
UserCard 컴포넌트의 props를 하나 바꾸면:
components/UserList.tsxpages/ProfilePage.tsxutils/userHelpers.tshooks/useUserData.ts
등 예상치 못한 곳들이 모두 영향을 받습니다.
Feature-Sliced Design의 의존성 구조

FSD는 프로젝트를 7개의 계층으로 설계합니다:
src/
app/ # 앱 초기화, 라우터, 글로벌 설정
pages/ # 페이지 수준 로직
widgets/ # 독립적인 UI 블록
features/ # 사용자 액션과 관련된 기능들
entities/ # 비즈니스 엔티티
shared/ # 공용 유틸리티
단방향 의존성
app → pages → widgets → features → entities → shared
각 계층은 자신보다 아래 계층만 import 할 수 있어 레이어에 따라 변경의 파급 효과를 예상할 수 있습니다.
src/
app/
providers/
router/
index.tsx
pages/
product-list/
ui/ProductListPage.tsx
index.ts
widgets/
header/
ui/Header.tsx
index.ts
features/
product-search/
ui/SearchForm.tsx
model/store.ts
api/searchApi.ts
index.ts
add-to-cart/
ui/AddToCartButton.tsx
model/store.ts
index.ts
entities/
product/
ui/ProductCard.tsx
model/types.ts
api/productApi.ts
index.ts
user/
model/types.ts
api/userApi.ts
index.ts
shared/
ui/Button.tsx
lib/http.ts
config/api.ts
Features: 사용자의 구체적인 액션
product-search: 상품 검색하기add-to-cart: 장바구니에 담기user-authentication: 로그인/로그아웃
Entities: 비즈니스 도메인 객체
product: 상품에 관한 모든 것user: 사용자에 관한 모든 것order: 주문에 관한 모든 것
Shared: 도메인에 무관한 재사용 가능한 코드
- UI 컴포넌트 (Button, Input)
- 유틸리티 함수
- API 클라이언트
캡슐화: Public API 패턴
각 슬라이스는 index.ts를 통해서만 외부에 노출됩니다. public 모듈과 private모듈을 구분함으로써 slice 레벨에서의 캡슐화를 구현했고 이에 따라 다른 레이어와 통신하는 인터페이스를 유지한체 내부 모듈을 유지 관리할 수 있게 됐습니다.
📁 src/
📁 entities/
📁 user/
📁 model/ # 비즈니스 로직
📁 ui/ # UI 컴포넌트
📁 api/ # API 관련 코드
📄 index.ts # 공개 인터페이스만 export
// features/add-to-cart/index.ts
export { AddToCartButton } from './ui/AddToCartButton';
export { addToCartModel } from './model/store';
// 내부 구현인 ./api/cartApi.ts는 노출하지 않음
// pages/product-list/ui/ProductListPage.tsx
import { AddToCartButton } from '@/features/add-to-cart'; // ✅ public API 사용
import { cartApi } from '@/features/add-to-cart/api/cartApi'; // ❌ 내부 구현 직접 접근 불가
대규모 프로젝트에서 얻는 이점
1.예상 가능한 영향 범위
코드 리뷰 과정에서 components 폴더에 있는 UI요소에 변경 사항이 생겼을 때, 이 모듈의 영향 범위를 직관적으로 이해할 수 있을까요? FSD를 통해 가장 체감되었던 변화는 모듈의 영향범위를 직관적으로 이해할 수 있게 됨에 따라 유지 보수 비용이 낮아졌다는 점이었습니다.
Before (전통적 구조)
// components/UserCard.tsx 수정 시 영향받는 파일들
- pages/ProfilePage.tsx
- pages/HomePage.tsx
- components/UserList.tsx
- utils/userHelpers.ts
- hooks/useUserData.ts
// 총 5개+ 파일이 영향받을 수 있음
After (FSD)
// entities/user/ui/UserCard.tsx 수정 시
// features/user-profile/ 폴더 내부만 영향
// 다른 feature들은 public API를 통해서만 접근하므로 안전
2. 안전한 병렬 개발
실제 시나리오: 4명의 개발자가 동시에 작업할 때
개발자 A: features/product-search/
개발자 B: features/shopping-cart/
개발자 C: features/user-reviews/
개발자 D: entities/product/ (공통)
각 기능이 독립적으로 개발되고, entities 계층의 public API만 공유하므로 충돌이 최소화됩니다.
3.점진적 마이그레이션
실제 저희 팀에서 FSD를 도입할 때 역할이 뚜렷한 Layer부터 도입하면서 얽혀있는 의존성을 점진적으로 풀어나갔습니다.
가장 명확하면서 모든 레이어에 영향을 줄 수 있는 shared 레이어의 도입이 1단계라 할 수 있습니다. 이제 의존성의 계층 구조가 명확해짐에 따라 코드 리뷰시에도 layer별로 변경에 대한 민감도를 조절할 수 있었습니다.
가장 밑바닥인 shared레이어를 시작으로 단계별로 레이어를 도입하시면 됩니다. entities, widget 레이어에 대해서 분류가 어려울 수 있습니다. 이럴 떄 pages, features, shared부터 단계적으로 도입하시길 권장 드립니다. 캡슐화와 단방향 의존성이라는 핵심 가치를 중심으로 의존성을 분류하다보면 자연스럽게 중간 레이어에 대한 필요성을 느끼게 되실 것 입니다.
단계별 접근법:
1단계: shared 계층 분리
src/
shared/
ui/
lib/
config/
legacy/ (기존 코드)
2단계: entities 분리
src/
entities/
user/
product/
shared/
legacy/
3단계: features 분리
src/
features/
add-to-cart/
product-search/
entities/
shared/
legacy/
마무리
대규모 프로젝트에서 중요한 것은 변경이 일어났을 때 어디까지 영향을 주는지 예상할 수 있는가입니다.
Feature-Sliced Design은 단방향 의존성과 캡슐화라는 원칙을 통해 이 문제를 해결합니다.
- 변경 범위가 예측 가능하게 제한됨
- 기능 단위로 안전하게 분리되어 병렬 개발 가능
- 점진적 도입으로 기존 프로젝트에도 적용 가능
- 측정 가능한 개발 속도 및 코드 품질 개선
FSD는 단순한 폴더 구조 제안이 아니라, 대규모 프론트엔드 협업을 가능하게 하는 아키텍처적 안전장치입니다.
다만 모든 프로젝트에 필요한 것은 아닙니다. FSD에서 해결하는 문제들은 대규모 프로젝트에서 발생하는 문제들입니다. 팀 규모, 프로젝트 복잡도, 장기 유지보수 계획을 고려해서 도입을 결정하시기 바랍니다.
참고 자료
'react' 카테고리의 다른 글
| FSD Architecture TypeScript 예시 (0) | 2025.08.29 |
|---|---|
| [설계] 객체지향의 유연한 구조 설계, TypeScript와 React로 이해하기 (0) | 2025.08.24 |
| [설계] 리액트에서 API 통신 추상화하기 (0) | 2025.08.23 |
| Next.js에서 SearchParams 상태 관리: Context vs nuqs (3) | 2025.08.21 |
| 추상화와 추상화 수준: 깔끔한 코드의 핵심 (0) | 2025.08.17 |