티스토리 뷰

반응형

프론트엔드 프로젝트를 진행할 때 마다 어떤 프로젝트 구조로 진행할지 고민을 많이 하게 됩니다.
필자는 도메인 기반, 컨택스트 기반 으로 프로젝트를 진행하다가 신규 프로젝트에서는 React 에 FSD 를 차용하여 Nextjs 에서 App Router 와 FSD 를 합친 구조를 만들게 되었습니다.
app 아래에서는 특정 도메인 형태로 구분하되, 그 외 기능 기반을 별도 FSD 형태로 관리하는 구조 입니다.
다만 Next.js App Router 환경에 그대로 가져오면 충돌이 생기는 부분이 있어서, App Router의 특성에 맞게 구조를 조정해야 합니다. 이 글은 FSD를 Next.js App Router에 최적화해서 도입하는 과정을 담고 있습니다.

목차

FSD를 그대로 적용하면 생기는 문제

FSD의 표준 레이어 구성입니다.

app → pages → widgets → features → entities → shared

 

이 구조를 Next.js App Router 프로젝트에 그대로 가져오면 두 가지 문제가 생깁니다.

첫째, src/pages/는 Next.js가 Pages Router 디렉토리로 인식합니다. App Router(src/app/)와 Pages Router가 동시에 활성화되어 빌드가 꼬이거나 의도하지 않은 라우팅이 발생합니다.

둘째, widgets 레이어는 역할이 features와 겹치는 경우가 많습니다. "이게 widget이야, feature야?"라는 판단 비용이 반복해서 생깁니다. (불필요한 레이어가 많습니다. App Router 구조를 활용하면서 필요한 레이어만 이식합시다.)

두 문제를 해결하면 App Router 환경에서 FSD가 훨씬 자연스럽게 동작합니다.

pages 레이어를 app/으로 대체하기

App Router의 app/ 폴더는 page.tsxroute.ts가 없으면 어떤 파일도 라우트로 노출되지 않습니다. 즉, 페이지 전용 컴포넌트를 별도 레이어로 분리하지 않아도 라우트 폴더 안에 함께 두면 됩니다. App Router가 FSD의 pages 레이어 역할을 자연스럽게 대신합니다.

페이지에서만 쓰는 컴포넌트는 해당 라우트 폴더 안에 components/로 co-locate합니다. co-locate는 관련 파일을 같은 폴더에 함께 두는 것을 의미합니다. 역할별로 분리(components/, hooks/, utils/)하는 대신 사용되는 위치를 기준으로 묶습니다. 해당 페이지가 사라지면 폴더 하나만 지우면 됩니다.

src/app/
└── benefit/
    ├── page.tsx
    ├── components/       # 이 페이지에서만 쓰는 컴포넌트
    └── [id]/
        ├── page.tsx
        └── components/

이 방식은 App Router의 co-location 철학과도 일치합니다. 파일이 어디 있는지 찾으려면 라우트 구조를 따라가면 됩니다.

widgets 레이어 판단 기준

widgets는 여러 feature를 조합한 복합 UI 블록을 담는 레이어입니다. 프로젝트에 따라 유용할 수 있지만, 다음 기준으로 판단하는 것이 좋습니다.

widgets가 필요한 경우:

  • 여러 feature를 조합하는 복잡한 UI 블록이 반복해서 등장할 때
  • features와 역할이 명확히 구분될 때

widgets 없이 features로 흡수해도 되는 경우:

  • 대부분의 복합 UI가 특정 도메인의 여러 페이지에서 공유되는 수준일 때
  • "이게 widget이야, feature야?" 판단이 계속 모호할 때

판단이 계속 애매하다면 widgets를 두지 않고 features에서 관리하는 것이 팀의 결정 비용을 줄입니다.
이 경우 의존 방향은 다음과 같이 단순해집니다.

app → features → entities → shared

최종 구조

src/
├── app/                      # Next.js 16 라우팅 + 페이지 전용 코드
│   ├── layout.tsx            # Provider 주입만
│   ├── {domain}/             # 도메인명 (예: benefit, coupon, user)
│   │   ├── page.tsx
│   │   ├── components/       # 이 페이지에서만 쓰는 컴포넌트
│   │   └── [id]/
│   │       ├── page.tsx
│   │       └── components/
│   └── api/
│       └── {resource}/
│           └── route.ts
│
├── features/                 # 같은 도메인 여러 페이지에서 공유
│   └── {feature-name}/
│       ├── ui/
│       ├── model/
│       └── index.ts
│
├── entities/                 # 여러 도메인에서 공유
│   └── {entity-name}/
│       ├── ui/
│       ├── model/
│       └── index.ts
│
└── shared/                   # 순수 재사용 코드
    ├── ui/
    ├── api/
    ├── lib/
    └── config/

 

proxy.ts(구 middleware.ts)는 Next.js 런타임이 직접 인식하는 파일이므로 프로젝트 루트에 둡니다.

코드 배치 기준

FSD의 핵심은 "이 코드가 얼마나 넓은 범위에서 쓰이는가"로 위치를 결정하는 것입니다. 다음 순서로 판단합니다.

여러 페이지에서 쓰이는가?
├── NO  → app/{도메인}/ 안에 co-locate
└── YES → 같은 도메인의 여러 페이지에서 쓰이는가?
          ├── YES → features/{도메인}/
          └── NO  → 여러 도메인에 걸쳐 쓰이는가?
                    ├── YES → entities/{도메인}/
                    └── NO  → shared/

 

처음 도입할 때는 모든 코드를 app/ 안에 작성하고, 재사용이 필요해질 때 위 기준에 따라 올려보내는 Bottom-Up 방식을 권장합니다. 처음부터 레이어를 예측해서 나누려 하면 판단 비용이 커집니다.

슬라이스 외부에서는 반드시 index.ts를 통해서만 import합니다.

// ✅
import { BenefitCard } from "@/entities/benefit"

// ❌
import { BenefitCard } from "@/entities/benefit/ui/BenefitCard"

이 규칙 덕분에 슬라이스 내부를 변경해도 외부에 영향을 주지 않습니다.

마무리

최종적 구조

FSD를 Next.js App Router에 도입할 때 핵심은 두 가지입니다. pages 레이어는 App Router의 co-location으로 대체하고, widgets 레이어는 프로젝트 상황에 맞게 유지하거나 features에 흡수합니다. 레이어가 단순해질수록 코드 위치에 대한 팀 내 논쟁도 줄어듭니다.

FSD는 규칙을 따르기 위한 도구가 아니라 "이 코드가 어디까지 영향을 미치는가"를 명확히 하기 위한 도구입니다. App Router의 특성을 활용하면 FSD의 의도를 더 자연스럽게 살릴 수 있습니다.

 

참고 자료

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함