카테고리 없음

FSD 아키텍처 정독하기 1편

harubanbo 2026. 3. 28. 17:34

현재 프론트 아키텍처 중 얘기가 많이 나오는 FSD 아키텍처에 대해 숙지할 필요가 있어서 FSD 공식문서를 읽고 두 번에 걸쳐서 내용을 정리할 예정이며 본 포스팅은 FSD 아키텍처의 기본적인 개념에 대해 정리하고 두 번째에서 FSD를 쓰면 무엇이 좋은지 주의할 점은 없는지 좀 더 세부적인 내용을 기록할 예정이다.

1. 개요

FSD (Feature-Sliced Design) 는 프론트엔드 애플리케이션 코드를 구조화하기 위한 아키텍처 방법론이다. 이 방법론은 요구사항이 바뀌어도 코드 구조가 무너지지 않고, 새 기능을 쉽게 추가할 수 있는 프로젝트를 만드는 것으로 FSD가 얼마나 많은 책임을 가지는지, 다른 모듈에 얼마나 의존하는지에 따라 계층화한다.

2. 당장 적용할 필요가 있는가?

단순 라이브러리보다 웹, 모바일, 데스크톱 등 프론트엔드 애플리케이션을 만드는 프로젝트에 어울리는 구조인데 어떤 언어나 프로젝트 환경에라도 적용이 가능하다. 다만 현재 프로젝트 구조에 특별한 문제가 없다면 굳이 바꿀 필요는 없으며 모든 프로젝트에 맞는 아키텍처도 아니다. 필요하다면 기존 프로젝트 구조에서 점진적으로 마이그레이션 하는 방법도있다.

  • 프로젝트가 커지면서 구조가 복잡해지고 유지보수 속도가 느려지는 경우
  • 기존 폴더 구조를 이해하기 힘든 경우

3. FSD의 기본 구조

FSD 구조는 크게 레이어(Layer), 슬라이스(Slice), 세그먼트(Segment) 레벨로 구분된다.

레이어는 FSD 프로젝트의 표준 최상위 폴더로 아래와 같은 폴더 구조를 가진다.

  1. App 폴더 - Routing, EntryPoint, Global Styles, Provider등 앱을 실행하는 모든 요소가 여기에 모인다. 주로 앱 전체에 적용되는 설정과 관련된 항목들이 포함
  2. Pages 폴더 - 라우팅 기준으로 구성된 주요 화면단위의 페이지를 의미(어떤 widget, feature, entity를 조합할지 정의하는 레이어)
  3. Widgets 폴더 - 크고 독립적으로 동작하는 UI 구성 단위, 일반적으로 하나의 완결된 화면 기능을 제공
  4. Features 폴더 - 사용자에게 비즈니스 가치를 제공하는 액션을 구현한 재사용 가능한 제품 기능 단위
  5. Entities 폴더 - 프로젝트가 다루는 비즈니스 Entity
  6. Shared 폴더 - 모든 Layer에서 재사용되는 코드(라이브러리, 유틸리티 등)

3-1. 레이어

레이어는 FSD에서 코드를 나눌 때 사용하는 가장 큰 구분 단위다. 코드를 나눌 때는 각 부분이 어떤 역할을 맡는지, 다른 코드에 얼마나 의존하는지를 기준으로 한다.

총 6개의 레이어가 있으며 아래로 내려갈수록 담당하는 기능과 의존성이 줄어드는 순서다 (담당 작업의 규모가 줄어든다는 의미) 모든 레이어를 프로젝트에 사용하는 것은 아니고 필요한 레이어만 골라서 쓴다. 그리고 새로운 레이어를 직접 정의해서 사용하는 것은 권장하지 않는다. 각 레이어의 역할이 이미 표준으로 충분히 정의되어있기 때문이다. 추가로 상위 레이어는 하위 레이어를 참조할 수 있지만 하위 레이어가 상위 레이어를 참조하는 것은 불가능하다. (pages는 features나 entities의 모듈을 참조할 수 있지만 features가 pages를 참조 불가능)

각 레이어마다 각자 여러 개의 슬라이스(서로 밀접하게 연관된 모듈 묶음)로 구성된다. 그리고 슬라이스들 사이의 연결은 레이어 단방향 import 규칙을 통해 제한한다. 하나의 슬라이스 안에서 작성된 코드는 위에서 얘기한 것처럼 자신이 속한 레이어보다 아래 레이어에 있는 다른 슬라이스만 단방향으로 import 가능하다.

