요약
Feature-Sliced Design(FSD)은 대규모 프론트엔드 애플리케이션의 구조를 체계적으로 관리하기 위한 아키텍처 방법론입니다. 전통적인 기능별 폴더 구조나 Atomic Design의 한계를 극복하고, 비즈니스 로직의 응집도를 높이면서 계층 간의 의존성을 명확히 관리할 수 있습니다. Next.js App Router와 결합했을 때, Server Components와 Client Components의 역할을 명확히 분리하고, 코드 재사용성과 유지보수성을 극대화할 수 있는 현대적인 프론트엔드 아키텍처 패턴입니다.
핵심 기능/개념 정리
FSD 계층 구조
계층명 | 역할 | 의존성 방향 | Next.js 적용 예시 |
---|---|---|---|
app | 전역 설정, 프로바이더 | 모든 계층 의존 | layout.tsx , providers.tsx |
pages | 라우팅, 페이지 컴포지션 | widgets, features 의존 | App Router page.tsx |
widgets | 페이지 섹션 단위 UI | features, entities 의존 | Header, ProductList, Footer |
features | 사용자 기능, 비즈니스 로직 | entities, shared 의존 | 로그인, 장바구니, 결제 |
entities | 비즈니스 엔티티, 도메인 모델 | shared만 의존 | User, Product, Order |
shared | 공통 코드, 유틸리티 | 외부 라이브러리만 의존 | UI Kit, API Client, Utils |
슬라이스 내부 세그먼트 구조
슬라이스(Slice)란 각 레이어 내에서 비즈니스 엔티티별로 구분 한 것을 말합니다.
- 사진 갤러리: photo/, album/, gallery/
- 소셜 네트워크: post/, user/, newsfeed/
- 이커머스: product/, cart/, order/
세그먼트(Segment)란 목적별로 코드 분할한 것을 말합니다.
feature-slice/
├── ui/ # React 컴포넌트
├── model/ # 상태 관리, 훅, 비즈니스 로직
├── api/ # API 호출 로직
├── lib/ # 유틸리티 함수
├── config/ # 설정값, 상수
└── index.ts # Public API 정의
핵심 원칙 : 계층 구조
app ⬆️ 상위 레이어
pages |
widgets |
features |
entities |
shared ⬇️ 하위 레이어
FSD의 장점
- 유연성: 컴포넌트 쉬운 교체/추가/제거
- 표준화: 일관된 아키텍처 구조
- 확장성: 프로젝트 규모에 따른 확장 용이
- 스택 독립성: 기술 스택에 무관하게 적용 가능
- 비즈니스 지향: 도메인 중심의 구조
- 제어된 결합: 예측 가능한 모듈 간 연결
FSD의 단점
- 높은 진입 장벽: 학습 곡선 존재
- 팀 문화: 전체 팀의 개념 이해 필요
- 즉시 해결: 문제를 미루지 않고 바로 해결해야 함
사용 예시 및 코드 스니펫
실제 이커머스 프로젝트 구조 예시
app/
├── entities/
│ ├── product/
│ │ ├── model/
│ │ │ ├── types.ts # Product 타입 정의
│ │ │ └── hooks.ts # 상품 관련 훅
│ │ ├── ui/
│ │ │ ├── product-card.tsx # 상품 카드 컴포넌트
│ │ │ └── product-image.tsx # 상품 이미지 컴포넌트
│ │ └── index.ts # Public API
│ └── user/
│ ├── model/types.ts # User 타입 정의
│ └── api/customer-api.ts # 고객 API 호출
├── features/
│ ├── auth/
│ │ ├── model/
│ │ │ ├── hooks/
│ │ │ │ ├── use-login.ts # 로그인 훅
│ │ │ │ └── use-logout.ts # 로그아웃 훅
│ │ │ └── types.ts # Auth 타입
│ │ ├── ui/
│ │ │ ├── login-form.tsx # 로그인 폼
│ │ │ └── kakao-login-button.tsx # 카카오 로그인
│ │ └── api/auth-service.ts # 인증 API
│ └── cart/
│ ├── model/hooks/use-cart.ts # 장바구니 훅
│ ├── ui/add-to-cart-button.tsx # 장바구니 추가 버튼
│ └── api/cart-api.ts # 장바구니 API
├── widgets/
│ ├── header/
│ │ ├── ui/header.tsx # 헤더 컴포넌트
│ │ └── model/use-navigation.ts # 네비게이션 로직
│ └── product-list/
│ ├── ui/product-grid.tsx # 상품 그리드
│ └── model/use-product-list.ts # 상품 목록 로직
├── shared/
│ ├── ui/
│ │ ├── button/button.tsx # 공통 버튼
│ │ ├── input/input.tsx # 공통 인풋
│ │ └── modal/modal.tsx # 공통 모달
│ ├── api/
│ │ ├── client.ts # API 클라이언트
│ │ └── graphql-client.ts # GraphQL 클라이언트
│ ├── lib/
│ │ ├── utils.ts # 유틸 함수
│ │ └── validation.ts # 검증 로직
│ └── constants/
│ ├── query-keys.ts # TanStack Query 키
│ └── routes.ts # 라우트 상수
└── (routes)/
├── page.tsx # 홈페이지
├── products/page.tsx # 상품 목록 페이지
└── cart/page.tsx # 장바구니 페이지
Features 계층 구현 예시
// app/features/auth/model/hooks/use-logout.ts
'use client'
import { useLogoutMutation } from 'app/gql/hooks'
import { QUERY_KEYS } from 'app/shared/constants/query-keys'
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { removeAuthToken, removeCartId } from 'app/shared/lib/auth-utils'
export function useLogout() {
const queryClient = useQueryClient()
const router = useRouter()
const logoutMutation = useLogoutMutation({
onSuccess: () => {
removeAuthToken()
removeCartId()
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.USER_ATTRIBUTES]
})
router.push('/')
},
onError: (error) => {
console.error('로그아웃 실패:', error)
},
})
return {
logout: logoutMutation.mutateAsync,
isLoggingOut: logoutMutation.isPending,
}
}
Entities 계층 구현 예시
// app/entities/product/model/types.ts
export interface Product {
id: string
name: string
price: number
imageUrl: string
categoryId: string
availability: 'Available' | 'Preorder' | 'Disabled'
}
export interface ProductCard {
product: Product
onAddToCart?: (productId: string) => void
showQuickView?: boolean
}
// app/entities/product/ui/product-card.tsx
import { Product } from '../model/types'
import { Button } from 'app/shared/ui/button'
interface ProductCardProps {
product: Product
onAddToCart?: (productId: string) => void
}
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<div className="border rounded-lg overflow-hidden shadow-sm">
<img
src={product.imageUrl}
alt={product.name}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="font-semibold text-lg">{product.name}</h3>
<p className="text-gray-600 text-xl font-bold">
{product.price.toLocaleString()}원
</p>
{onAddToCart && (
<Button
onClick={() => onAddToCart(product.id)}
className="mt-3 w-full"
>
장바구니 추가
</Button>
)}
</div>
</div>
)
}
Widgets 계층 구현 예시
// app/widgets/product-list/ui/product-grid.tsx
import { ProductCard } from 'app/entities/product'
import { useAddToCart } from 'app/features/cart'
import { Product } from 'app/entities/product/model/types'
interface ProductGridProps {
products: Product[]
title?: string
}
export function ProductGrid({ products, title }: ProductGridProps) {
const { addToCart } = useAddToCart()
return (
<section className="py-8">
{title && (
<h2 className="text-2xl font-bold mb-6">{title}</h2>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={addToCart}
/>
))}
</div>
</section>
)
}
실제 사용 시 주의점 / Best Practice
1. 의존성 방향 준수
- 상위 계층만 하위 계층을 import:
features
에서entities
를 사용할 수 있지만, 그 반대는 불가 - 같은 계층 내 교차 의존성 금지:
features/auth
에서features/cart
를 직접 import하지 않음 - Shared 계층 의존성 관리: 모든 계층에서
shared
를 사용할 수 있지만,shared
는 외부 라이브러리만 의존
2. Public API 활용
// app/entities/product/index.ts
export { ProductCard } from './ui/product-card'
export { useProductData } from './model/hooks'
export type { Product, ProductCard } from './model/types'
// 다른 계층에서 사용
import { ProductCard, useProductData } from 'app/entities/product'
3. Next.js App Router와의 통합
// app/(routes)/products/page.tsx
import { ProductGrid } from 'app/widgets/product-list'
import { getProducts } from 'app/entities/product/api'
// Server Component에서 초기 데이터 로딩
export default async function ProductsPage() {
const products = await getProducts()
return (
<main className="container mx-auto px-4">
<ProductGrid products={products} title="전체 상품" />
</main>
)
}
4. TanStack Query와의 조화
// app/shared/constants/query-keys.ts
export const QUERY_KEYS = {
PRODUCTS: 'products',
USER_ATTRIBUTES: 'user-attributes',
CART: 'cart',
} as const
// app/entities/product/model/hooks/use-products.ts
import { useQuery } from '@tanstack/react-query'
import { QUERY_KEYS } from 'app/shared/constants'
import { fetchProducts } from '../api/product-api'
export function useProducts() {
return useQuery({
queryKey: [QUERY_KEYS.PRODUCTS],
queryFn: fetchProducts,
})
}
5. 타입 안전성 보장
// app/shared/types/global.ts
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
// Entity 타입에서 활용
export interface Product {
id: string
name: string
price: number
}
export type ProductsResponse = ApiResponse<Product[]>
6. 코드 재사용성 극대화
- UI 컴포넌트 분리: 비즈니스 로직과 UI 로직을 분리하여 재사용성 향상
- Custom Hook 활용: 상태 관리 로직을 훅으로 분리하여 여러 컴포넌트에서 활용
- 유틸리티 함수 공유:
shared/lib
에 공통 로직을 모아 중복 제거
참고자료 / 공식 문서 출처
공식 문서
- Feature-Sliced Design 공식 사이트 - FSD 아키텍처 원칙과 가이드라인
- Next.js App Router 공식 문서 - App Router 구조와 패턴
- React 공식 문서 - Thinking in React - 컴포넌트 설계 원칙
아티클
GitHub 예시 프로젝트
- Feature-Sliced Design Examples - 실제 프로젝트 예시
반응형
'Framework > Nextjs' 카테고리의 다른 글
백엔드 개발자가 다시 프론트엔드로: 2024년 생태계 변화 총정리 (1) | 2025.08.24 |
---|
댓글