콘텐츠로 이동

기존 아키텍처에서 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) 의 승인을 사전에 확보하고 이점을 공유하세요.


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

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

섹션 제목: “1단계: 페이지 단위로 코드 분리하기”

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

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

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

📁 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단계: 페이지 외부 코드를 분리하기

섹션 제목: “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

한 페이지에서만 사용되는 코드는 해당 페이지의 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용도 예시
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/containersshared/ui
    • helpers/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) 을 중심으로 한 sliceFeatures layer로 옮겨 독립적으로 관리합니다.

EntitiesFeatures는 서로 의존하지 않고 사용할 수 있도록 설계해야 합니다.
Entity 간의 관계가 필요하다면 Business-Entities Cross-Relations 가이드를 참고해 구조화하면 됩니다.
해당 slice와 연관된 API 함수는 📁 shared/api에 그대로 두어도 괜찮습니다.

📁 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에서 독립적으로 관리해도 문제 없습니다.