FSD 아키텍처 — 코드 위치를 고민할 필요가 없어지는 설계

10 min read
FSD아키텍처프론트엔드TypeScriptReact
FSD 아키텍처 — 코드 위치를 고민할 필요가 없어지는 설계

"이 컴포넌트는 components에 넣어야 하나, features에 넣어야 하나?"

팀이 커지고 기능이 늘어날수록 이 질문이 반복된다. 정답이 없으니 각자 다른 판단을 내리고, 어느 순간 코드베이스가 제각각이 된다. 카카오페이 사장님 플러스 팀이 FSD를 도입한 이유가 바로 여기 있다.

기존 방식의 문제

초기에는 역할 중심으로 디렉토리를 구성했다.

기존 폴더 구조
src/
├─ components/   # UI 컴포넌트 전부
├─ apis/         # API 호출 전부
├─ pages/        # 페이지 전부
├─ hooks/        # 훅 전부
└─ utils/        # 유틸 전부

서비스가 단순할 때는 괜찮다. 혜택, 쿠폰, 멤버십 기능이 추가되면서 문제가 생겼다.

탐색 비용 증가components에 혜택 관련 컴포넌트, 쿠폰 관련 컴포넌트가 뒤섞인다. "혜택 목록 화면 컴포넌트가 어디 있지?"를 찾는 시간이 늘어난다.

사이드 이펙트 증가 — 모듈 간 경계가 없으니 A 컴포넌트를 수정했는데 B 화면이 깨진다. 영향 범위를 예측하기 어려워진다.

일관성 붕괴 — 새로운 팀원이 오면 "이거 어디 넣어야 해요?"라고 묻고, 각자 다른 답을 듣는다.

FSD란

Feature-Sliced Design — "기능 중심으로 코드를 수직·수평으로 나누는 아키텍처 방법론"이다.

핵심은 세 가지 축이다.

FSD 3축
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 클라이언트

단방향 의존성 규칙 — 위 레이어는 아래 레이어에만 의존할 수 있다. FeaturesEntitiesShared를 쓸 수 있지만, Pages를 쓸 수 없다.

허용/금지 의존 방향
App → Pages → Widgets → Features → Entities → Shared
 
Pages가 Features를 쓴다   ✅
Features가 Pages를 쓴다   ❌
Entities가 Features를 쓴다 ❌

이 규칙 하나로 순환 참조가 원천 차단된다.

Slice: 도메인 분리

레이어 안에서 코드를 비즈니스 도메인별로 나눈다. 같은 레이어 내 다른 슬라이스는 참조하지 않는다.

슬라이스 예시
features/
├─ benefit/    # 혜택 관련 기능
├─ coupon/     # 쿠폰 관련 기능
└─ membership/ # 멤버십 관련 기능

쿠폰 기능을 수정할 때 혜택 기능에 영향을 줄 수 없다. 슬라이스 경계가 격리막 역할을 한다.

Segment: 기술 목적 분리

슬라이스 안에서 코드를 기술적 목적으로 나눈다.

세그먼트역할
uiReact 컴포넌트, 스타일
apiAPI 요청, 응답 타입
model비즈니스 로직, 상태 관리
lib슬라이스 내부 유틸
config설정값, 상수, feature flag

카카오페이의 실제 적용

원칙을 그대로 따르기보다 프로젝트 특성에 맞게 조정했다. 세 가지 핵심 결정을 내렸다.

1) Widgets 레이어 제외 — 재사용 컴포넌트가 많지 않아서 6레이어 대신 5레이어로 운영한다. 복잡성을 줄이는 선택이다.

2) Slice Grouping 허용 — 같은 도메인의 여러 페이지를 그룹으로 묶는다.

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여러 도메인에서 재사용 (데이터 중심)

비유하면 특정 팀에서만 쓰는 도구는 그 팀 창고에, 여러 팀이 쓰는 도구는 공용 창고에 두는 것이다.

최종 폴더 구조

카카오페이 사장님 플러스 FSD 구조
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)을 사용하면 의존성 규칙을 자동으로 검증할 수 있다.

.eslintrc.json
{
  "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"라는 사실이 경로 자체에서 드러난다.

FSD 선택 체크리스트
✅ 팀이 3명 이상이고 규칙이 필요하다
✅ 기능이 늘어날수록 코드 찾기가 어려워지고 있다
✅ 한 곳 수정이 다른 곳에 영향을 주는 일이 잦다
 
⚠️  혼자 하는 작은 프로젝트라면 오버엔지니어링일 수 있다
⚠️  팀 전체가 규칙을 이해하지 못하면 효과가 반감된다

처음에는 레이어 결정이 낯설지만, 익숙해지면 "이건 features고 저건 shared다"가 직관적으로 보이기 시작한다.


참고:

관심 있을 만한 포스트

MDN이 React를 버린 이유 — 콘텐츠 사이트에서 Web Components가 맞는 선택인 이유

13년간 React 기반으로 운영하던 MDN이 Web Components와 자체 서버 컴포넌트 시스템으로 프론트엔드를 전면 재구축한 배경과 기술적 판단.

Web ComponentsMDN

VS Code 1.117 — BYOK와 점진적 채팅 렌더링의 등장

VS Code 1.117에서 추가된 Bring Your Own Key, 점진적 채팅 렌더링, VS Code Agents App, TypeScript 6.0.3 업데이트를 정리한다.

VS CodeCopilot

무료 npm 패키지를 유료 REST API로 — Cloudflare Workers 기반 아키텍처

JavaScript 전용 텍스트 분석 패키지를 모든 언어에서 쓸 수 있는 유료 API로 변환한 실제 아키텍처를 분석한다.

npmCloudflare Workers

pnpm 모노레포에서 React 19로 단계적 마이그레이션하기 — 타입 오염 문제와 해결

우아한형제들이 pnpm catalogs로 React 18/19를 동시에 운영하다 마주친 98개 타입 에러의 원인과 packageExtensions 해결법.

pnpmReact 19

React Gantt 차트 라이브러리 벤치마크 — 6개 직접 비교

SVAR, DHTMLX, Bryntum, Syncfusion, DevExtreme, KendoReact를 100,000개 태스크 기준으로 로딩 속도, 스크롤, CRUD 성능을 비교한다.

ReactGantt

Zustand 소프트 삭제 — enumerable:false로 컴포넌트 크래시 없이 처리하기

JavaScript property descriptor의 enumerable 플래그를 활용해 삭제된 엔티티를 투명하게 처리하는 Zustand 패턴을 소개한다.

Zustand상태 관리

SVG 아이콘 — 코드 배포 없이 프로덕트 팀이 직접 관리하는 법

CSS mask-image와 S3를 조합해 개발자 개입 없이 아이콘을 교체하는 패턴을 소개한다.

SVGCSS

Naver FE News 2026년 4월 — 49MB 웹 페이지부터 Temporal Stage 4까지

Naver FE News 2026년 4월호에서 프론트엔드 개발자가 주목할 6가지 소식을 선별해 정리한다.

Naver FE NewsTemporal