FSD 아키텍처란

애플리케이션을 기능과 책임 중심으로 분리하여 구성하는 아키텍처 패턴이에요.
기존의 역할 중심 구조와 달리 비즈니스 관점에서 의미 있는 단위를 기준으로 코드를 구성하는 것이 특징이에요.
가장 중요한 규칙은 단방향 의존성이에요.
자신보다 하위 레이어의 구성 요소만 참조하거나 import할 수 있어요.
이 덕분에 코드 결합도를 낮추고, 특정 레이어의 변경이 다른 레이어로 확산되는 것을 방지할 수 있어요.
FSD는 다음과 같은 레이어 구조를 기반으로 해요.
- shared : 프로젝트 전역에서 재사용 가능한 공통 유틸리티, 기본 컴포넌트, 스타일을 관리하는 레이어예요. 특정 기능이나 비즈니스 로직은 여기에 두지 않아요.
└── shared/
├── ui/ # 비즈니스 로직 없는 UI
├── utils/ # 유틸리티 함수
├── types/ # 공통 타입
├── hooks/ # 공통 훅
├── constants/ # 공통 상수
├── config/ # 앱 설정
├── apis/ # API 클라이언트
├── router/ # 라우터 설정
├── query/ # React Query 설정
└── layouts/ # 레이아웃 컴포넌트
- entities : 프로젝트에서 다루는 비즈니스의 핵심 데이터를 나타내는 레이어예요. 특정 도메인의 모델을 정의하고, 해당 데이터에 대한 API 호출 함수와 쿼리 옵션을 관리해요.
entities/
├── api/
│ └── get~Status.ts # 데이터 조회(GET) 함수
├── model/ # 해당 엔티티에서만 사용하는 타입, 상수, 유틸
│ ├── types.ts
│ ├── constants.ts
│ └── utils.ts
├── ui/ # 엔티티와 관련된 재사용 가능한 UI 컴포넌트
│ ├── VisaCard.tsx
│ └── VisaStatusBadge.tsx
└── queries/ # queryOptions와 queryKey 관리
├── visaQueries.ts
└── visaKeys.ts
- features : 사용자 행동 단위의 기능을 잘게 나눈 레이어예요. 버튼 클릭 같은 유저 인터랙션과 비즈니스 로직이 담겨 있어요. sendComment, addCart, followUser 등이 여기에 해당해요.
여러 페이지와 위젯에서 재사용 가능한 독립적인 비즈니스 로직을 이 레이어에 두면, 동일한 기능을 구현할 때 중복 코드를 줄이고 유지보수도 한 곳에서 할 수 있어요. (Colocation)
features/
├── comments/
│ ├── privateCommentA.ts
│ ├── privateCommentB.ts
│ ├── privateCommentC.ts
│ └── index.ts # 배럴 패턴 - 외부에서는 index.ts를 통해서만 참조
├── share/
├── upload/
└── follow/
- widgets : 하위 레이어들을 조합해 페이지 내에서 독립적으로 작동하는 큰 UI 블록을 만드는 레이어예요. 특정 도메인과 강결합되어 있어 여러 페이지에서 재사용하기 어려울 수 있지만, 맡고 있는 도메인이 명확하게 구분돼요.
예를 들어 UserProfile 위젯은 사용자 사진, 이름, 팔로워 수 등을 표시하는 컴포넌트로 구성되며, followUser 같은 features 레이어의 기능을 포함해요. 이 위젯은 UserPage와 HomePage 양쪽에서 재사용될 수 있어요.

- pages : 하위 레이어들을 조합해 완전한 페이지를 제공하는 레이어예요. 각 라우터에 해당하는 페이지를 작성하는 곳으로, UserPage는 사용자 관련 정보와 여러 widget, feature가 모여 하나의 페이지를 이뤄요.

