
프로젝트가 커질수록 가장 자주 발생하는 문제는 '이 파일은 어디에 둬야 하지?'라는 질문이다.
기능이 늘어나면 컴포넌트, API, 훅, 타입이 여기저기 흩어지고, 작은 수정도 영향 범위를 예측하기 어려워진다.
이 글은 FSD(Feature-Sliced Design)를 이번 프로젝트에 맞춰 적용하면서 정리한 판단 기준과 폴더 구조를 기록한 글이다.
*Processes 레이어가 deprecated 된 이후의 방식을 기준으로 설명한다.
0. 프로젝트 맥락 - 홈 화면이 가진 특성이 구조를 결정한다
이번 프로젝트의 홈 화면은 대략 이런 형태이다.

이 한 장의 화면만 봐도 '어디까지가 전역 골격이고, 어디부터가 기능이며, 무엇이 도메인 상태인지'가 갈린다.
- 하단 탭은 단순 UI가 아니라 라우팅과 결합된 앱 골격이다.
- 링크 복사는 사용자 액션(동사)이므로 feature로 분리하는 편이 자연스럽다.
- 값2/값3은 화면 컴포넌트가 아니라 도메인 상태(명사)와 계산 로직으로 관리되어야 한다.
이 글은 이 판단을 FSD의 레이어 규칙으로 안정적으로 고정하는 과정이다.
1. 구조가 없을 때 벌어지는 일
대부분의 프로젝트는 아래와 같은 구조로 시작한다.

초기에는 빠르고 직관적이다. 하지만 화면과 기능이 늘어나는 순간부터 문제가 시작된다.
- 어떤 UI가 특정 페이지 전용인지, 여러 페이지에서 재사용되는지 구분이 흐려진다.
- API 요청 코드가 apis/에 쌓이면서 어떤 기능에 필요한 요청인지 헷갈리며 도메인이 사라진다.
- 공용 훅과 기능 훅이 섞여서 의존성이 꼬이기 시작한다.
- 신규 기능을 추가할 때마다 '일단 여기 넣고 보자'가 반복된다.
- 수정할 때마다 '이거 다른 화면에도 쓰던데?'라는 불안이 생긴다.
이 상태가 되면 개발 생산성이 떨어지는 것보다 더 큰 문제가 생긴다.
팀이 구조를 믿지 못하게 되는 상황이 된다.
결국 '이 코드는 누가 제일 잘 아는가'가 중요해지고, 기존 작업자 의존이 심해진다.
이러한 문제를 해결하기 위해 FSD를 도입했다.
2. FSD란 무엇인가
FSD는 프론트엔드 애플리케이션 구조를 설계하기 위한 방법론이다. 핵심은 다음 두 가지이다.
1. 코드를 재사용 범위와 역할에 따라 명확히 나눈다.
2. 의존성 방향(단방향)을 강제해 구조적 혼란을 막는다.
FSD는 코드를 세 가지 축으로 나눈다.
- Layers (레이어) : 재사용 범위와 의존성 방향에 따라 수직으로 나누는 계층
- Slices (슬라이스) : 레이어 안에서 비즈니스 도메인별로 나누는 묶음
- Segments (세그먼트) : 슬라이스 안에서 기술 목적별로 나누는 폴더
이 조합이 제공하는 가장 큰 장점은 하나다.
코드를 어디에 둬야 하는지 판단하는 기준이 생긴다는 점이다.
3. Layers - '어디까지 재사용되는가'로 나누는 기준
FSD에서 가장 중요한 규칙은 단방향 의존성이다.
- 위 레이어는 아래 레이어를 import 할 수 있다
- 아래 레이어는 위 레이어를 import 할 수 없다
일반적인 레이어 흐름은 다음과 같다.
app → pages → widgets → features → entities → shared
각 레이어는 다음 역할을 가진다. (진행하는 프로젝트 기준으로 설명)
3-1. app - 앱 전역을 조립하는 레이어
app은 앱이 실행되기 위한 골격이 모이는 곳이다.
라우팅, 프로바이더, 전역 레이아웃, 전역 설정이 들어간다.
이번 프로젝트에서 app은 이런 모양이 된다.

