2025-08-24 13:53
리액트 넥스트를 위한 FSD 폴더 구조 완벽 핸드북
“이 컴포넌트 어디에 둬야 하지?” 프론트엔드 개발자라면 누구나 한 번쯤 해봤을 고민입니다. 프로젝트가 커지고 팀원이 늘어날수록 코드베이스는 정글처럼 변하기 쉽습니다. 어디에 무엇이 있는지 찾기 어렵고, 작은 수정 하나가 예상치 못한 곳에서 버그를 터뜨리기도 합니다. 이런 혼돈 속에서 우리를 구해줄 등대 같은 존재가 바로 FSD(Feature-Sliced Design) 아키텍처입니다.
FSD는 단순히 폴더를 예쁘게 정리하는 방법을 넘어, 확장 가능하고 유지보수하기 쉬우며, 팀원 모두가 쉽게 이해할 수 있는 코드베이스를 만드는 철학입니다. 이 핸드북에서는 FSD가 왜 탄생했는지부터 시작하여 그 핵심 구조를 파헤치고, 실제 리액트와 넥스트 프로젝트에 어떻게 적용하는지 구체적인 예시와 함께 완벽하게 마스터해 보겠습니다.
1. FSD는 왜 세상에 나왔을까? 기존 구조의 한계
FSD를 이해하려면 먼저 기존 폴더 구조가 어떤 문제들을 안고 있었는지 알아야 합니다. 흔히 사용되는 구조는 크게 두 가지가 있습니다.
-
타입 기반 구조 (Component-Based):
components
,hooks
,apis
,utils
처럼 코드의 종류에 따라 폴더를 나눕니다. 작은 프로젝트에서는 직관적이지만, 프로젝트가 커지면 문제가 발생합니다.- 문제점: 특정 기능을 수정하려면
components
,hooks
,apis
폴더를 모두 헤집고 다녀야 합니다. 기능의 응집도가 떨어지고, 관련 코드가 여러 곳에 흩어져 있어 파악하기 어렵습니다. 마치 요리책이 ‘재료’별로만 정리되어 있어서 ‘김치찌개’ 레시피를 찾으려면 ‘두부’ 섹션, ‘돼지고기’ 섹션, ‘김치’ 섹션을 모두 뒤져야 하는 것과 같습니다.
- 문제점: 특정 기능을 수정하려면
-
도메인 기반 구조 (Domain-Based):
user
,product
,order
처럼 비즈니스 도메인을 기준으로 폴더를 나눕니다. 타입 기반 구조보다는 낫지만, 여전히 모호함이 존재합니다.- 문제점: 여러 도메인에 걸친 기능은 어디에 둬야 할까요? 예를 들어, ‘상품 상세 페이지에서 해당 상품을 등록한 유저의 프로필 보기’ 기능은
product
폴더에 있어야 할까요,user
폴더에 있어야 할까요? 이런 경계가 불분명해지면서 결국 구조가 무너지기 시작합니다.
- 문제점: 여러 도메인에 걸친 기능은 어디에 둬야 할까요? 예를 들어, ‘상품 상세 페이지에서 해당 상품을 등록한 유저의 프로필 보기’ 기능은
FSD는 이러한 문제들을 해결하기 위해 등장했습니다. **관심사의 분리(Separation of Concerns)**를 극단적으로 추구하되, 그 기준을 명확한 규칙으로 제시하여 코드베이스의 예측 가능성과 안정성을 높이는 것을 목표로 합니다.
2. FSD의 핵심 철학 3가지
FSD는 세 가지 핵심적인 개념으로 이루어져 있습니다. 바로 계층(Layers), 슬라이스(Slices), **세그먼트(Segments)**입니다. 이 세 가지를 이해하면 FSD의 90%를 이해한 것과 같습니다.
가. 계층 (Layers)
FSD는 코드를 7개의 명확한 계층으로 나눕니다. 이 계층들은 위에서 아래로 향하는 의존성 규칙을 가집니다. 즉, 상위 계층은 하위 계층의 코드를 가져와 사용할 수 있지만, 하위 계층은 절대 상위 계층의 코드를 참조할 수 없습니다. 이 규칙이 FSD의 핵심이며, 코드베이스를 안정적으로 유지하는 비결입니다.
계층 (Layer) | 설명 | 예시 |
---|---|---|
1. app | 애플리케이션의 가장 상위 계층. 전체 앱의 설정, 라우팅, 글로벌 스타일, 공통 레이아웃 등을 담당합니다. | App.tsx , providers , styles , router |
2. processes | (선택적) 여러 페이지에 걸쳐 일어나는 복잡한 비즈니스 프로세스를 다룹니다. 로그인/회원가입 플로우 등. | auth-flow , payment-process |
3. pages | 특정 페이지를 구성하는 단위. 여러 위젯, 피처, 엔티티를 조합하여 하나의 완전한 페이지를 만듭니다. | HomePage , ProductDetailPage |
4. widgets | 독립적인 UI 블록. 여러 피처와 엔티티를 묶어 의미 있는 단위로 만듭니다. 헤더, 사이드바, 상품 목록 등. | Header , ProductList , UserProfileSidebar |
5. features | 사용자의 구체적인 액션, 즉 비즈니스 로직을 담는 단위. 하나의 명확한 기능을 수행합니다. | add-to-cart , user-search , send-comment |
6. entities | 핵심 비즈니스 개체. 데이터와 그 데이터를 보여주는 순수한 UI 컴포넌트로 구성됩니다. | user , product , order |
7. shared | 가장 낮은 계층. 특정 비즈니스 로직에 의존하지 않는 순수한 코드. 모든 계층에서 재사용 가능합니다. | ui-kit (Button, Input), config , api , lib |
이 계층 구조는 마치 레고 블록을 쌓는 것과 같습니다. 가장 아래에는 어떤 모양에도 쓸 수 있는 기본 블록(shared
)이 있고, 그 위로 특정 모델을 표현하는 블록(entities
), 기능을 가진 블록(features
) 등을 차례로 쌓아 최종적으로 멋진 성(pages
, app
)을 완성하는 원리입니다.
나. 슬라이스 (Slices)
features
, entities
, pages
, widgets
계층 내부는 기능(도메인)별로 잘게 쪼개지는데, 이 단위를 ‘슬라이스’라고 부릅니다. 슬라이스는 특정 기능을 책임지는 독립적인 모듈입니다.
예를 들어, entities
계층 안에는 user
, product
, order
와 같은 슬라이스가 존재할 수 있고, features
계층 안에는 add-to-cart
, user-login
과 같은 슬라이스가 존재할 수 있습니다.
src/
├── features/
│ ├── add-to-cart/ # 'add-to-cart' 슬라이스
│ └── user-login/ # 'user-login' 슬라이스
└── entities/
├── user/ # 'user' 슬라이스
└── product/ # 'product' 슬라이스
슬라이스의 가장 중요한 규칙은 다른 슬라이스를 직접 참조할 수 없다는 것입니다. user
슬라이스가 product
슬라이스의 내부 코드를 직접 import해서 사용하는 것은 금지됩니다. 슬라이스 간의 조합은 오직 상위 계층(widgets
, pages
)에서만 이루어져야 합니다. 이는 기능 간의 결합도를 낮춰 독립성을 보장합니다.
다. 세그먼트 (Segments)
각 슬라이스 내부는 역할에 따라 다시 한번 폴더로 나뉩니다. 이를 ‘세그먼트’라고 합니다. 가장 일반적인 세그먼트는 다음과 같습니다.
-
ui
: UI 컴포넌트 (React 컴포넌트, 스타일 등) -
model
: 비즈니스 로직 (상태 관리 로직, hooks, selectors 등) -
api
: 이 슬라이스에서만 사용하는 API 요청 함수 -
lib
: 이 슬라이스에서만 사용하는 헬퍼 함수
src/
└── features/
└── add-to-cart/
├── ui/
│ ├── AddToCartButton.tsx
│ └── AddToCartButton.module.css
├── model/
│ └── useAddToCart.ts
└── index.ts # Public API
그리고 각 슬라이스는 외부로 노출할 코드들을 index.ts
파일(Public API)을 통해 내보냅니다. 다른 모듈에서는 이 index.ts
를 통해서만 해당 슬라이스의 기능에 접근할 수 있습니다. 이는 마치 클래스의 public
메서드처럼, 내부 구현을 숨기고 외부와의 명확한 계약을 정의하는 캡슐화 역할을 합니다.
3. 리액트 & 넥스트에 FSD 적용하기
이제 이론을 바탕으로 실제 프로젝트에 FSD를 적용해 봅시다.
전체 폴더 구조 예시
src/
├── app/
│ ├── providers/ # ThemeProvider, StoreProvider 등
│ ├── styles/ # global.css, reset.css
│ └── index.tsx # 애플리케이션 진입점
├── processes/
│ └── auth/ # 회원가입/로그인 프로세스
├── pages/
│ ├── home/
│ │ └── index.tsx # HomePage 컴포넌트
│ └── product-details/
│ └── index.tsx # ProductDetailsPage 컴포넌트
├── widgets/
│ ├── header/
│ │ ├── ui/
│ │ └── index.ts
│ └── product-list/
│ ├── ui/
│ └── index.ts
├── features/
│ ├── add-to-cart/
│ │ ├── ui/
│ │ ├── model/
│ │ └── index.ts
│ └── theme-toggler/
│ ├── ui/
│ └── index.ts
├── entities/
│ ├── product/
│ │ ├── ui/
│ │ ├── model/
│ │ └── index.ts
│ └── user/
│ ├── ui/
│ └── index.ts
└── shared/
├── ui/ # Button, Input, Modal 등 재사용 UI
├── api/ # 공통 API 인스턴스, 요청 함수
├── config/ # 환경 변수, 라우팅 경로 등
├── lib/ # 공통 유틸리티 함수 (formatDate 등)
└── assets/ # 이미지, 폰트 등
넥스트(Next.js)와 FSD의 환상적인 궁합
넥스트의 App Router는 FSD의 pages
계층과 매우 유사한 개념을 가지고 있어 FSD를 적용하기에 아주 좋습니다.
-
Next.js의
app/
디렉토리: FSD의pages
계층 역할을 합니다.app/products/[id]/page.tsx
와 같은 파일이 FSD의pages/product-details
슬라이스에 해당합니다. -
layout.tsx
: FSD의app
계층에서 관리하던 공통 레이아웃 역할을 합니다. 여기에widgets/Header
등을 배치할 수 있습니다. -
page.tsx
: 이 파일이 바로 페이지를 조합하는 공간입니다. 필요한 위젯과 피처들을 이곳에서 불러와 조립합니다.
예시: app/page.tsx
(홈페이지)
// src/pages/home/index.tsx 또는 Next.js의 app/page.tsx
import { Header } from "@/widgets/header";
import { ProductList } from "@/widgets/product-list";
import { Footer } from "@/widgets/footer";
// 이 파일은 FSD의 'pages' 계층에 속합니다.
// 상위 계층으로서 하위 계층인 'widgets'를 자유롭게 가져와 조합합니다.
export default function HomePage() {
return (
<div>
<Header />
<main>
<h1>우리 가게 베스트 상품</h1>
<ProductList />
</main>
<Footer />
</div>
);
}
예시: widgets/product-list
// src/widgets/product-list/ui/ProductList.tsx
import { useEffect, useState } from "react";
import { ProductCard, productModel } from "@/entities/product";
import { AddToCartButton } from "@/features/add-to-cart";
// 이 파일은 'widgets' 계층에 속합니다.
// 하위 계층인 'entities'와 'features'를 가져와 조합합니다.
export const ProductList = () => {
// entities/product/model 에서 상태나 데이터를 가져올 수 있습니다.
const products = productModel.useProducts(); // 가상의 훅
return (
<div>
{products.map((product) => (
// ProductCard는 entities/product 에서, AddToCartButton은 features/add-to-cart 에서 가져옵니다.
// 이렇게 위젯은 더 작은 단위들을 조립하는 역할을 합니다.
<ProductCard
key={product.id}
product={product}
actions={<AddToCartButton productId={product.id} />}
/>
))}
</div>
);
};
예시: entities/product
// src/entities/product/ui/ProductCard.tsx
import { ReactNode } from "react";
import { Product } from "@/shared/api/types"; // shared 계층의 타입을 사용
// 이 파일은 'entities' 계층에 속합니다.
// 비즈니스 개체(Product)의 데이터를 표현하는 순수한 UI 컴포넌트입니다.
// 'actions' prop을 통해 상위 계층(widgets)에서 기능을 주입받습니다.
// 이를 통해 ProductCard는 '장바구니 담기' 기능에 대해 전혀 알 필요가 없습니다. (의존성 역전)
interface ProductCardProps {
product: Product;
actions?: ReactNode;
}
export const ProductCard = ({ product, actions }: ProductCardProps) => {
return (
<div>
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}원</p>
{actions}
</div>
);
};
이 예시들에서 볼 수 있듯이, 각 컴포넌트는 자신의 계층에 맞는 역할만 수행하며 명확한 의존성 방향을 따릅니다. 이로 인해 ProductCard
는 어떤 기능(actions
)이 들어올지 신경 쓰지 않고 재사용될 수 있으며, ProductList
는 비즈니스 요구사항에 따라 다양한 피처를 조합하여 보여줄 수 있습니다.
4. FSD 심화 학습 및 주의사항
-
상태 관리(State Management): 상태 관리 로직(Zustand, Jotai, Redux 등)은 주로
model
세그먼트에 위치합니다. 여러 슬라이스에 걸친 전역 상태는app/providers
에서 관리하거나, 특정 엔티티에 종속된 상태는 해당entities
슬라이스의model
에 두는 것이 일반적입니다. -
절대적인 규칙은 아니다: FSD는 엄격한 가이드라인이지만, 프로젝트의 특성에 따라 유연하게 변형할 수 있습니다. 예를 들어 아주 작은 기능은
features
슬라이스를 만들지 않고widgets
에서 바로 구현할 수도 있습니다. 중요한 것은 ‘왜’ 이 구조를 선택했는지 팀원 모두가 동의하고 이해하는 것입니다. -
초기 학습 비용: FSD는 처음 도입할 때 학습 곡선이 존재합니다. 팀원 전체가 FSD의 철학과 규칙을 공유하는 시간이 반드시 필요합니다. 하지만 이 비용을 치르고 나면, 장기적으로는 훨씬 더 큰 생산성 향상으로 돌아올 것입니다.
결론: 혼돈에서 질서로
FSD는 단순히 폴더를 나누는 기술이 아닙니다. 복잡한 소프트웨어를 예측 가능하고, 유지보수하기 쉬우며, 확장 가능하게 만드는 아키텍처 설계 철학입니다. FSD를 도입하면 다음과 같은 명확한 이점을 얻을 수 있습니다.
-
표준화: 누가 코드를 작성하든 일관된 구조를 유지하여 팀원 간의 코드 이해도를 높입니다.
-
재사용성: 각 슬라이스는 독립적으로 개발되어 다른 곳에서 쉽게 재사용될 수 있습니다.
-
안정성: 명확한 의존성 규칙 덕분에 한 부분의 수정이 다른 부분에 미치는 영향을 최소화합니다.
-
쉬운 리팩토링: 기능 단위로 코드가 캡슐화되어 있어 리팩토링이나 기능 제거가 매우 용이합니다.
처음에는 다소 복잡하고 과하게 느껴질 수 있지만, 한번 FSD의 세계에 발을 들이면 그 명확함과 안정성에 매료될 것입니다. 더 이상 “이 파일 어디에 둬야 하지?”라는 고민으로 시간을 낭비하지 마세요. FSD와 함께 혼돈의 코드베이스에 질서를 부여하고, 진정으로 중요한 비즈니스 로직 개발에 집중해 보시길 바랍니다.