FSD 드세요
git branch 전략들
프로젝트를 할 때마다 폴더 구조가 달라져서 혼란스러웠던 경험 누구나 있으실 거예요. 명확한 기준이 없다 보니 프로젝트를 새로 맡은 사람은 물론, 만든 본인조차 잠깐 딴 일 했다 돌아오면 “이 파일 어디 있었지…?” 하게 됩니다. 저도 이런 이유떄문에 항상 폴더 구조를 고민하다가 더 마음에 안 들게 되는 경우도 있고, 더 복잡해지는 경우가 많았던 것 같아요. 좀 더 좋은 방법이 없을까 이것저것 공부하면서 FSD(Feature-Sliced Design) 라는 아키텍처를 알게 되었습니다.
FSD패턴을 공부하면서, 폴더 구조를 그때마다 다르게 기준을 두지 않고 도메인 중심으로 안정적이게 고정하는 방향으로 고민을 하게 되었습니다.
FSD(Feature-Sliced Design) Pattern
FSD는 레이어(Layer) → 슬라이스(Slice) → 세그먼트(Segment) 3단계로 코드를 나눠서, 폴더 뎁스를 3단계 이내로 제한하고, 기능(도메인) 기준으로 관리하는 패턴입니다. 이 구조의 가장 큰 특징은 폴더의 깊이를 3단계로 제한한다는 점이에요.
흔히 발생하는 ../../../../
무한 참조 문제나, 한 폴더에 파일이 몰려서 관리가 어려운 상황을 깔끔하게 정리할 수 있습니다.
핵심 철학
도메인 우선
유저/게시글/코멘트 처럼 현실의 개념(Entities)으로 코드를 묶기.
단방향 의존성
shared → entities → features → widgets → pages → app 순으로 참조. 역방향은 금지!
레이어(Layer)
도메인에 가까울수록 아래, 화면/라우팅에 가까울수록 위
- App: 전역 설정, Provider, Router, Client 같은 HOC가 위치
Processes: 여러 페이지가 연결된 플로우(예: 회원가입 1 → 2 → 3 단계), 위젯으로 대체되어 사용하지 않음- Pages: 브라우저 주소 단위로 분리된 컴포넌트, 라우팅에 따라 나뉘는 페이지
- Widgets: 여러 Feature를 묶은 단위로, 레이아웃 같은 틀을 담당
- Features: 사용자 행동 단위(로그인, 좋아요, API 호출 등)
- Entities: 데이터의 명사적 단위(유저, 게시물, 댓글). API를 통해 데이터를 조회
- Shared: 여러 곳에서 공유되는 유틸, 훅, 타입, 아이콘, 컴포넌트 등
각 세그먼트 쪼개기
각 레이어 아래 폴더를 세그먼트로 세분화합니다. 이름은 팀 취향을 따르되 보통 아래 4가지를 자주 사용합니다.
-
ui/: 프레젠테이션 컴포넌트(스타일, 뷰 로직)
-
model/: 상태, 셀렉터, 비즈니스 로직(RTK slice 등)
-
api/: API 클라이언트, 쿼리/뮤테이션 정의
-
lib/: 도메인 헬퍼, 유틸(순수 함수 선호)
Examples
리액트 폴더 구조
src/
app/
providers/
router/
index.tsx
pages/
posts/
ui/Page.tsx
index.ts
widgets/
post-list/
ui/PostList.tsx
model/usePostList.ts
index.ts
features/
like-post/
ui/LikeButton.tsx
model/slice.ts
api/likePost.ts
index.ts
entities/
post/
ui/PostCard.tsx
model/postSlice.ts
api/getPosts.ts
lib/format.ts
index.ts
user/
...
shared/
ui/Button.tsx
hooks/useDebounce.ts
utils/date.ts
icons/index.ts
config/env.ts
각 폴더의 index.ts는 Public API(외부로 내보낼 것만 re-export)
외부에서 import할 땐 폴더 루트만 보도 “깊은 경로” 참조를 원천 차단합니다.
// ❌
import PostCard from '@/entities/post/ui/PostCard';
import like from '@/features/like-post/model/slice';
// ✅
import { PostCard } from '@/entities/post';
import { LikeButton } from '@/features/like-post';
버튼
버튼의 껍데기(UI), 동작(행동), 내용물(데이터)를 분리합니다.
-
Shared: 공용 버튼 컴포넌트(모양·인터랙션)
-
Features: 그 버튼이 수행할 행동(좋아요)
-
Entities: 버튼이 보여줄 데이터(좋아요 수)
<Shared.Button
onClick={feature.api.like}
icon={shared.icon.fork}
data={entity.model.likeCount}
/>
이렇게 분리하면 역할과 책임이 명확히 분리되고, 서로 간의 불필요한 임포트를 막음으로써 재사용성은 높이고 의존성은 낮출 수 있습니다.(응높결낮 ㅎㅎ)
의존성 규칙 강제하기
ESLint로 깊은 경로 import 금지와 역방향 의존 금지를 걸어두면 협업할 때 실수도 줄고 편해지겠죠!?
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
// 내부 세그먼트로의 '깊은 경로' 차단
'**/*/ui/*',
'**/*/model/*',
'**/*/api/*',
'**/*/lib/*',
],
},
],
},
settings: {
'import/resolver': { typescript: {} },
},
};
경로 별칭도 필수 ✨
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}
마이그레이션
FSD의 또 다른 장점은 점증적 적용입니다. 하나씩 순서대로 적용하다보면 더 깊이 이해할 수 있습니다.
경로 별칭(@/*) 부터 잡아서 상대경로를 간소화 하기
→ shared entities features widgets pages app 순으로 폴더 나누기
→ 각 폴더 루트에 index.ts Public API를 만들고, 외부 import를 바꾸기
→ ESLint 깊은 경로 금지 규칙 추가, CI에서 체크
→ api/model/ui/lib 세그먼트를 천천히 분리하며 테스트 진행하기
처음부터 완벽 분리하겠다는 마음보단 “경로 단절 + Public API” 두 가지만 먼저 적용 후 나머지는 시간에 맡겨봅시다.
마무리
폴더 구조는 취향 차이가 크지만, FSD는 “자유”가 아니라 “방향”을 제시한다고 생각합니다.
FSD 공식 문서는 큰 가이드라인만 제시하고, 세부 구현은 팀마다 다릅니다. 결국 실무에서 가장 중요한 건 팀 합의와 컨벤션 정립이라고 생각합니다. 두 가지만 지키면 나머지는 팀에 맞게 유연하게 바꿔도 충분합니다.
- 도메인 중심으로 묶기
- 단방향 의존을 지키기
폴더 구조에 매번 고민하셨다면, 이제 FSD 적용해보세요.