- app : 프로젝트 진입점이자 전역 설정을 담당해요. 라우팅, 글로벌 스타일, 최상위 프로바이더를 여기서 관리해요.
import { RouterProvider } from 'react-router'; import { router } from '@shared/router'; function App() { return <RouterProvider router={router} />; } export default App;
각 레이어는 상위 레이어에서만 하위 레이어를 참조하도록 설계되며, 상위 레이어로 갈수록 비즈니스 로직이 심화돼요.
우리 프로젝트에서는 모노레포 환경에서 여러 패키지와 애플리케이션을 함꼐 관리하는 만큼, 각 프로젝트마다 일관된 아키텍쳐 규칙을 기반으로 한 공통 구조가 필요하다고 생각했어요.
FSD 구조의 장점이 해당 프로젝트의 여러 요구사항에 부합하였기 떄문에 확장성과 유지보수를 우선적으로 고려하여 FSD 아키텍처를 선택하게 되었어요.
프로젝트 폴더 구조
apps/web/src/
├── app/
├── pages/
│ ├── dashboard/
│ ├── roadmap/
│ ├── eligibility/
│ └── my-page/
├── widgets/
│ ├── dashboard/
│ ├── roadmap/
│ ├── eligibility/
│ └── my-page/
├── features/
├── entities/
└── shared/
├── ui/
├── utils/
├── types/
├── hooks/
├── constants/
├── config/
├── apis/
├── router/
├── query/
└── layouts/
모노레포 패키지 구조
packages/
├── kds-ui/ # 디자인 시스템 (원자적 UI 컴포넌트)
│ └── src/
│ ├── components/ # Button, Input, Card 등
│ └── styles/ # Vanilla Extract 스타일
└── configs/ # 공유 설정
FSD 구조 커스터마이징
FSD 표준 레이어 내부 슬라이스 구조(ui/, model/, lib/)를 팀에 더 익숙한 구조로 변경했어요.
변경 사항
- FSD 표준: ui/, model/, lib/
- 프로젝트 적용: components/, configs/, constants/, hooks/, types/, utils/
변경 이유
폴더명만으로 역할을 바로 파악할 수 있고, 익숙한 구조 덕분에 러닝커브도 줄일 수 있어요.
모노레포 패키지 UI와 FSD Shared UI의 역할 분리
FSD 아키텍처를 세팅하는 과정에서 모노레포의 @kds/ui 패키지와 FSD의 shared/ui 레이어가 역할이 겹치는 문제가 발생했어요. 두 곳 모두 UI 컴포넌트를 관리하는데, 어디에 무엇을 배치해야 할지 기준이 불명확했어서 ...
세 가지 방안을 검토했어요
1. @kds/ui 패키지에서 전부 관리
한 곳에서 관리해 로직이 명확하지만, shared/ui 레이어가 무의미해져 FSD 원칙에서 벗어나요.
2. Re-export 패턴
패키지에 모든 컴포넌트를 두고 shared/ui에서 다시 export하는 방식이에요. import 경로는 통일되지만, 굳이 두 단계를 거쳐야 하는 이유에 대한 혼란을 유발할 수 있어요.
3. 역할 분리
각 레이어의 책임을 명확하게 나누는 방식이에요. 비즈니스 컴포넌트를 분리하는 과정에서 혼란이 생길 수 있지만, FSD 원칙을 유지하면서 명확한 기준을 가질 수 있어요.
최종적으로 역할 분리 방안을 선택했어요.
@kds/ui의 독립성과 FSD의 구조적 원칙을 동시에 챙기면서, 비즈니스 로직을 명확하게 분리할 수 있는 방식이라고 판단했어요.
정리하자면,
@kds/ui (모노레포 패키지)
- 위치:
packages/kds-ui/src/components/ - 역할: 디자인 시스템의 원자적 컴포넌트
- 특징: 독립적인 도메인, 비즈니스 로직 없음
shared/ui (FSD 레이어)
- 위치:
apps/web/src/shared/ui/ - 역할: 원자 컴포넌트를 조합한 비즈니스 로직 없는 UI
- 예시:
FormField,SearchInput,PageHeader,Container,Grid
entities / features / widgets (FSD 레이어)
- 역할: 비즈니스 로직이 포함된 UI
FSD는 정해진 답이 아니라 방향을 제시하는 아키텍처예요.
중요한 건 원칙을 이해하고, 팀의 맥락에 맞게 적용하는 거라고 생각해요.
저희 팀도 완벽한 FSD를 구현했다기보다는, 원칙을 기반으로 우리에게 맞는 구조를 찾아가는 과정 중에 있어요.
앞으로도 계속 고민해보는 과정이 중요할 거 같아요!
'web' 카테고리의 다른 글
| TanStack Query 원리부터 제대로 쓰기 (0) | 2026.07.08 |
|---|---|
| 컴포넌트를 설계하는 원칙 (0) | 2026.07.08 |
| 근거 있는 스타일 라이브러리 선택: Tailwind vs vanilla-extract (0) | 2026.07.08 |
| 상황별 최적의 상태 관리 선택 가이드 (Context API, Zustand, Jotai) (0) | 2026.07.08 |
| Tailwind CSS Deep Dive (0) | 2026.07.08 |