FSD를 알아보자 (1): 개념과 레이어 구조

Frontend

프론트엔드 프로젝트는 규모가 조금만 커져도 구조 문제가 빠르게 드러납니다. 처음에는 components, hooks, utils, api 정도로도 충분하지만, 기능이 늘어날수록 "이 코드는 어디에 둬야 하지?" 같은 고민이 반복됩니다.

이럴 때 도움이 되는 구조가 바로 FSD(Feature-Sliced Design) 입니다. FSD는 단순히 폴더를 나누는 규칙이 아니라, 비즈니스 기준으로 코드를 분리하고 의존성을 통제하는 아키텍처에 가깝습니다.

이 글은 FSD 시리즈 1편입니다. 먼저 개념과 구조를 짧게 정리합니다.

왜 구조가 금방 무너질까?

프론트엔드는 생각보다 많은 책임을 동시에 가집니다.

  • UI 렌더링
  • 사용자 인터랙션 처리
  • 서버 통신
  • 캐싱 및 상태 관리
  • 비즈니스 규칙 처리
  • 로딩, 에러, 접근성 처리

특히 React나 Next.js에서는 컴포넌트 단위 개발이 자연스럽다 보니, 조금만 방심하면 페이지 컴포넌트 하나가 모든 것을 떠안게 됩니다.

흔한 안티 패턴

function TodoPage() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
  const [input, setInput] = useState('');
 
  useEffect(() => {
    fetch('/api/todos')
      .then((res) => res.json())
      .then(setTodos);
  }, []);
 
  const addTodo = async () => {
    const newTodo = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ title: input }),
    }).then((res) => res.json());
 
    setTodos((prev) => [newTodo, ...prev]);
  };
 
  const visibleTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });
 
  return <div>{/* ... */}</div>;
}

이 구조의 문제는 분명합니다.

  • 데이터 조회, 생성, 필터링, 렌더링이 한 파일에 몰림
  • 기능 수정 시 항상 같은 파일을 열게 됨
  • 테스트와 재사용이 어려워짐

FSD는 이런 구조를 더 작은 책임 단위로 나누는 데 초점을 맞춥니다.

FSD 아키텍처란?

FSD는 Feature-Sliced Design의 약자입니다. 애플리케이션을 파일 종류가 아니라 **기능(feature)**과 도메인(domain) 중심으로 나누는 프론트엔드 아키텍처입니다.

핵심 목표는 다음과 같습니다.

  • 변경이 쉬운 구조 만들기: 특정 기능 수정이 다른 영역으로 퍼지지 않도록 함
  • 비즈니스 기준으로 코드 정리하기: 파일 타입이 아니라 기능과 도메인 기준으로 묶기
  • 의존성 방향 통제하기: 아무 데서나 아무 모듈이나 import 하지 못하게 설계
  • 협업 효율 높이기: 팀원이 코드를 봤을 때 수정 위치를 빠르게 찾을 수 있게 함

많은 프로젝트는 아래처럼 시작합니다.

src/
  components/
  hooks/
  utils/
  api/
  pages/

초반에는 단순하지만, 기능이 늘어나면 하나의 기능을 이해하기 위해 여러 폴더를 동시에 봐야 하는 구조가 되기 쉽습니다. FSD는 이 문제를 줄이기 위해 등장한 방식입니다.

FSD 핵심 개념

FSD를 이해할 때 중요한 개념은 세 가지입니다.

1. 레이어

애플리케이션을 관심사 수준에 따라 여러 층으로 나눕니다.

  • app: 앱 전체 설정, Provider, 라우팅
  • pages: 라우트 단위 화면
  • widgets: 여러 feature와 entity를 조합한 화면 블록
  • features: 사용자 액션 중심 기능
  • entities: 도메인 모델
  • shared: 공통 UI, 유틸, 설정

2. 슬라이스

레이어 안에서 도메인이나 기능 단위로 나눈 묶음입니다.

예를 들면:

  • entities/todo
  • features/add-todo
  • widgets/todo-list

처럼 나눌 수 있습니다.

3. Public API

각 슬라이스는 내부 구현을 숨기고, 외부에는 필요한 것만 노출합니다. 보통 index.ts를 통해 공개 API를 만듭니다.

이렇게 하면:

  • 내부 파일 구조를 바꿔도 외부 영향이 줄어들고
  • import 경로가 단순해지며
  • 슬라이스 경계가 더 명확해집니다

FSD 레이어 구조 이해하기

FSD의 레이어는 "기술 기준"이 아니라 "비즈니스와 책임 기준"으로 이해하는 것이 좋습니다.

shared

특정 도메인을 모르는 공통 코드입니다.

  • 공용 UI 버튼, 인풋, 모달
  • API 클라이언트
  • 날짜 포맷 함수
  • 공통 타입 유틸
  • 상수, 환경 설정

