주요 콘텐츠로 건너뛰기

기존 아키텍처에서 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)이 정말 필요한지 먼저 확인하세요.
모든 프로젝트가 새로운 아키텍처를 요구하는 것은 아닙니다.

전환을 고려해야 할 징후​

  1. 신규 팀원이 프로젝트에 적응하기 어려워하는 경우
  2. 코드 일부를 수정할 때, 관련 없는 다른 코드에 오류가 발생하는 경우가 잦은 경우
  3. 새 기능을 추가할 때 고려해야 할 사항이 너무 많아 어려움을 겪는 경우

팀의 합의 없이 FSD 전환을 시작하지 마세요.
팀 리더라도 전환의 이점이 학습/전환 비용을 상회한다는 점을 먼저 설득해야 합니다.
또한, 개선 효과가 바로 눈에 띄지 않을 수 있으므로 팀원 및 프로젝트 매니저(PM) 의 승인을 사전에 확보하고 이점을 공유하세요.

PM 설득 시 고려할 사항
  • FSD 전환은 단계적으로 진행할 수 있어 기존 기능 개발을 중단하지 않아도 됩니다.
  • 명확한 아키텍처 구조는 신규 개발자 온보딩 시간을 단축합니다.
  • 공식 문서를 활용하면 별도 문서 유지·관리 비용을 절감할 수 있습니다.

마이그레이션을 시작하기로 결정했다면, 📁 src 폴더에 별칭(alias)을 설정하는 것을 첫 단계로 삼으세요.

1단계: 페이지 단위로 코드 분리하기​

대부분의 커스텀 아키텍처는 규모와 관계없이 이미 어느 정도 페이지 단위로 코드를 나누고 있습니다.
📁 pages 폴더가 있다면 이 단계를 건너뛰어도 됩니다.

위에 예시 폴더처럼 📁 routes만 있다면 다음 순서를 따르세요.

  1. 📁 pages 폴더를 새로 만듭니다.
  2. 📁 routes에 있던 페이지용 컴포넌트를 가능한 한 모두 📁 pages 폴더로 옮깁니다.
  3. 코드를 옮길 때마다 해당 페이지 전용 폴더를 만들고 그 안에 index.tsx 파일을 추가해 진입점(entry point) 를 노출합니다.
note

이 단계에서는 Page A에서 Page B의 코드를 import해도 괜찮습니다.
나중 단계에서 이러한 의존성을 분리할 예정이니, 우선 페이지 폴더를 만드는 것에 집중하세요.

📁 Route File

route file:src/routes/products.[id].js
export { ProductPage as default } from "src/pages/product";

📁 Page Index File

src/pages/product/index.js
export { ProductPage } from "./ProductPage.jsx";

📁 Page Component File

src/pages/product/ProductPage.jsx
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
note

코드를 복사하는 것은 잘못이 아닙니다.
경우에 따라서는 중복을 허용하더라도 페이지 간 의존성을 줄이는 것이 더 중요합니다.
다만, 비즈니스 로직처럼 변경 가능성이 큰 핵심 부분은 중복을 피하고, 복사할 때에도 가능한 한 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용도 예시
uiComponents, formatters, styles
apiBackend requests, DTOs, mappers
modelStore, schema, business logic
libShared utilities / helpers
configConfiguration files, feature flags

무엇인지가 아니라 무엇을 위해 존재하는지를 기준으로 폴더를 구분합니다.
따라서 components, utils, types처럼 목적이 모호한 폴더 이름은 지양합니다.

  1. 각 페이지 내부에서, 필요한 segment(ui, model, api 등)를 구성합니다.
  2. Shared 폴더는 공통 기능만 남기도록 정리합니다.
    • components/containers → shared/ui
    • helpers/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에서 독립적으로 관리해도 문제 없습니다.

참고 자료​