예를 들어서 📁 ~/features/aaa/api/request.ts 는 다음의 경우에만 다른 slice를 import할 수 있다.

  • 같은 레이어의 📁 ~/features/bbb 는 import 불가능 (같은 레이어의 다른 슬라이스를 의미)
    • 같은 레이어에서 상호 참조가 필요한 경우에는 상위 레이어에서 조합하거나 예외적으로 @x 를 사용한 cross-import를 사용한다.
  • 하위 레이어의 📁 ~/entities, 📁 ~/shared 등 내부 슬라이스 요소들은 import 가능
  • 같은 slice의 📁 ~/features/aaa/lib/cache.ts 는 import 가능

app과 shared는 레이어이면서 동시에 하나의 큰 슬라이스처럼 동작한다. 그래서 내부에는 바로 세그먼트가 위치한다. 그래서 app과 shared 레이어 내부에서 세그먼트끼리는 자유롭게 import 가능하다. shared는 비즈니스 도메인이 없기 때문이고, app은 모든 도메인을 묶는 상위 조정자 역할을 하기 때문이다.


shared 레이어는 여러 곳에서 사용하는 UI 요소나 라이브러리 등 도구가 위치하는 곳이다. App과 동일하게 슬라이스 없이 세그먼트만 존재하는데 세그먼트의 예시는 보통 아래와 같다.

  • api 폴더 - API 클라이언트나 공통 백엔드 요청함수 등 API 호출에 대한 공통적으로 적용되어야 하는 래퍼나 공통 트리거 함수 등등
  • ui 폴더 - 공통 UI 컴포넌트가 위치하며 비즈니스 로직이 없는 순수한 버튼, 모달, 폼 입력 등의 컴포넌트가 위치한다. 단, UI 자체 로직을 포함하는 컴포넌트는 허용된다.
    • 비즈니스 로직이란 앱이 무엇을 하는가에 대한 로직으로 API 호출이나 도메인 데이터 처리, 인증권한체크, 비즈니스 규칙 적용 등의 내용을 의미
    • UI자체 로직이라는 것은 열림 닫힘 상태, 포커스 관리, 키보드 네비게이션, 애니메이션, 입력값 필터링 등의 단순 로직을 의미
  • lib 폴더 - 단순히 utils나 helpers를 모아두는 폴더를 말하는 게 아니며 lib/datetime등 폴더명이 명확하게 하나의 주제에 대해 목적이 무엇인지 드러나야 한다.
  • config 폴더 - 환경변수 관리 등
  • routes 폴더 - 라우팅 기본요소 및 라우트 상수 등
  • i18n 폴더 - 언어 지원에 대한 구성

가끔가다 앱 전체에 걸쳐서 동일하게 사용되어야 하는 로직이 존재한다. FSD 구조에서 슬라이스는 격리되도록 설계되었기 때문에 코드 재사용은 더 낮은 계층인 shared에 의해 촉진된다. shared 계층은 계층과 슬라이스의 하이브리드 느낌이다. 일반적으로 shared 코드는 미리 계획되지 않고 개발 중에 추출될 여지가 크다. 실제로 어떤 코드 부분이 공유되는지는 개발 중에 명확해지기 때문인데 어떤 종류의 코드가 자연스럽게 shared에 속하는지 알아두는 것은 도움이 될 수 있다.

세그먼트의 이름은 이 폴더가 무엇을 하는지 목적이 드러나는 이름이어야 한다. 그냥 components, hooks, types처럼 모호한 이름은 지양한다. (상황에 따라 다르지만 이런 이름을 쓰는 경우도 있으나 쓰더라도 이 폴더 안에 무엇이 있는지 바로 인지가 가능한 것이 좋다) 가급적 세그먼트에는 README를 통해 역할과 범위를 문서화하는 것을 권장한다.


entities 레이어는 프로젝트에서 다루는 핵심 비즈니스 개념을 표현한다. 대부분의 경우에는 실제 도메인 용어와 일치한다. 각 엔티티 슬라이스의 세그먼트는 다음과 같이 구성될 수 있다.

  • model 폴더 - 데이터 상태, 도메인 로직, 검증 스키마
  • api 폴더 - 해당 엔티티와 관련된 API 요청
  • ui 폴더 - 엔티티의 시각적 표현으로 완성된 큰 UI 블록이 아니어도 됨, 여러 페이지에서 재사용 가능한 형태로 설계하고 비즈니스 로직은 가능하면 props/slot으로 외부에서 주입하는 방식 권장