entities

도메인 모델을 표현합니다. todo, user, comment 같은 명사 중심 레이어입니다.

예를 들면:

  • Todo 타입
  • Todo 조회 API
  • TodoItem UI
  • Todo와 관련된 순수 비즈니스 헬퍼

features

사용자 액션이나 유즈케이스를 표현합니다. 명사가 아니라 동사 중심 레이어입니다.

  • Todo 추가
  • Todo 완료 토글
  • Todo 삭제
  • Todo 필터 변경

widgets

화면에서 의미 있는 큰 블록입니다. 여러 entity와 feature를 조합합니다.

  • Todo 리스트 영역
  • 상단 툴바
  • 통계 패널

pages

라우트 단위 화면을 구성합니다. 페이지는 조립 역할에 집중하는 편이 좋습니다.

app

애플리케이션의 진입점입니다.

  • Router
  • QueryClientProvider
  • ThemeProvider
  • 전역 스타일

레이어 구조 다이어그램

flowchart TD
    appLayer[app] --> pagesLayer[pages]
    pagesLayer --> widgetsLayer[widgets]
    widgetsLayer --> featuresLayer[features]
    featuresLayer --> entitiesLayer[entities]
    entitiesLayer --> sharedLayer[shared]

위에서 아래로 갈수록 더 범용적이고 재사용 가능한 코드가 위치합니다.

FSD 의존성 규칙

FSD는 보통 아래 방향으로만 의존하도록 설계합니다.

app
  -> pages
    -> widgets
      -> features
        -> entities
          -> shared

즉, 상위 레이어는 하위 레이어를 사용할 수 있지만 하위 레이어가 상위 레이어를 참조하면 안 됩니다.

예를 들면:

  • features/add-todoentities/todo를 import 할 수 있음
  • entities/todofeatures/add-todo를 import 하면 안 됨
  • shared/ui/buttonentities/todo를 import 하면 안 됨

잘못된 의존성 예시

// entities/todo/model/todo.ts
import { useAddTodo } from '@/features/add-todo';

entityfeature를 아는 순간 레이어 규칙이 무너집니다. Todo라는 도메인 모델은 "추가 기능 UI가 어떻게 동작하는지"를 알 필요가 없습니다.

FSD를 적용하면 무엇이 좋아질까?

1. 변경 범위가 줄어든다

예를 들어 "Todo 삭제 시 확인 모달을 띄우자"는 요구사항이 들어오면, 주로 features/delete-todo만 보면 됩니다.

2. 협업이 쉬워진다

한 명은 features/add-todo, 다른 한 명은 widgets/todo-summary, 또 다른 한 명은 entities/todo/api를 맡아 작업할 수 있습니다.

3. 테스트 포인트가 명확해진다

  • entities: 타입, 도메인 헬퍼, 순수 함수 테스트
  • features: 사용자 액션과 mutation 훅 테스트
  • widgets: 조합 결과와 렌더링 테스트

4. 재사용성이 높아진다

같은 entity를 사용하면서도 화면마다 다른 widget 구성을 만들 수 있습니다.

모든 프로젝트에 FSD가 필요할까?

그렇지는 않습니다. 작은 프로젝트에서는 오히려 과할 수 있습니다.

다음과 같은 경우에 특히 잘 맞습니다.

  • 화면과 기능 수가 빠르게 늘어나는 서비스
  • 여러 명이 동시에 프론트엔드를 개발하는 팀
  • 도메인과 사용자 액션이 복잡한 프로젝트
  • 유지보수 기간이 긴 프로덕트

반대로 아래 경우에는 단순한 구조가 더 나을 수 있습니다.

  • 페이지 수가 적은 마케팅 사이트
  • 실험용 MVP
  • 짧은 기간에 빠르게 폐기될 프로젝트

즉, FSD는 "무조건 정답"이 아니라 복잡도를 관리하는 도구입니다.

다음 글에서 이어서 볼 내용

이 글에서는 FSD의 개념과 구조만 다뤘습니다. 실제로 감이 잘 오지 않는다면 다음 글을 이어서 보는 편이 좋습니다.

결론

FSD는 단순한 폴더링 규칙이 아니라, 프론트엔드 코드베이스를 비즈니스 중심으로 조직하는 방법입니다.

중요한 것은 처음부터 완벽한 구조를 만드는 것이 아닙니다. 작은 화면 하나부터 시작해 "이 코드는 무엇을 표현하는가?", **"이 코드는 사용자가 무엇을 하게 만드는가?"**를 기준으로 분리해 나가는 것이 핵심입니다.

프로젝트 규모가 커지고 협업 인원이 늘어날수록, FSD는 단순히 보기 좋은 구조가 아니라 변경 비용을 낮추는 실전 아키텍처가 됩니다.