기존 아키텍처에서 FSD로의 마이그레이션
이 가이드는 기존 아키텍처를 Feature-Sliced Design(FSD) 으로 단계별 전환하는 방법을 설명합니다.
아래 폴더 구조를 예시로 살펴보세요. (파란 화살표를 클릭하면 펼쳐집니다).
디렉터리src/
디렉터리actions/
디렉터리product/
- …
디렉터리order/
- …
디렉터리api/
- …
디렉터리components/
- …
디렉터리containers/
- …
디렉터리constants/
- …
디렉터리i18n/
- …
디렉터리modules/
- …
디렉터리helpers/
- …
디렉터리routes/
- products.jsx
- products.[id].jsx
디렉터리utils/
- …
디렉터리reducers/
- …
디렉터리selectors/
- …
디렉터리styles/
- …
- App.jsx
- index.js
시작 전 체크리스트
섹션 제목: “시작 전 체크리스트”Feature-Sliced Design(FSD)이 정말 필요한지 먼저 확인하세요.
모든 프로젝트가 새로운 아키텍처를 요구하는 것은 아닙니다.
전환을 고려해야 할 징후
섹션 제목: “전환을 고려해야 할 징후”- 신규 팀원이 프로젝트에 적응하기 어려워하는 경우
- 코드 일부를 수정할 때, 관련 없는 다른 코드에 오류가 발생하는 경우가 잦은 경우
- 새 기능을 추가할 때 고려해야 할 사항이 너무 많아 어려움을 겪는 경우
팀의 합의 없이 FSD 전환을 시작하지 마세요.
팀 리더라도 전환의 이점이 학습/전환 비용을 상회한다는 점을 먼저 설득해야 합니다.
또한, 개선 효과가 바로 눈에 띄지 않을 수 있으므로 팀원 및 프로젝트 매니저(PM) 의 승인을 사전에 확보하고 이점을 공유하세요.
마이그레이션을 시작하기로 결정했다면, 📁 src 폴더에 별칭(alias)을 설정하는 것을 첫 단계로 삼으세요.
1단계: 페이지 단위로 코드 분리하기
섹션 제목: “1단계: 페이지 단위로 코드 분리하기”대부분의 커스텀 아키텍처는 규모와 관계없이 이미 어느 정도 페이지 단위로 코드를 나누고 있습니다.
📁 pages 폴더가 있다면 이 단계를 건너뛰어도 됩니다.
위에 예시 폴더처럼 📁 routes만 있다면 다음 순서를 따르세요.
📁 pages폴더를 새로 만듭니다.📁 routes에 있던 페이지용 컴포넌트를 가능한 한 모두📁 pages폴더로 옮깁니다.- 코드를 옮길 때마다 해당 페이지 전용 폴더를 만들고 그 안에
index.tsx파일을 추가해 진입점(entry point) 를 노출합니다.
📁 Route File
export { ProductPage as default } from "src/pages/product";📁 Page Index File
export { ProductPage } from "./ProductPage.jsx";📁 Page Component File
export function ProductPage(props) { return <div />;}2단계: 페이지 외부 코드를 분리하기
섹션 제목: “2단계: 페이지 외부 코드를 분리하기”📁 src/shared 폴더를 만들고, 📁 pages 또는 📁 routes를 import하지 않는 모든(파일)은 이 폴더로 모읍니다.
📁 src/app 폴더를 만들고, 📁 pages 또는 📁 routes를 import하는 모듈과 라우트 정의 파일은 이 폴더에 배치합니다.
Shared layer는 slice 개념이 존재하지 않기 때문에, 서로 다른 segment 간에도 자유롭게 import할 수 있습니다
이제 폴더 구조는 다음과 같아야 합니다:
디렉터리src/
디렉터리app/
디렉터리routes/
- products.jsx
- products.[id].jsx
- App.jsx
- index.js
디렉터리pages/
디렉터리product/
디렉터리ui/
- ProductPage.jsx
- index.js
디렉터리catalog/
- …
디렉터리shared/
디렉터리actions/
- …
디렉터리api/
- …
디렉터리components/
- …
디렉터리containers/
- …
디렉터리constants/
- …
디렉터리i18n/
- …
디렉터리modules/
- …
디렉터리helpers/
- …
디렉터리utils/
- …
디렉터리reducers/
- …
디렉터리selectors/
- …
디렉터리styles/
- …
3단계: 페이지 간 cross-imports 해결
섹션 제목: “3단계: 페이지 간 cross-imports 해결”한 페이지가 다른 페이지의 코드를 직접 import하고 있다면, 아래 두 가지 방식 중 하나로 의존성을 정리합니다.
| 방법 | 사용 시점 |
|---|---|
| A. 코드 복사하여 독립시키기 | 페이지별로 로직이 달라질 가능성이 높거나, 재사용성이 낮은 경우 |
| B. Shared로 이동하여 공통화하기 | 여러 페이지에서 반복적으로 사용되는 경우 |
- Shared 이동 위치 예시
- UI 구성 요소 →
📁 shared/ui - 설정 상수 →
📁 shared/config - 백엔드 호출 →
📁 shared/api
- UI 구성 요소 →
4단계: Shared Layer 정리하기
섹션 제목: “4단계: Shared Layer 정리하기”한 페이지에서만 사용되는 코드는 해당 페이지의 slice로 이동합니다.
actions, reducers, selectors 역시 예외가 아니며, 사용되는 위치와 가까운 곳에 두는 것이 가장 좋습니다.
Shared는 모든 layer가 의존할 수 있는 공통 의존 지점이기 때문에,
이곳에 코드를 과도하게 쌓아두지 않고 최소한으로 유지하는 것이 변경 위험을 줄이는 핵심 원칙입니다.
이 단계를 마치면 폴더 구조는 아래와 같은 형태가 되는 것이 자연스럽습니다:
디렉터리src/
디렉터리app/ (unchanged)
- …
디렉터리pages/
디렉터리product/
디렉터리actions/
- …
디렉터리reducers/
- …
디렉터리selectors/
- …
디렉터리ui/
- Component.jsx
- Container.jsx
- ProductPage.jsx
- index.js
디렉터리catalog/
- …
디렉터리shared/ (only objects that are reused)
디렉터리actions/
- …
디렉터리api/
- …
디렉터리components/
- …
디렉터리containers/
- …
디렉터리constants/
- …
디렉터리i18n/
- …
디렉터리modules/
- …
디렉터리helpers/
- …
디렉터리utils/
- …
디렉터리reducers/
- …
디렉터리selectors/
- …
디렉터리styles/
- …
5단계: 기술적 목적별 segment 정리
섹션 제목: “5단계: 기술적 목적별 segment 정리”| segment | 용도 예시 |
|---|---|
ui | Components, formatters, styles |
api | Backend requests, DTOs, mappers |
model | Store, schema, business logic |
lib | Shared utilities / helpers |
config | Configuration files, feature flags |
무엇인지가 아니라 무엇을 위해 존재하는지를 기준으로 폴더를 구분합니다.
따라서components,utils,types처럼 목적이 모호한 폴더 이름은 지양합니다.
- 각 페이지 내부에서, 필요한
segment(ui, model, api 등)를 구성합니다. - Shared 폴더는 공통 기능만 남기도록 정리합니다.
components/containers→shared/uihelpers/utils→shared/lib(기능별 그룹화 후)constants→shared/config
선택 단계
섹션 제목: “선택 단계”6단계: 여러 페이지에서 재사용되는 Redux slice를 Entities / Features layer로 분리하기
섹션 제목: “6단계: 여러 페이지에서 재사용되는 Redux slice를 Entities / Features layer로 분리하기”여러 페이지에서 반복적으로 사용되는 Redux slice는 대부분 product, user처럼 명확한 business entity를 표현합니다.
이러한 slice는 Entities layer로 이동하며, entity마다 별도의 폴더를 구성합니다.
반대로, 댓글 작성처럼 사용자의 특정 행동(action) 을 중심으로 한 slice는 Features layer로 옮겨 독립적으로 관리합니다.
Entities와 Features는 서로 의존하지 않고 사용할 수 있도록 설계해야 합니다.
Entity 간의 관계가 필요하다면 Business-Entities Cross-Relations 가이드를 참고해 구조화하면 됩니다.
해당 slice와 연관된 API 함수는 📁 shared/api에 그대로 두어도 괜찮습니다.
7단계: modules 폴더 리팩터링
섹션 제목: “7단계: modules 폴더 리팩터링”📁 modules는 과거에 비즈니스 로직을 모아두던 공간으로, 성격상 Features layer와 비슷합니다.
다만, 앱 Header처럼 large UI block(예: global Header, Sidebar)이라면 Widgets layer로 옮기는 편이 좋습니다.
8단계: shared/ui에 presentational UI 기반 마련하기
섹션 제목: “8단계: shared/ui에 presentational UI 기반 마련하기”📁 shared/ui에는 비즈니스 로직이 전혀 없는, 재사용 가능한 presentational UI 컴포넌트만 남겨야 합니다.
기존 📁 components / 📁 containers에 있던 컴포넌트에서 비즈니스 로직을 분리해 상위 layer로 이동시킵니다.
여러 곳에서 쓰이지 않는 부분은 복사(paste) 해서 각 layer에서 독립적으로 관리해도 문제 없습니다.