슬라이스가 없고 세그먼트만 존재하는 형태가 자연스럽다.
3-2. pages - 라우팅되는 화면 단위
pages는 URL에 매핑되는 화면이다.
이 레이어의 핵심은 무엇을 보여줄지가 아닌 무엇을 조립할지이다.
이번 프로젝트라면 이런 페이지들이 된다.

페이지 내부에 로직이 과도하게 들어가면 다른 레이어의 역할이 무너진다.
그렇기 때문에 페이지는 큰 덩어리들을 가져와 배치하는 곳으로 남겨두는 편이 구조가 오래간다.
3-3. widgets - 여러 페이지에서 조립되는 큰 UI 블록
widgets는 페이지에서 조립되는 큰 블록 UI이다.
여러 페이지에서 재사용될 수 있는 덩어리가 들어간다.

중요한 점은 '위젯은 UI 덩어리'라는 점이다.
위젯은 내부에서 feature/entities를 가져다 쓰되, 자기보다 위 레이어(app/pages)를 알면 안 된다는 규칙만 지키면 된다.
3-4. features - 사용자 액션(동사) 단위
features는 버튼 클릭, 모달 열기, 폼 제출처럼 동사 기준으로 나뉜다.
이 레이어는 'UI도 포함할 수 있지만, 본질은 사용자 액션'이다.

다음과 같은 역할을 하게 된다.
- auth/social-login: 소셜 로그인 실행
- auth/refresh-session: refresh로 세션 갱신
- profile/create-profile: 프로필 생성 제출
- profile/nickname-check: 닉네임 중복 검사(디바운스/검증 상태 포함)
- share/copy-share-link: 링크 복사(토스트 포함)
홈 화면 요구사항으로 보면 '링크 복사'는 대표적인 feature이다.
반대로 '고정값/값2/값3의 계산'은 사용자 액션이 아니라 상태 모델에 가까우므로 entity 쪽으로 내려가야 한다.
3-5. entities - 비즈니스 도메인(명사)의 기준점
entities는 프로젝트가 다루는 명사 기준의 타입/상태/API가 모인다.
이 레이어가 잡히면 화면이 늘어나도 데이터의 기준점이 흔들리지 않는다.

- session/{model,api}: access/refresh, 로그인 상태
- user/model: 사용자 정보
예를 들어 랭킹 정책이 '1위는 고정, 2~10위만 갱신'이라면
이 조건은 ranking/model의 selector/계산 로직에서 정해지고 UI는 결과만 받아 그리는 편이 안전하다.
3-6. shared - 완전 공용(비즈니스와 최대한 분리)
shared는 특정 도메인에 속하지 않고 어디서든 쓰일 수 있는 코드가 모인다.

