FrontEnd - FSD 아키텍처

Hunjin·2025년 11월 13일

SOPT

목록 보기
6/6
post-thumbnail

FSD(Feature-Sliced Design)은 복잡한 프론트엔드 애플리케이션의 구조를 체계적으로 조직화하기 위해 고안된 아키텍처 방법론입니다.

FSD를 구현하는 간단한 예시를 알아보겠습니다.

- 📁 app
- 📁 pages
- 📁 shared

이러한 최상위 폴더를 레이어라고 부릅니다.

- 📂 app
	- 📁 routes
	- 📁 analytics
- 📂 pages
	- 📁 home
	- 📂 article-reader
		- 📁 ui
		- 📁 api
	- 📁 settings
- 📂 shared
	- 📁 ui
	- 📁 api

폴더 안에 있는 폴더를 슬라이스라고 합니다.
슬라이스는 레이어를 도메인(페이지)별로 나눕니다.

개념


FSD 구조는 크게 레이어, 슬라이스, 세그먼트 총 3가지의 계층 구조로 나눌 수 있습니다.

레이어

FSD 프로젝트의 레이어는 표준화되어 있습니다.

  • app : 애플리케이션 초기화, 전역 스타일, 라우팅 정의
    -> 가장 상위 계층으로 다른 어떤 계층도 사용할 수 없음
  • processes : 사용 안 함
  • pages : 특정 라우트(URL, 페이지)에 해당하는 페이지 컴포넌트
    -> Widgets or Feature를 조합하여 페이지 구성
  • widgets : 페이지를 구성하는 독립적인 UI 블록(헤더, 푸터 등)
    -> Features와 Entities를 조합하여 UI를 구성
  • Features : 사용자와의 상호작용 및 단일 기능 로직
    -> Entities를 사용함
  • Entities : 데이터 구조, 상태관리, API 호출 등 도메인 관련 로직(CRUD)
  • Shared : 전역에서 사용되는 재사용 가능한 코드

FSD의 슬라이스 원칙

각 계층은 다시 슬라이스 또는 세그먼트의 단위로 수직 분할됩니다.
슬라이스는 특정 도메인이나 기능을 나타내며, 그 안에 해당 기능과 관련된 모든 코드를 모아놓습니다.

슬라이스는 애플리케이션의 비즈니스 도메인에 따라 직접 결정되므로 표준화되지 않습니다. 예를 들어 사진 갤러리에는 photo, effects와 같은 슬라이스가 있을 수 있습니다.

  • 수직 분할 : 각 계층은 슬라이스라는 단위로 수직 분할
  • 도메인 / 기능 집중 : 슬라이스는 특정 도메인이나 기능을 나타내며, 해당 기능과 관련된 모든 코드를 모아둠
  • 비표준화 및 자율 결정 : 슬라이스는 애플리케이션의 비즈니스 도메인에 따라 직접 결정되므로, 이름이나 개수가 표준화되어 있지 않고 자유롭게 정의 가능
  • 높은 응집도 & 낮은 결합도 : 이상적인 슬라이스는 같은 레이어의 다른 슬라이스와 독립적이며, 해당 목표와 관련된 코드를 최대한 많이 포함하여 높은 응집도와 낮은 결합도를 보장합니다.

결함이 없고 응집력이 높음

src/
└── entities/
    └── article/                <-- 'article' 이라는 슬라이스
        ├── model/              // 아티클의 상태 관리 (Redux/Zustand 등)
        ├── ui/                 // 아티클을 보여주는 컴포넌트
        └── api/                // 아티클 관련 API 요청 로직

FSD의 가장 큰 장점은 모든 코드가 정의된 위치에 존재하며
엄격한 의존성 규칙을 따르기 위해 코드가 얽히지 않고 독립적으로 유지될 수 있습니다.

FSD 슬라이스 예시

  1. 쇼핑몰 A : 기능 증심 슬라이스

src/
└── features/
    ├── addToCart/                  <-- '장바구니에 담기' 기능 슬라이스
    │   ├── model/                  // 장바구니 추가 관련 상태 및 로직
    │   └── ui/                     // '장바구니에 담기' 버튼 컴포넌트
    ├── productReview/              <-- '상품 리뷰' 기능 슬라이스
    │   ├── model/                  // 리뷰 작성, 조회 관련 로직
    │   └── ui/                     // 리뷰 목록 및 작성 폼 컴포넌트
    └── productDetails/             <-- '상품 상세' 화면 슬라이스
        ├── model/                  // 상세 페이지 데이터 로직
        └── ui/                     // 상세 페이지 레이아웃 및 컴포넌트
  1. 쇼핑몰 B : 데이터 / 도메인 중심 슬라이스 정의

