FSD 알아보기

milkbottle·2025년 2월 19일

Next.js

목록 보기
4/5
post-thumbnail

FSD

프론트엔드인데 굳이 아키텍처까지 따져가며 개발해야한다고..?

그렇다. 프론트엔드 개발을 하게 되면 사실 수많은 비즈니스 로직, 그리고 이벤트 핸들러, 순수 함수 등 다양한 함수들을 구현해야한다.

심지어 이런 로직만 다루는 것이 아니라 컴포넌트, 페이지 까지 구조화 하여 보여주어야한다.

이런 고민을 해본 사람들에게 FSD(Feature Sliced Design)을 해보는 것을 추천한다.

기존 Next.js의 흔한 아키텍처

우선 FSD를 알아보기 전에 기존에 자주 쓰던 프론트엔드 아키텍처를 보자.

src
├⎯ app(pages) 			// 앱(페이지) 라우팅
├⎯ components			// 컴포넌트
├⎯ containers			// 컴포넌트를 모아 만든 레이아웃급 컴포넌트
├⎯ hooks				// 커스텀 훅
├⎯ utils				// 순수함수(ex. 2022.12.10을 GMT로 바꾸기 등)
└⎯ types				// 각종 타입들..

우리가 아는 구조는 역할별로 눈에 보일 컴포넌트, 비즈니스 로직을 다룰 커스텀훅, 유틸함수 등으로 구분하여 사용한다.

하지만 나중에는 하나의 폴더가 정리가 되지 않고 엄청 비대해지게 된다.

또한, UI와 관련된 파일들은 자연스레 라우트에 매핑되도록 로그인페이지, 마이페이지, 서치페이지 로 나누게 되고 그안에 관련된 컴포넌트를 때려박게 된다.
그 결과가 위의 사진이다.

hooks, utils, types 같은 경우는 그러면 components 와 동일하게 라우트에 매핑해서 서브 폴더를 만들어줘야할까? 라는 고민을 하게된다.

그러면 결국 공유하는 컴포넌트나 로직은 common에 다 넣어버리게 되고 결국 common이 비대해지는 더러운 아키텍처가 된다.

메인페이지에 헤더바가 있고.. 마이페이지에도 헤더바가 있으니 common에 넣자!

썸네일 컴포넌트는 메인페이지, 검색페이지에도 있으니 common에 넣자!


만일 처음 프로젝트를 인수인계받는 신입이나 인턴이 와서 이 코드를 보면 뭐라고 생각할까?
아하! 페이지 2개이상 걸치는 컴포넌트나 로직은 전부 common에 넣으면 되는거구나!

장점

  • 러닝커브가 쉽다
  • 역할별로 분류되기에 최상단에서 어떤 파일들끼리 묶여있는지 이해하기 쉽다

단점

  • 확장성이 낮다
    왜냐하면 처음 폴더가 app, components, hooks, utils, types 에서 더 늘어나지 않고 그 하위레벨이 계속 추가되고 사라지기 때문에 최상단부터 프로젝트의 구조를 알 수 없기 때문이다.

  • 프로젝트의 구조를 파악하기 어렵다
    어떠한 프로젝트든 간에 app, components, hooks, utils, types 이런식으로 있기에 어떤 기능을 제공하는지 알 수가 없다.

FSD의 탄생

그래서 코드의 역할이 아닌, 기획의 기능(도메인)에 기반한 아키텍처가 나왔다.
Feature(도메인 = 기능 = slieces, 기획명세로 이해하면 쉽다.) 별로 Sliced를 하여 Design을 하는 FSD 아키텍처인 것이다.

공식문서에 따르면 Layers, Slices, Segments 로 나누고 있다.
그러니까 폴더구조는 다음과 같다는 말이다. (process는 안쓰므로 제외)

src
├⎯ app
├⎯ pages
├⎯ widgets
├⎯ features
├⎯ entities
	├⎯ user
    ├⎯ post
    └⎯ comment
└⎯ shared

Layers

그러면 첫번째는 무엇일까? 기능의 규모를 뜻한다.
말이 어렵지만 벨로그 개인 페이지로 예를 들어보겠다.

여기서 UI를 의미있는 단위로 나누면 다음과같다.

빨간색 네모의 영역이 pages에 들어갈 수 있는 크기이다.
파란색 네모의 영역이 widgets로 그 다음 작은 단위로 들어가는 크기이다.

features, entities는 UI의 영역이라기보다는 비즈니스 로직의 의미가 크다.
예를들면 상단바인 경우 (로고/알림버튼/검색버튼/새글작성버튼/프로필버튼)이 있으면
이 5가지를 분리해 features에 넣어 기능을 구현한다.

페이지안에는 상단바, 게시글, 검색바, 해당 벨로그주인장의 정보 등의 컴포넌트가 들어가야한다.

즉, pages는 widgets를 import 할 수 있어야한다. 하지만 widgets는 pages를 import 할 수 없다.
순환참조를 방지할 수 있으며, 하위 레벨로 갈수록 독립성이 높아저 재사용성이 높아지게 된다.

Slices

