기존 아키텍처에서 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) 의 승인을 사전에 확보하고 이점을 공유하세요.
- FSD 전환은 단계적으로 진행할 수 있어 기존 기능 개발을 중단하지 않아도 됩니다.
- 명확한 아키텍처 구조는 신규 개발자 온보딩 시간을 단축합니다.
- 공식 문서를 활용하면 별도 문서 유지·관리 비용을 절감할 수 있습니다.
마이그레이션을 시작하기로 결정했다면, 📁 src 폴더에 별칭(alias)을 설정하는 것을 첫 단계로 삼으세요.
1단계: 페이지 단위로 코드 분리하기
대부분의 커스텀 아키텍처는 규모와 관계없이 이미 어느 정도 페이지 단위로 코드를 나누고 있습니다.
📁 pages 폴더가 있다면 이 단계를 건너뛰어도 됩니다.
위에 예시 폴더처럼 📁 routes만 있다면 다음 순서를 따르세요.
📁 pages폴더를 새로 만듭니다.📁 routes에 있던 페이지용 컴포넌트를 가능한 한 모두📁 pages폴더로 옮깁니다.- 코드를 옮길 때마다 해당 페이지 전용 폴더를 만들고 그 안에
index.tsx파일을 추가해 진입점(entry point) 를 노출합니다.
이 단계에서는 Page A에서 Page B의 코드를 import해도 괜찮습니다.
나중 단계에서 이러한 의존성을 분리할 예정이니, 우선 페이지 폴더를 만드는 것에 집중하세요.
📁 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단계: 페이지 외부 코드를 분리하기
📁 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 해결
한 페이지가 다른 페이지의 코드를 직접 import하고 있다면, 아래 두 가지 방식 중 하나로 의존성을 정리합니다.
| 방법 | 사용 시점 |
|---|---|
| A. 코드 복사하여 독립시키기 | 페이지별로 로직이 달라질 가능성이 높거나, 재사용성이 낮은 경우 |
| B. Shared로 이동하여 공통화하기 | 여러 페이지에서 반복적으로 사용되는 경우 |
- Shared 이동 위치 예시
- UI 구성 요소 →
📁 shared/ui - 설정 상수 →
📁 shared/config - 백엔드 호출 →
📁 shared/api
- UI 구성 요소 →
코드를 복사하는 것은 잘못이 아닙니다.
경우에 따라서는 중복을 허용하더라도 페이지 간 의존성을 줄이는 것이 더 중요합니다.
다만, 비즈니스 로직처럼 변경 가능성이 큰 핵심 부분은 중복을 피하고, 복사할 때에도 가능한 한 DRY 원칙을 고려합니다.
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 정리
| 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로 분리하기
여러 페이지에서 반복적으로 사용되는 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 폴더 리팩터링
📁 modules는 과거에 비즈니스 로직을 모아두던 공간으로, 성격상 Features layer와 비슷합니다.
다만, 앱 Header처럼 large UI block(예: global Header, Sidebar)이라면 Widgets layer로 옮기는 편이 좋습니다.
8단계: shared/ui에 presentational UI 기반 마련하기
📁 shared/ui에는 비즈니스 로직이 전혀 없는, 재사용 가능한 presentational UI 컴포넌트만 남겨야 합니다.
기존 📁 components / 📁 containers에 있던 컴포넌트에서 비즈니스 로직을 분리해 상위 layer로 이동시킵니다.
여러 곳에서 쓰이지 않는 부분은 복사(paste) 해서 각 layer에서 독립적으로 관리해도 문제 없습니다.