FSD(Feature-Sliced Design)은 복잡한 프론트엔드 애플리케이션의 구조를 체계적으로 조직화하기 위해 고안된 아키텍처 방법론입니다.
FSD를 구현하는 간단한 예시를 알아보겠습니다.
- 📁 app
- 📁 pages
- 📁 shared
이러한 최상위 폴더를 레이어라고 부릅니다.
- 📂 app
- 📁 routes
- 📁 analytics
- 📂 pages
- 📁 home
- 📂 article-reader
- 📁 ui
- 📁 api
- 📁 settings
- 📂 shared
- 📁 ui
- 📁 api
폴더 안에 있는 폴더를 슬라이스라고 합니다.
슬라이스는 레이어를 도메인(페이지)별로 나눕니다.

FSD 구조는 크게 레이어, 슬라이스, 세그먼트 총 3가지의 계층 구조로 나눌 수 있습니다.
FSD 프로젝트의 레이어는 표준화되어 있습니다.
각 계층은 다시 슬라이스 또는 세그먼트의 단위로 수직 분할됩니다.
슬라이스는 특정 도메인이나 기능을 나타내며, 그 안에 해당 기능과 관련된 모든 코드를 모아놓습니다.
슬라이스는 애플리케이션의 비즈니스 도메인에 따라 직접 결정되므로 표준화되지 않습니다. 예를 들어 사진 갤러리에는 photo, effects와 같은 슬라이스가 있을 수 있습니다.

src/
└── entities/
└── article/ <-- 'article' 이라는 슬라이스
├── model/ // 아티클의 상태 관리 (Redux/Zustand 등)
├── ui/ // 아티클을 보여주는 컴포넌트
└── api/ // 아티클 관련 API 요청 로직
FSD의 가장 큰 장점은 모든 코드가 정의된 위치에 존재하며
엄격한 의존성 규칙을 따르기 위해 코드가 얽히지 않고 독립적으로 유지될 수 있습니다.
src/
└── features/
├── addToCart/ <-- '장바구니에 담기' 기능 슬라이스
│ ├── model/ // 장바구니 추가 관련 상태 및 로직
│ └── ui/ // '장바구니에 담기' 버튼 컴포넌트
├── productReview/ <-- '상품 리뷰' 기능 슬라이스
│ ├── model/ // 리뷰 작성, 조회 관련 로직
│ └── ui/ // 리뷰 목록 및 작성 폼 컴포넌트
└── productDetails/ <-- '상품 상세' 화면 슬라이스
├── model/ // 상세 페이지 데이터 로직
└── ui/ // 상세 페이지 레이아웃 및 컴포넌트
src/
└── entities/
├── products/ <-- '상품 데이터' 도메인 슬라이스
│ ├── model/ // 상품 데이터 CRUD 로직 (API 포함)
│ └── ui/ // 상품 카드/목록 컴포넌트 (UI)
├── cart/ <-- '장바구니 데이터' 도메인 슬라이스
│ ├── model/ // 장바구니 상태, 수량 변경 로직
│ └── ui/ // 장바구니 목록 컴포넌트
└── orders/ <-- '주문 데이터' 도메인 슬라이스
├── model/ // 주문 생성, 조회 로직
└── ui/ // 주문 내역 컴포넌트
두 쇼핑몰 모두 FSD 원칙을 따르지만 핵심 기능을 다루는 방식이 다릅니다.
쇼핑몰 A는 addToCart라는 슬라이스를 만들어 장바구니에 담는 행위에 집중
쇼핑몰 B는 cart라는 슬라이스를 만들어 장바구니 데이터 및 관련 UI에 집중합니다.
슬라이스 내부에서 코드는 원하는 방식으로 구성할 수 있습니다.
모든 슬라이스에는 공개 API만 참조할 수 있습니다.
슬라이스 외부의 다른 코드가 이 슬라이스와 상호작용할 수 있도록 접점을 정의하는 파일입니다.
일반적으로 슬라이스 폴더의 최상단에 있는 index.ts 파일이 이 역할을 수행합니다.
공개 API를 사용하는 주된 이유는 낮은 결합도와 쉬운 유지보수를 달성하기 위함입니다.
폴더 구조
src/
└── entities/
└── article/
├── model/
│ ├── selectors.ts // 내부에서만 사용하는 셀렉터
│ └── slice.ts // Redux 슬라이스 로직
├── ui/
│ └── ArticleCard/ // 아티클 카드 컴포넌트
│ └── ArticleCard.tsx
└── index.ts // 공개 API 파일 (Public API)
index.js는 슬라이스 외부로 노출할 요소만 선택적으로 export합니다.
// 1. 필요한 UI 컴포넌트만 Export
export { ArticleCard } from './ui/ArticleCard/ArticleCard';
// 2. 외부에서 상태를 읽기 위해 필요한 셀렉터만 Export
export {
getArticleTitle,
getArticleText
} from './model/selectors';
// 3. 외부에서 상태를 변경하기 위해 필요한 액션만 Export
// (slice.ts 내부에 있지만 index.ts를 통해 외부에 노출)
export {
fetchArticleById
} from './model/slice';
다른 슬라이스에서 article 슬라이스 기능을 가져다 쓸 때는 오직 공개 API를 통해서만 가져옵니다.
// 공개 API를 통한 안전한 Import
import { ArticleCard, getArticleTitle } from 'entities/article';
function CommentForm({ articleId }) {
const title = getArticleTitle(articleId);
return (
<>
<ArticleCard articleId={articleId} />
<h2>{title}에 대한 댓글</h2>
{/* ... */}
</>
);
}
인덱스 파일은 index.js공개 API를 정의하는 가장 일반적인 방법입니다. 쉽게 만들 수 있지만, 특정 번들러와 프레임워크에서 문제를 일으키는 것으로 알려져 있습니다.
두 개 이상의 파일이 서로 간접적 또는 직접적으로 가져오는 현상을 말합니다.