여기서 기준은 단순하다.
ex) 이 코드가 방문자(guest)를 알면 shared가 아니다.
4. Processes가 빠진 이유와 대체 방식
FSD의 레이어 설명에는 Processes가 deprecated로 표기되어 있다.
복잡한 페이지 간 시나리오를 담던 레이어였지만, 이제는 deprecated로 정리되어 있다.
이 변화는 현실적인 문제를 반영한 결과로 보였다.
- '페이지 간 시나리오'는 대부분 라우팅과 상태로 해결 가능하다.
- processes는 범위가 애매해 또 하나의 '여기 둬도 되나?' 폴더가 되기 쉽다.
- 복잡한 흐름을 한 폴더로 모으면 오히려 추적이 어려워진다.
대신 다음과 같은 방식이 자연스럽게 자리 잡았다.
- 라우팅 / 가드 / 셸 구조 → app에서 해결
- 사용자 행동 단위 → features로 분리
- 상태의 기준점 → entities에 두기
5. Segments - components/hooks가 아니라 ui/api/model로 나누는 이유
폴더를 components, hooks, types로 나누면 처음에는 직관적이다.
하지만 시간이 지나면 '이 코드가 왜 존재하는지'가 사라진다. 기술 형태만 남고 도메인이 지워진다.
FSD에서는 세그먼트를 목적 중심으로 나눈다.
- ui : 화면, 컴포넌트, 스타일
- api : 서버 통신
- model : 상태, 스키마, 비즈니스 로직
- lib : 슬라이스 내부 유틸
- config : 설정 파일, feature flag
폴더를 보는 순간 '이 파일이 무슨 목적으로 존재하는가'가 보이기 시작한다.
이게 규모가 커질수록 결정적으로 크게 작동한다.
6. 의문 - layouts는 왜 app에 있나
처음에는 하단 탭도 공용 컴포넌트처럼 보였다. 그래서 shared/ui에 두려고 했었다.
하지만 폴더 구조를 공부하다 보니 이번 프로젝트의 하단 탭은 단순 UI가 아니었다.
1. /home, /guests, /ranking 라우팅 구조와 강하게 결합된다.
2. 현재 활성 탭은 URL과 동기화되어야 한다.
3. 방문자 탭 배지(새로 온 방문자 체크)는 전역 상태와 결합된다.
4. 어떤 페이지에서는 탭이 사라져야 한다.
즉, 탭은 재사용 UI가 아니라 앱의 골격이다.
그래서 app/layouts에 두는 것이 구조적으로 자연스럽다.
그러면서 'layouts에 두면 늘 고정되어 있는 것은 아닌가?'라는 의문이 들었다.
이에 대한 답으로는 layout에 둔다고 해서 모든 페이지에 항상 탭이 붙는 것은 아니라는 것이다.
라우트 구조로 탭이 있는 구간 / 없는 구간을 나누면 된다.
이 차이를 라우트 구조로 표현하는 순간 layout의 의미가 명확해진다.
7. 예시 - HomePage.tsx 하나로 시작한 상태에서의 분해 기준
처음에는 HomePage.tsx 하나로 시작하는 것이 자연스럽다.
문제는 '언제 무엇을 빼낼지' 기준이 없을 때 생긴다. 기준만 있으면 분해는 어렵지 않다.
7-1. HomePage에 남길 것
- 위젯을 조립하는 역할
- 페이지 수준의 레이아웃 배치
- 라우팅 이동 트리거 정도
7-2. HomePage에서 빠질 것
- 홈 상단 요약(고정값/값2/값3 카드 묶음) → widgets/home-summary
- 공유 링크 패널(복사 버튼, 토스트) → widgets/share-link-panel
- 링크 생성/조회 보장 같은 액션 → features/share/ensure-share-link
- 고정값/값2/값3의 계산 로직, 상태 기준점 → entities/*/model
- API 호출 정의 → entities/*/api 또는 해당 feature/api
이렇게 분리하면 HomePage는 화면으로 남고, 나머지는 각자의 역할로 이동한다.
결과적으로 수정할 때 '어디를 고쳐야 하는지'가 더 빠르게 보이게 된다.
8. 결론
FSD를 도입하며 가장 크게 달라진 점은 폴더 구조가 아니다.
결정 비용이 사라진 것이 핵심이다.
새 코드를 만들 때마다 이 질문만 하면 된다.
- 이 코드는 앱 전역 조립인가 → app
- 이 코드는 라우팅되는 화면인가 → pages
- 이 코드는 큰 덩어리 UI인가 → widgets
- 이 코드는 사용자 액션인가 → features
- 이 코드는 도메인 데이터의 기준점인가 → entities
- 이 코드는 완전 공용인가 → shared
FSD 아키텍처를 공부하면서 협업하기 좋은 폴더 구조란
누가 코드를 추가하더라도 같은 기준으로 같은 위치를 선택할 수 있는 구조라는 결론에 도달했다.
'FRONTEND' 카테고리의 다른 글
| 프론트엔드 개발자는 무엇을 설계하고 만드는가 (2) | 2025.08.05 |
|---|---|
| [UI/UX 분석] 토스 ① : 홈 화면 살펴보기 (1) | 2025.07.22 |
| React, Vue, Angular 차이점 (0) | 2025.05.26 |
| 실시간 협업 캔버스에서의 동기화 처리 방식 (Yjs 기반) (0) | 2025.05.18 |
| Yjs Awareness로 실시간 사용자 상태 공유하기 (0) | 2025.05.11 |