src/
└── entities/
    ├── products/                   <-- '상품 데이터' 도메인 슬라이스
    │   ├── model/                  // 상품 데이터 CRUD 로직 (API 포함)
    │   └── ui/                     // 상품 카드/목록 컴포넌트 (UI)
    ├── cart/                       <-- '장바구니 데이터' 도메인 슬라이스
    │   ├── model/                  // 장바구니 상태, 수량 변경 로직
    │   └── ui/                     // 장바구니 목록 컴포넌트
    └── orders/                     <-- '주문 데이터' 도메인 슬라이스
        ├── model/                  // 주문 생성, 조회 로직
        └── ui/                     // 주문 내역 컴포넌트

두 쇼핑몰 모두 FSD 원칙을 따르지만 핵심 기능을 다루는 방식이 다릅니다.
쇼핑몰 A는 addToCart라는 슬라이스를 만들어 장바구니에 담는 행위에 집중
쇼핑몰 B는 cart라는 슬라이스를 만들어 장바구니 데이터 및 관련 UI에 집중합니다.

슬라이스 API 규칙

슬라이스 내부에서 코드는 원하는 방식으로 구성할 수 있습니다.

모든 슬라이스에는 공개 API만 참조할 수 있습니다.

공개 API란?

슬라이스 외부의 다른 코드가 이 슬라이스와 상호작용할 수 있도록 접점을 정의하는 파일입니다.
일반적으로 슬라이스 폴더의 최상단에 있는 index.ts 파일이 이 역할을 수행합니다.

왜 필요하지?

공개 API를 사용하는 주된 이유는 낮은 결합도와 쉬운 유지보수를 달성하기 위함입니다.

  • 캡슐화 강제 : 슬라이스 내부 구현 로직을 외부에 숨겨 외부에서 내부 구현에 직접 의존하는 것을 방지할 수 있습니다.
  • 리팩토링의 용이함 : 슬라이스 내부 코드를 대대적으로 수정해도, 외부에 공개된 API의 형태만 유지한다면 해당 슬라이스를 사용하는 외부 코드에는 영향을 주지 않습니다.

공개 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를 정의하는 가장 일반적인 방법입니다. 쉽게 만들 수 있지만, 특정 번들러와 프레임워크에서 문제를 일으키는 것으로 알려져 있습니다.

1. 순환 가져오기 문제

두 개 이상의 파일이 서로 간접적 또는 직접적으로 가져오는 현상을 말합니다.

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

순환 가져오기 예시 :

  • HomePage (자식) -> index.js (부모)
  • index.js (부모) -> HomePage (자식)Export
    이러한 상호 참조가 순환을 만들고 문제를 발생시킵니다.

순환 가져오기 해결 원칙

  1. 동일 슬라이스 내부
  • 항상 상대적 가져오기를 사용해서 전체 경로를 명시
  • 목표 : index.js를 거치지 않고, pages/home/ui/HomePage.jsxpages/home/api/loadUserStatistics를 직접 가져오도록 합니다.
  1. 서로 다른 슬라이스 간
  • 항상 별칭(Alias)을 사용한 절대 가져오기를 사용하기(FSD 기본 원칙)
  • 목표 : 슬라이스 간 의존성이 명확해지고 유출을 방지할 수 있습니다.

2. 트리 셰이킹 문제 및 큰 번들 크기

트리 셰이킹은 모듈 번들러(Webpack, Rollup 등) 실제 사용되지 않는 코드를 최종 번들에서 제거하여 파일 크기를 줄이는 최적화 과정입니다.
인덱스 파일이 해당 슬라이스 내부의 모든 것을 export * from ... 형태로 다시 내보낼 경우, 일부 번들러는 모든 것을 사용한다고 오해하여 트리 셰이킹이 제대로 작동하지 않을 수 있습니다.

문제의 고도화

일반적인 슬라이스는 문제가 덜합니다.
하지만 공유(Share) 레이어의 shared/uishared/lib와 같이 서로 관련이 없는 다양한 모듈의 모음에서는 이 문제가 심각해질 수 있습니다.

  • 예시: shared/ui의 인덱스 파일이 Button, TextField, Carousel 등 수십 가지 컴포넌트를 모두 내보낸다고 가정해 봅시다.

만약 어떤 페이지에서 Button 하나만 가져다 써도, 번들러가 Carousel이나 Accordion의 코드와 그에 딸린 무거운 라이브러리 의존성까지 최종 번들에 포함시켜 버릴 수 있습니다.

해결책

shared/uishared/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';

참고 : https://feature-sliced.design/docs/reference/layers

profile
프론트 개발을 해보아요👨🏻‍💻

0개의 댓글