두번째 폴더 depth인 Slices(도메인이라고도 칭함) 영역은 공식문서에 따르면 app, shared 레이어에선 사용하지 않는다고 한다.

그 이유는 설명되어 있지않지만, 잠시라도 생각해보면 당연한 이치이다.

app은 전체적인 프로젝트의 전반적인 것들(app.tsx 같은 것이나 project initializing을 돕는 코드)이 들어가기에 특정 도메인에 의존성을 지닐 수 없다.
또한 shared는 최하위 Layer이므로 특정 도메인에 속할 수 없는 것이다. app에서도 shared를 참조할 수 있고, pages, widgets, features에서도 shared 를 참조할 수 있는데, shared가 도메인 의존성을 지니게 되면 어불성설이다.

예를들어 src/features/posts(features 레이어, posts 슬라이스)에서src/shared/comments(shared 레이어, comments 슬라이스)를 참조하는 것은 말이안 되기에 shared는 도메인 의존성을 지니지 않아 Slices 가지지 않는다.

Segments

Segments는 우리가 흔히 Next.js 아키텍처에서 사용하던 코드의 역할분류를 뜻한다.
hooks, components, utils 등이 되는 것이다.

앞서 shared layer는 slices를 지니지 않으므로, 공통으로 사용되는 코드들이 들어간다.
(ex. 백엔드 요청에 필요한 엔드포인트들을 묶은 Record<string, string> 타입의 const EndPoint)

초기에 연습하기 좋은 구조

"Layers 가 굳이 6개로 구분이 되어야할까?"라는 생각을 했다. 너무 많은 레이어는 개발보다 고민시간을 더 크게 만들 뿐이다.

그래서 Slices, Segments의 개념은 유지한체 Layers 를 app, widgets, domain, shared로 3가지로 나누었다.

src
├⎯ app(pages)          // 앱(페이지) 라우팅
├⎯ widgets             // 컴포넌트들이 모여 레이아웃급이 되는 규모의 컴포넌트
│   ├⎯ header         // 상단바 관련
│   │   ├⎯ ui 		   // 헤더 내부 컴포넌트들, 헤더가 여러가지라면 안에 여러가지가 들어갈 수 있음
│   │   └⎯ hooks	   // 헤더 내부에 동작할 비즈니스 로직 커스텀훅
│   ├⎯ search-bar     // 검색바 관련
│   │   ├⎯ ui		   // 검색바 관련 컴포넌트
│   │   ├⎯ types	  // 검색과 관련된 타입(검색 필터의 enum, 검색어 api 요청, 응답을 위한 interface)
│   │   └⎯ hooks
│   └⎯ post-list      // 게시글 리스트
│       ├⎯ ui
│       └⎯ hooks      // 게시글
├⎯ domain
│   ├⎯ search         // 검색과 관련된 코드
│   │   ├⎯ api        // 검색 관련 API 호출
│   │   ├⎯ hooks      // 검색 관련 커스텀 훅
│   │   ├⎯ types      // 검색 도메인 타입
│   │   └⎯ utils      // 검색 관련 유틸 함수
│   ├⎯ posts          // 글과 관련된 코드
│   └⎯ comments       // 댓글과 관련된 코드
└⎯ shared
    ├⎯ ui             // 팝오버, 버튼, 모달, 가상화 리스트 등 도메인 의존성이 없는 ui
    ├⎯ api            // api baseURL, react-query를 쓴다면 QueryClientProvider, axiosClient 등
    └⎯ types          // 전역으로 사용할 타입
	

예를 들면 아래와 같이 코드가 있을 수 있다.

widgets/header
├⎯ components
│   ├⎯ Logo.tsx
│   ├⎯ NavMenu.tsx
│   ├⎯ SearchButton.tsx
│   └⎯ UserProfile.tsx
└⎯ index.tsx          // 헤더 컴포넌트들을 조합

domain/search
├⎯ api
│   ├⎯ searchService.ts    // 검색 API 호출 함수들
│   └⎯ types.ts           // API 요청/응답 타입
├⎯ hooks
│   ├⎯ useSearch.ts       // 검색 로직 훅
│   └⎯ useRecentSearch.ts // 최근 검색어 관리 훅
├⎯ types
│   └⎯ search.ts          // 검색 도메인 타입 정의
└⎯ utils
    ├⎯ searchFilter.ts    // 검색 필터링 유틸
    └⎯ searchHistory.ts   // 검색 기록 관리 유틸

장점

  • 위젯 레벨의 명확한 분리
    - 각 위젯은 자체적인 컴포넌트들을 관리
    - 위젯 내부 구조가 일관성 있게 유지됨

  • 도메인 단위의 체계적인 코드 관리
    - API, 훅, 타입, 유틸 등이 도메인별로 응집도 있게 구성
    - 각 도메인의 비즈니스 로직이 독립적으로 관리됨
    - 명확한 책임 분리
    widgets: UI 조합에 집중
    domain: 비즈니스 로직에 집중
    shared: 공통 요소 관리

이렇게 구조화하면 코드의 위치를 예측하기 쉽고, 새로운 기능을 추가할 때도 어디에 코드를 작성해야 할지 명확해진다.

참고

0개의 댓글