번들러가 이를 처리하기 어렵거나, 예상치 못한 런타임 오류를 발생시킬 수 있습니다.
인덱스 파일은 슬라이스의 모든 기능을 한곳에 모아두기 때문에 슬라이스 내부의 파일들이 서로를 참조하게 만들어 순환 가져오기를 유발할 가능성이 높아집니다.
순환 가져오기 예시 :

HomePage (자식) -> index.js (부모)index.js (부모) -> HomePage (자식)를 Exportindex.js를 거치지 않고, pages/home/ui/HomePage.jsx가 pages/home/api/loadUserStatistics를 직접 가져오도록 합니다.트리 셰이킹은 모듈 번들러(Webpack, Rollup 등) 실제 사용되지 않는 코드를 최종 번들에서 제거하여 파일 크기를 줄이는 최적화 과정입니다.
인덱스 파일이 해당 슬라이스 내부의 모든 것을 export * from ... 형태로 다시 내보낼 경우, 일부 번들러는 모든 것을 사용한다고 오해하여 트리 셰이킹이 제대로 작동하지 않을 수 있습니다.
일반적인 슬라이스는 문제가 덜합니다.
하지만 공유(Share) 레이어의 shared/ui나 shared/lib와 같이 서로 관련이 없는 다양한 모듈의 모음에서는 이 문제가 심각해질 수 있습니다.
shared/ui의 인덱스 파일이 Button, TextField, Carousel 등 수십 가지 컴포넌트를 모두 내보낸다고 가정해 봅시다.만약 어떤 페이지에서 Button 하나만 가져다 써도, 번들러가 Carousel이나 Accordion의 코드와 그에 딸린 무거운 라이브러리 의존성까지 최종 번들에 포함시켜 버릴 수 있습니다.
shared/ui나 shared/lib처럼 응집도가 낮은 공유 폴더의 경우,
단일 공개 API(shared/ui/index.js)를 포기하고 모듈별로 별도의 인덱스 파일을 갖는 것이 좋습니다.
📂 shared/ui/
├── button/
│ └── index.js // Button의 공개 API
└── text-field/
└── index.js // TextField의 공개 API
이렇게 하면 필요한 컴포넌트만 정확하게 지정하여 가져올 수 있어
번들러가 트리 셰이킹을 효율적으로 수행할 수 있습니다.
<// 필요한 컴포넌트만 직접 가져오기
import { Button } from 'shared/ui/button';