적용 환경: React, Vue, Angular 등 모든 프론트엔드 프레임워크
들어가며
프론트엔드 프로젝트가 커지면서 겪는 전형적인 문제가 있습니다. "이 코드 어디 있지?"라며 components, hooks, utils 폴더를 뒤지거나, 파일 하나를 수정했는데 예상치 못한 곳이 깨지거나, 새로 합류한 팀원이 구조를 파악하는 데 오래 걸리는 상황입니다.
기술 기반으로 폴더를 나누면(components, services, utils) 처음에는 깔끔해 보이지만, 기능이 늘어날수록 각 폴더가 비대해지고 연관된 코드가 흩어집니다.
정보
Feature Sliced Design(FSD)은 "무엇을 하는 코드인가"(비즈니스 도메인)를
기준으로 코드를 조직하고, 명확한 의존성 규칙으로 스파게티 코드를 방지하는
아키텍처 방법론입니다.
핵심 구조: Layers, Slices, Segments
FSD는 코드를 세 가지 축으로 조직합니다.
| 축 | 설명 | 예시 |
|---|---|---|
| Layer | 코드의 영향 범위 | app, pages, features, entities, shared |
| Slice | 비즈니스 도메인 | user, post, comment, cart |
| Segment | 기술적 목적 | ui, model, api, lib |
Layer(레이어)는 코드의 영향 범위를 나타냅니다. 전역적인 설정부터 재사용 가능한 인프라까지, 위에서 아래로 영향력이 좁아집니다. 상위 레이어는 하위 레이어만 import할 수 있어 의존성 방향이 명확합니다.
Slice(슬라이스)는 비즈니스 도메인을 나타냅니다. 소셜 미디어라면 user, post, comment 같은 단위입니다. 같은 레이어의 슬라이스끼리는 서로 import할 수 없어 결합도가 낮게 유지됩니다.
Segment(세그먼트)는 기술적 목적에 따른 분류입니다. ui(컴포넌트), model(상태, 타입), api(서버 통신) 등으로 나뉩니다.
레이어 이해하기
FSD는 6개의 레이어를 정의합니다. 각 레이어는 자신보다 아래에 있는 레이어만 import할 수 있습니다.
프로젝트 전체에서 재사용되는 코드 중 비즈니스 로직이 없는 것들입니다. 디자인 시스템의 Button, Input 같은 기본 컴포넌트, API 클라이언트, 날짜 포맷 같은 유틸리티가 여기 속합니다.
// shared/ui/Button.tsx - 비즈니스 로직 없음
export const Button = ({ children, variant = 'primary', ...props }) => (
<button className={`btn btn-${variant}`} {...props}>{children}</button>
);
// shared/lib/format.ts
export const formatDate = (date: Date) =>
new Intl.DateTimeFormat('ko-KR').format(date);shared는 슬라이스가 없습니다. 비즈니스 도메인과 무관하기 때문입니다. 세그먼트(ui, api, lib, config)로만 구분합니다.
의존성 규칙
FSD의 핵심은 명확한 의존성 방향입니다. 두 가지 규칙만 기억하면 됩니다.
규칙 1: 상위 → 하위만 import
pages는 widgets, features, entities, shared를 가져올 수 있지만, app은 가져올 수 없습니다. features는 entities와 shared만 가져올 수 있습니다.
규칙 2: 같은 레이어 Slice 간 import 금지
entities/post에서 entities/user를 직접 가져오면 안 됩니다. 둘을 함께 써야 한다면 상위 레이어(widgets)에서 조합합니다.
올바른 예 vs 잘못된 예
// ✅ 올바른 예
// widgets/post-card/ui/PostCard.tsx
import { PostPreview } from "@/entities/post"; // widgets → entities ✅
import { UserAvatar } from "@/entities/user"; // widgets → entities ✅// ❌ 잘못된 예
// entities/post/ui/PostCard.tsx
import { UserAvatar } from "@/entities/user"; // entities → entities ❌이 규칙 덕분에 순환 의존성이 원천 차단되고, 코드 변경의 영향 범위를 예측할 수 있습니다.
Public API
각 슬라이스는 index.ts 파일을 통해 외부에 공개할 것만 내보냅니다. 이를 Public API라고 합니다. 외부에서는 슬라이스의 내부 구조를 모른 채 index.ts에서 제공하는 것만 사용합니다.
// features/auth/index.ts (Public API)
export { LoginForm } from './ui/LoginForm';
export { LogoutButton } from './ui/LogoutButton';
export { useAuth } from './model/useAuth';
export type { LoginCredentials } from './model/types';
// 내부 구현은 내보내지 않음
// - ./api/authApi.ts
// - ./model/authStore.ts
리팩토링의 자유
Public API만 유지하면 슬라이스 내부 구조를 자유롭게 리팩토링할 수 있습니다. 외부에 영향을 주지 않습니다.
실전 의사결정 가이드
코드를 작성할 때 어느 레이어에 둘지 고민된다면 다음 질문을 따라가 보세요.
| 질문 | 답이 Yes라면 |
|---|---|
| 비즈니스 로직이 없는 범용 코드인가요? | shared |
| 비즈니스 개념(명사)을 표현하나요? | entities |
| 사용자 동작(동사)을 처리하나요? | features |
| 여러 entities/features를 조합한 독립 UI인가요? | widgets |
| 특정 라우트에 대응하는 화면인가요? | pages |
작은 프로젝트를 위한 팁
작은 프로젝트에서는 모든 레이어가 필요하지 않습니다. app, pages, features(entities 병합), shared 정도로 시작해서 필요할 때 레이어를 추가하면 됩니다.
같은 레이어 참조 문제 해결
entities 간에 서로 데이터가 필요한 경우가 흔합니다. post를 보여줄 때 작성자 정보도 필요하죠. 하지만 entities/post에서 entities/user를 직접 import하면 규칙 위반입니다.
해결책: Slot 패턴 사용하기
엔티티 컴포넌트가 특정 영역을 props로 받게 합니다.
// entities/post/ui/PostCard.tsx
interface PostCardProps {
post: Post;
authorSlot: React.ReactNode; // 외부에서 주입
}
export const PostCard = ({ post, authorSlot }) => (
<article>
<header>{authorSlot}</header>
<PostPreview post={post} />
</article>
);// widgets/post-card/ui/FullPostCard.tsx
import { PostCard } from "@/entities/post";
import { UserAvatar } from "@/entities/user";
export const FullPostCard = ({ post, author }) => (
<PostCard post={post} authorSlot={<UserAvatar user={author} />} />
);기존 프로젝트에 적용하기
한 번에 전체 구조를 바꾸기보다 점진적으로 마이그레이션하는 것이 현실적입니다.
1단계: shared 정리
가장 안전한 출발점입니다. 공통 컴포넌트를 shared/ui로, API 클라이언트를 shared/api로, 유틸리티를 shared/lib로 옮깁니다.
shared/
├── ui/ # Button, Input, Modal...
├── api/ # API 클라이언트, interceptors
├── lib/ # formatDate, debounce...
└── config/ # 환경 변수, 상수
2단계: entities 추출
핵심 도메인 타입과 기본 UI를 entities로 분리합니다. User, Post 같은 핵심 개념부터 시작합니다.
// entities/user/index.ts
export { UserAvatar } from './ui/UserAvatar';
export { UserBadge } from './ui/UserBadge';
export type { User, UserRole } from './model/types';3단계: features 분리
로그인, 좋아요 같은 재사용되는 인터랙션을 features로 추출합니다.
// features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';4단계: pages와 widgets 정리
페이지 컴포넌트를 pages로 이동하고, 재사용 UI 블록을 widgets로 분리합니다.
정보
새 기능을 추가할 때부터 FSD 구조를 따르고, 기존 코드는 건드릴 때 함께 마이그레이션하는 방식이 부담이 적습니다.
도구 활용
FSD 규칙을 자동으로 검사하는 도구들이 있습니다.
ESLint 설정
@feature-sliced/eslint-config는 FSD 공식 린터입니다. 레이어 간 잘못된 import, Public API 위반 등을 검사합니다.
npm install -D @feature-sliced/eslint-config// .eslintrc.js
module.exports = {
extends: ["@feature-sliced"],
};경로 별칭 설정
경로 별칭을 설정하면 import 경로가 깔끔해집니다.
// tsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["src/*"] }
}
}정리
FSD는 프론트엔드 프로젝트를 비즈니스 도메인 중심으로 조직하는 방법론입니다.
FSD의 세 가지 핵심
- 6개 레이어로 코드의 영향 범위를 구분. 상위 → 하위만 import 가능 2. 같은 레이어의 슬라이스는 서로 격리. 조합이 필요하면 상위 레이어에서 처리
Public API(index.ts)를 통해서만 외부와 소통. 내부 구현은 캡슐화
이 규칙들 덕분에 "이 코드 어디 있지?"라는 질문에 명확히 답할 수 있고, 변경의 영향 범위를 예측할 수 있으며, 새 팀원도 구조를 빠르게 파악할 수 있습니다.