features 레이어는 사용자가 애플리케이션에서 수행하는 주요 기능이 들어간다. 보통 하나 이상의 엔티티와 연관되어 동작하는데 다음을 고려해서 구성한다.

  • 모든 동작을 무조건 feature로 만들 필요는 없다. feature가 많아지면 중요한 기능이 어디 있는지 찾기 어려워질 수 있다.
  • 여러 페이지에서 재사용되는 기능일 때 feature로 추출하는 걸 고려한다.
    • 예를 들어서 여러 종류의 에디터에서 동일한 댓글 기능을 사용한다면 comments라는 명칭으로 feature를 만들 수 있다.

feature의 구성은 보통은 다음과 같다

  • ui 폴더 - 상호작용 UI(폼, 검색 바 등)
  • api 폴더 - 해당 기능과 직접 관련된 API 요청
  • model 폴더 - 검증 로직, 내부 상태 관리
  • config 폴더 - feature flag 등 기능별 설정…

pages와 features 레이어만 훑어봐도 이 앱이 어떤 기능을 제공하는지 대략 이해할 수 있도록 구성하는게 좋다.


widgets 레이어는 독립적으로 동작하는 비교적 큰 UI 블록을 두는 곳이다. 여러 페이지에서 재사용되거나 한 페이지에서 큰 섹션 단위로 나누어지는 UI 블록이 있을 때 유용하다. 만약 재사용되지 않고 특정 페이지의 핵심 콘텐츠에만 쓰인다면 굳이 위젯으로 분리하지 말고 페이지 레이어 내부에 두는 것이 좋다.

nested routing 환경에서는 위젯이 페이지와 비슷한 역할을 할 수 있다. 예를 들어서 데이터 로딩, 로딩 상태 표시, 에러처리 등을 모두 포함하는 하나의 라우터 단위 UI 블록으로 동작할 수 있다.

pages 레이어는 웹,앱에서는 보이는 화면(screen) 또는 액티비티(activity)에 해당한다.

일반적으로 페이지1개 - 슬라이스1개 구조를 많이 사용하지만 구조가 유사한 페이지들은 하나의 슬라이스로 묶는 것도 가능하다.

  • slice group? - 슬라이스가 많아지면 평탄하게 나열하는 것만으로는 관리가 여려워지므로 관련된 슬라이스들을 폴더로 묶어서 정리할 수도 있다. pages 레이어에만 해당되는 게 아니라 다른 레이어의 슬라이스에도 동일하게 적용될 수 있는 개념이다. 단, 단순히 정리 목적일 뿐이라서 그룹 자체가 슬라이스처럼 index.ts같은 public API를 가지면 안되고 FSD의 import 규칙(단방향, 같은 레이어의 슬라이스간 import 금지)은 그대로 적용된다.

      📂 pages/
        📂 home/           # 홈 화면
        📂 profile/        # 프로필 화면
        📂 settings/       # 설정 화면
        📂 product-detail/ # 상품 상세 화면
    
      📂 pages/
        📂 auth/           # 로그인 + 회원가입 (구조가 비슷해서 그룹으로 묶음)
          📂 ui/
            📄 LoginPage.tsx
            📄 RegisterPage.tsx

코드를 찾기만 쉽다면 페이지의 슬라이스의 갯수에 특별한 제한은 없으며 재사용되지 않는 UI는 그대로 페이지 내부에 두면 된다.(다른 페이지에서 재사용하지 않는 컴포넌트는 굳이 widgets/shared/로 분리할 필요 없이 페이지 내부에 두면 된다)

페이지 레이어에는 보통 전용 모델이 없으며 필요한 경우 간단한 상태만 컴포넌트 내부에서 관리한다.

  • ui 폴더 - 페이지 UI, 로딩상태, 에러상태처리
  • api 폴더 - 페이지에서 사용하는 데이터 페칭, 변경 요청

app 레이어는 앱 전역에서 동작하는 환경설정과 공용 로직을 관리하는 앱 전체를 실행시킬 때 영향을 주는 설정과 초기화를 담당하는 레이어다.

예를 들어서 라우터 설정, 전역 상태관리(store설정) , 글로벌 스타일 앱 진입점 설정 등과 같이 앱 전체에 영향을 주는 코드가 위치한다. shared와 마찬가지로 슬라이스 없이 세그먼트로만 구성된다.

대표적인 세그먼트 예시

📂 app/
├── 📁 providers/      # React Query, Redux, Theme Provider 등
├── 📁 routes/         # 라우팅 설정
├── 📁 styles/         # 전역 CSS, CSS 변수
├── 📁 config/         # 환경 설정, 앱 설정
└── 📁 types/          # 전역 타입 선언
└── 📁 store/
└── 📁 entrypoint/

앱이 어떻게 구성이 되는지를 정의하므로 비즈니스 로직이 없고 순수한 인프라적인 코드가 들어간다.