FSD 아키텍처 — 코드 위치를 고민할 필요가 없어지는 설계
"이 컴포넌트는 components에 넣어야 하나, features에 넣어야 하나?"
팀이 커지고 기능이 늘어날수록 이 질문이 반복된다. 정답이 없으니 각자 다른 판단을 내리고, 어느 순간 코드베이스가 제각각이 된다. 카카오페이 사장님 플러스 팀이 FSD를 도입한 이유가 바로 여기 있다.
기존 방식의 문제
초기에는 역할 중심으로 디렉토리를 구성했다.
src/
├─ components/ # UI 컴포넌트 전부
├─ apis/ # API 호출 전부
├─ pages/ # 페이지 전부
├─ hooks/ # 훅 전부
└─ utils/ # 유틸 전부서비스가 단순할 때는 괜찮다. 혜택, 쿠폰, 멤버십 기능이 추가되면서 문제가 생겼다.
탐색 비용 증가 — components에 혜택 관련 컴포넌트, 쿠폰 관련 컴포넌트가 뒤섞인다. "혜택 목록 화면 컴포넌트가 어디 있지?"를 찾는 시간이 늘어난다.
사이드 이펙트 증가 — 모듈 간 경계가 없으니 A 컴포넌트를 수정했는데 B 화면이 깨진다. 영향 범위를 예측하기 어려워진다.
일관성 붕괴 — 새로운 팀원이 오면 "이거 어디 넣어야 해요?"라고 묻고, 각자 다른 답을 듣는다.
FSD란
Feature-Sliced Design — "기능 중심으로 코드를 수직·수평으로 나누는 아키텍처 방법론"이다.
핵심은 세 가지 축이다.
Layer : 재사용 범위에 따른 수직 분리 (App → Shared)
Slice : 비즈니스 도메인에 따른 수평 분리 (benefit, coupon)
Segment: 기술적 목적에 따른 내부 분리 (ui, api, model)[💡 잠깐! 이 용어는?] FSD(Feature-Sliced Design): 프론트엔드 아키텍처 방법론. 코드를 재사용 범위(레이어), 비즈니스 도메인(슬라이스), 기술적 목적(세그먼트)으로 3차원 분류한다. 공식 사이트: feature-sliced.design
Layer: 6개 계층
레이어는 가장 중요한 개념이다. **"이 코드가 얼마나 넓은 범위에서 재사용되는가"**에 따라 계층이 결정된다.
| 레이어 | 재사용 범위 | 예시 |
|---|---|---|
| App | 앱 전체 | 라우터, 전역 상태, 글로벌 스타일 |
| Pages | 특정 페이지 | BenefitListPage, CouponDetailPage |
| Widgets | 여러 페이지 | GNB, Footer, 공통 대시보드 |
| Features | 특정 기능 | 혜택 조회, 쿠폰 발급, 멤버십 등록 |
| Entities | 비즈니스 데이터 | 혜택 정보, 쿠폰 정보, 파트너 정보 |
| Shared | 프로젝트 전체 | Button, Input, formatDate, api 클라이언트 |
단방향 의존성 규칙 — 위 레이어는 아래 레이어에만 의존할 수 있다. Features는 Entities와 Shared를 쓸 수 있지만, Pages를 쓸 수 없다.
App → Pages → Widgets → Features → Entities → Shared
Pages가 Features를 쓴다 ✅
Features가 Pages를 쓴다 ❌
Entities가 Features를 쓴다 ❌이 규칙 하나로 순환 참조가 원천 차단된다.
Slice: 도메인 분리
레이어 안에서 코드를 비즈니스 도메인별로 나눈다. 같은 레이어 내 다른 슬라이스는 참조하지 않는다.
features/
├─ benefit/ # 혜택 관련 기능
├─ coupon/ # 쿠폰 관련 기능
└─ membership/ # 멤버십 관련 기능쿠폰 기능을 수정할 때 혜택 기능에 영향을 줄 수 없다. 슬라이스 경계가 격리막 역할을 한다.
Segment: 기술 목적 분리
슬라이스 안에서 코드를 기술적 목적으로 나눈다.
| 세그먼트 | 역할 |
|---|---|
ui | React 컴포넌트, 스타일 |
api | API 요청, 응답 타입 |
model | 비즈니스 로직, 상태 관리 |
lib | 슬라이스 내부 유틸 |
config | 설정값, 상수, feature flag |
카카오페이의 실제 적용
원칙을 그대로 따르기보다 프로젝트 특성에 맞게 조정했다. 세 가지 핵심 결정을 내렸다.
1) Widgets 레이어 제외 — 재사용 컴포넌트가 많지 않아서 6레이어 대신 5레이어로 운영한다. 복잡성을 줄이는 선택이다.
2) Slice Grouping 허용 — 같은 도메인의 여러 페이지를 그룹으로 묶는다.
pages/
├─ benefit/ # Slice Group (도메인)
│ ├─ benefitList/ # Slice (개별 페이지)
│ ├─ benefitDetail/
│ └─ benefitCreate/
└─ coupon/
├─ couponList/
└─ couponDetail/3) API 배치 기준 명확화 — 어느 레이어에 API를 두어야 하는지가 가장 많이 논의됐다.
| API 위치 | 기준 |
|---|---|
pages/[page]/api | 특정 페이지에서만 사용 |
features/[slice]/api | 같은 도메인 여러 페이지에서 재사용 (기능 중심) |
entities/[domain]/api | 여러 도메인에서 재사용 (데이터 중심) |
비유하면 특정 팀에서만 쓰는 도구는 그 팀 창고에, 여러 팀이 쓰는 도구는 공용 창고에 두는 것이다.
최종 폴더 구조
src/
├─ app/ # 전역 설정, 라우터
├─ pages/
│ ├─ benefit/ # 혜택 Slice Group
│ │ ├─ benefitList/
│ │ │ ├─ api/ # 이 페이지에서만 쓰는 API
│ │ │ ├─ ui/ # 페이지 전용 컴포넌트
│ │ │ └─ model/ # 페이지 전용 상태
│ │ └─ benefitDetail/
│ └─ coupon/
├─ features/
│ └─ benefit/ # 혜택 도메인 공통 기능
│ ├─ api/ # 여러 페이지에서 쓰는 API
│ └─ model/
├─ entities/
│ └─ partner/ # 파트너 데이터 (여러 도메인 공유)
│ ├─ api/
│ └─ model/
└─ shared/
├─ ui/ # 공통 컴포넌트
└─ api/ # API 클라이언트, 기본 설정도입 전략: 상향식 마이그레이션
기존 코드를 한 번에 전환하지 않았다. Pages → Features → Entities → Shared 순서로 점진적으로 이동했다.
이 순서가 효과적인 이유가 있다. 개발자가 "이 코드가 이 페이지에서만 쓰이나, 아니면 더 넓게 쓰이나?"를 자연스럽게 고민하면서 레이어를 결정하게 된다. 의사결정이 체계화된다.
ESLint 플러그인(eslint-plugin-fsd)을 사용하면 의존성 규칙을 자동으로 검증할 수 있다.
{
"extends": ["plugin:@feature-sliced/recommended"],
"rules": {
"@feature-sliced/layers-slices": "error",
"@feature-sliced/absolute-relative": "warn"
}
}레이어 간 잘못된 의존성이 생기면 CI에서 바로 잡힌다.
결과
카카오페이 팀이 FSD 도입 후 얻은 것들이다.
- 코드 탐색 시간 감소 — "혜택 기능 코드가 어디 있어요?"에 "pages/benefit 또는 features/benefit이요"로 바로 답할 수 있다.
- 영향 범위 예측 가능 — 슬라이스 경계가 격리막이라 한 기능 수정이 다른 기능을 깨트리기 어렵다.
- 신규 팀원 온보딩 단축 — 레이어 규칙을 이해하면 어디에 코드를 넣을지 스스로 결정할 수 있다.
마무리
FSD의 핵심은 단순하다. 코드 위치가 재사용 범위를 나타내게 만들기다. pages/benefit/benefitList/api에 있는 코드는 "혜택 목록 페이지에서만 쓰는 API"라는 사실이 경로 자체에서 드러난다.
✅ 팀이 3명 이상이고 규칙이 필요하다
✅ 기능이 늘어날수록 코드 찾기가 어려워지고 있다
✅ 한 곳 수정이 다른 곳에 영향을 주는 일이 잦다
⚠️ 혼자 하는 작은 프로젝트라면 오버엔지니어링일 수 있다
⚠️ 팀 전체가 규칙을 이해하지 못하면 효과가 반감된다처음에는 레이어 결정이 낯설지만, 익숙해지면 "이건 features고 저건 shared다"가 직관적으로 보이기 시작한다.
참고:
- 카카오페이 기술 블로그: https://tech.kakaopay.com/post/fsd/
- FSD 공식 문서: https://feature-sliced.design/
- eslint-plugin-fsd: https://github.com/feature-sliced/eslint-config
관심 있을 만한 포스트
MDN이 React를 버린 이유 — 콘텐츠 사이트에서 Web Components가 맞는 선택인 이유
13년간 React 기반으로 운영하던 MDN이 Web Components와 자체 서버 컴포넌트 시스템으로 프론트엔드를 전면 재구축한 배경과 기술적 판단.
VS Code 1.117 — BYOK와 점진적 채팅 렌더링의 등장
VS Code 1.117에서 추가된 Bring Your Own Key, 점진적 채팅 렌더링, VS Code Agents App, TypeScript 6.0.3 업데이트를 정리한다.
무료 npm 패키지를 유료 REST API로 — Cloudflare Workers 기반 아키텍처
JavaScript 전용 텍스트 분석 패키지를 모든 언어에서 쓸 수 있는 유료 API로 변환한 실제 아키텍처를 분석한다.
pnpm 모노레포에서 React 19로 단계적 마이그레이션하기 — 타입 오염 문제와 해결
우아한형제들이 pnpm catalogs로 React 18/19를 동시에 운영하다 마주친 98개 타입 에러의 원인과 packageExtensions 해결법.
React Gantt 차트 라이브러리 벤치마크 — 6개 직접 비교
SVAR, DHTMLX, Bryntum, Syncfusion, DevExtreme, KendoReact를 100,000개 태스크 기준으로 로딩 속도, 스크롤, CRUD 성능을 비교한다.
Zustand 소프트 삭제 — enumerable:false로 컴포넌트 크래시 없이 처리하기
JavaScript property descriptor의 enumerable 플래그를 활용해 삭제된 엔티티를 투명하게 처리하는 Zustand 패턴을 소개한다.
SVG 아이콘 — 코드 배포 없이 프로덕트 팀이 직접 관리하는 법
CSS mask-image와 S3를 조합해 개발자 개입 없이 아이콘을 교체하는 패턴을 소개한다.
Naver FE News 2026년 4월 — 49MB 웹 페이지부터 Temporal Stage 4까지
Naver FE News 2026년 4월호에서 프론트엔드 개발자가 주목할 6가지 소식을 선별해 정리한다.