abonglog logoabonglog

경험에 의거한 FSD (Feature Sliced Design) 구조 완전 공략 의 썸네일

경험에 의거한 FSD (Feature Sliced Design) 구조 완전 공략

웹 브라우저 지식
프로필 이미지
yonghyeun

FSD 자료구조를 이용한 사이드 프로젝트를 3개 진행했었는데 그 중 하나는 완전히 망했고 나머지 두 개는 꽤나 잘 쓰고 있다.

지금 하고 있는 사이드 프로젝트에서 프론트엔드 파트의 구조를 다시 살펴 보기 전 FSD 공식 문서 를 경험에 의거해서 하나씩 천천히 다시 살펴보도록 하자.

이전에 봤던 때와 다르게 사이트 도메인과 내용이 조금 변경되었더라 !

FSD (Feature Sliced Design) 요약

FSD를 가장 잘 표현하는 이미지FSD를 가장 잘 표현하는 이미지

FSD 아키텍쳐는 7가지 레이어와 레이어 별 N 개의 슬라이스, 슬라이스 별 M 개의 세그먼트로 나뉘는 프론트엔드 아키텍쳐이다.

이 때 FSD 는 개별적인 의미를 가지는 레이어들 별로 계층적 구조를 가지며 하위 레이어는 상위 레이어의 모듈을 import 할 수 없다는 규칙을 가진다.

또 레이어 내부 슬라이스들은 다른 슬라이스를 import 하지 못한다는 규칙을 가지는 것이 특징이다.

모든 폴더 구조들은 Layer -> Slice -> Segment 순으로 관리된다. 예를 들어 entities/users/ui 처럼 말이다.

공식문서에선 Layer -> Slice -> Segment 순으로 설명하는데 개인적으론 역순으로 소개하는 편이 FSD 를 이해하는데 더 도움이 된다고 생각한다.

내 첫 번째 FSD 사용이 망했던 이유가 Slice 개념을 이해하지 못하고 Layer 개념만 이해한채로 사용했기 때문이다.

Segment

세그먼트는 Layer -> Slice 이후에 존재하는 모듈들의 역할에 맞춰 파일을 담는 폴더들을 칭한다.

즉 코드들을 코드 본연의 역할에 맞춰 구분 한다. 대표적인 예시들은 다음과 같다.

  • 컴포넌트들을 담는 ui
  • 컴포넌트 내부 데이터들과 관련 있는 모듈들 (타입시스템, 상태관리 스토어, 데이터 스키마) 을 담는 model
  • 실제 세계인 서버에게 데이터를 요청하는 api
  • 여러 로직들을 처리하는 함수들을 담는 lib
  • 변하지 않는 상수나 파일들을 담는 config

Slice

SliceLayer 다음에 존재하는 폴더로 각 Layer 내부에 존재하는 코드들을 의미에 맞게 묶는 역할을 하는 폴더 이다.

이 때 특징적인 것은 같은 레이어 내부의 Slice 들은 서로를 웬만해선 참조하면 안된다는 강한 규칙을 갖는다.

즉 위에서 말한 레이어별 Segment 들을 어떤 기준들로 묶어둘 것인가를 결정하는 폴더이다.

Zero coupling and high cohesionZero coupling and high cohesion
Slice 들은 다른 Slice 를 참조하지 않음으로서 같은 레이어 내부 Slice 들간 결합도를 낮추고 같은 목적들을 가진 Segment들을 Slice 내부에 모아둠으로서 응집도를 높혀 세그먼트들간 Zero coupling and high cohesion 를 가능하게 한다.

Layer

마지막 FSD 구조의 가장 첫 번째 폴더인 Layer 다.

Layer 들은 shared -> entities -> features -> widgets -> pages -> app 순으로 계층적인 구조를 가지며 각 레이어들은 하위 레이어의 모듈만 참조 가능하고 상위 레이어의 모듈은 참조하지 못하도록 한다.

이런 모듈이 의존하는 파일들의 의존성 순서를 레이어 별 계층에 따른 제약 조건을 갖게 함으로서 더욱 아키텍쳐를 스파게티처럼 꼬인 의존성을 가지게 하는 것이 아니라,예측 가능한 순차적 구조를 가지게 한다.

낮은 계층 레이어들 (shared,entities,features)의 ui 들은 추상화 수준이 높도록 설계하고 높은 계층의 레이어들 (widgets,pages)은 추상화 수준이 낮은 하위레이어들을 조립하여 완성된 하나의 컴포넌트를 만든다.

각 레이어들 별 의미를 알아보자.

Shared

Shared 는 프로젝트 전반에 사용되는 모듈들을 담는 레이어며 어떤 비즈니스와 관련된 로직도 가지지 않는다.

그렇기 때문에 해당 레이어는 Slice 없이 Segment 만을 갖는다.

예를 들어 이런 파일들이 있을 수 있다.

  • Shared/ui/Button.tsx
  • Shared/lib/useInterver.ts
  • Shared/model/useThemeStore.ts
  • Shared/config/colors.ts

특정 비즈니스와 관련되지 않으면서 프로젝트 전반에 사용 가능한 모듈들이 존재한다.

Entities

엔티티 레이어는 실제 세계를 표방하는 모든 것들을 모아둔 레이어다.

처음 공식문서에서 실제 세계를 표방한다 했을 때 처음엔 엥? 이게 무슨 이야기지? 하고 생각했었고, 해당 의미를 정확히 해석하지 못해 완전 엉터리 FSD 구조를 썼었다.

왜 이해하지 못했나 생각해보면 데이터베이스와 관련된 기초 지식이 부족했기 때문이라 생각한다.

즉 실제 세계란 프로젝트 내부에서 사용되는 모든 데이터들을 의미한다.

예를 들어 내가 중고나라 서비스를 구현하려 하고 해당 서비스의 데이터 베이스를 만들려고 한다 생각해보자.

가상의 중고나라 ERD가상의 중고나라 ERD

이 때 이 서비스에는 구매자,판매자,물품 데이터들이 필요 할 것이고 이 데이터들은 각자의 특징에 맞춰 엔티티란 이름으로 묶이고 이런 ERD 형태로 표현 가능 할 것이다.

즉 사용하고자 하는 프로젝트의 실제 세계 데이터들은 구매자, 판매자, 물품들이다.

그렇다면 Entities 레이어 내부는 이런식으로 표현 가능 할 것이다.

  • entities/customers/ui,api..
  • entities/seller/ui,api..
  • entities/stock/ui,api..

Features

Features 레이어는 외부 세계와 인터렉션이 일어나는 모든 것들을 담는 레이어다.

예를 들어 유저들이 폼을 입력하는 Form 이나 댓글을 입력하는 댓글창인 Comments 들처럼 말이다.

그렇다면 Features 레이어는 이처럼 표현 가능 할 것이다.

  • Features/Form/ui/SignupForm.tsx
  • Features/Form/model/useSignupFormStore.ts

Widgets

Widgets 레이어는 large self-sufficient blocks of UI, 즉 외부에서 데이터를 주입 하지 않아도 혼자 데이터를 렌더링 할 수 있는 거대한 블록들을 담는다.

주로 Entiites , Features 레이어에 있는 컴포넌트들을 불러와 Widgets 레이어에서 조립하여 사용한다.

여기서 잠깐!

하지만 모든 데이터들을 entities , features,widgets 레이어에 둘 필요 없다. 만약 어떤 컴포넌트가 2번 이상 재사용되지 않는다면 굳이 다른 레이어에 둘 필요 없이 해당 모듈이 사용되는 pages 레이어 내부에서 직접 선언해도 된다.

FSD 를 사용하는 가장 큰 목적은 가장 빠르게 모듈을 찾고 관리하기 위함이다. 불필요하게 레이어 내부에 모듈들이 쌓여버린다면 이는 목적에 맞지 않는다.

Pages

Pages 는 말 그대로 개별 페이지들을 의미한다.

문서에선 Pages 레이어에서 정의된 api 세그먼트 내부에 정의된 코드로 데이터를 요청하거나 전송하고 ui 세그먼트 내부에서 정의된 컴포넌트들에 데이터를 주입 하는 것이 전형적이라고 한다.

다만 내 개인적인 경험상으로는 api 세그먼트는 entities 레이어 내부에 모두 정의해두는 편이 낫더라. 이유는 추후 기술한다.

App

마지막 가장 상위 레이어인 App 레이어이다.

App 레이어 내부 모듈들은 아마 vite 라면 app.tsx 파일에서 NextJS 라면 최상위 layout.tsx 에서 import 하여 사용 할 프로젝트의 전반적인 모듈들을 담는 레이어다.

이 레이어도 Shared 레이어처럼 비즈니스 로직과 상관 없이 구성되기 때문에 Slice 를 가지지 않는다.

이것들 말고도 index.ts 를 이용한 export,import 규칙이나 slice 들간 어쩔 수 없이 의존해야 하는 경우 사용하는 @x export,import 방식들이 있다.

공식문서를 참조하자!

개인적인 FSD 경험담

프로젝트 사이즈가 작으면 FSD 가 적절하지 않다는데

종종 프로젝트 사이즈가 작으면 FSD 가 적절치 않다는 의견들이 있었다.

예전엔 공식문서에서도 그 내용이 있었던 거 같은데 지금 리뉴얼된 문서에선 잘 안보인다.

아마 그런 의견이 있던 이유는 프로젝트 사이즈가 작으면 entities 레이어 내부 슬라이스 폴더가 하나 밖에 없을 수 있어서 그랬던게 아닐까 싶긴한데

여전히 계층에 따라 Segment 들을 구분하는 개념은 계층적인 프론트엔드 구조를 만드는데 있어 큰 도움이 된다 생각한다.

확실히 스파게티처러 꼬인 복잡한 구조가 아니라 계단 형식으로 차츰차츰 올라가는 구조라 훨씬 관리하기 편했다.

api 세그먼트의 위치는 entities에 때려박는 편이 좋더라

문서에선 pages 레이어 내부에서 페이지 별 api 세그먼트에 정의된 모듈로 데이터를 요청하거나 전송하라 했으나

리액트 쿼리를 사용하는 내 입장에선 entities 레이어에서 모든 api 세그먼트들을 정의해두는 편이 낫더라.

나는 entities 레이어를 이용 할 때 무조건 데이터베이스 상의 ERD 구조에 맞춰 슬라이스들을 구성한다.

이래야 백엔드 파트와 같은 곳을 바라보고 이야기 할 수 있고 실제 비즈니스 로직에 맞춰 이동하는 데이터 흐름에 따라

프론트엔드상에선 전역 api 데이터들을 담고 있는 queryClient 의 데이터들을 쿼리키에 따라 invalidate 하거나 refetching 해야 하기 때문이다.

예를 들어 entities/stock/api 에서 새로운 제품을 post 해서 보내면 제품 리스트를 받아오는 get 요청을 하는 쿼리들을 invalidate 해야 한다.

하지만 여전히 고민인 부분, 훅과 상태관리와 컴포넌트의 응집도

하지만 여전히 고민인 부분은 프론트엔드는 결국 컴포넌트들을 개발하는 일을 하는 곳이고

대부분의 훅 (lib)이나 상태관리 (model) 들은 특정 ui 를 위해 존재하는 경우가 대부분이다. (물론 상위 레이어에서 재사용 할 수도 있긴 하겠지만)

그렇다면 특정 컴포넌트를 위한 훅이나 상태관리로직들을 굳이 슬라이스 내에서 lib,model 로 나눠 저장해야 하나?

차라리 이렇게 하는편이 훨씬 응집도가 높지 않나? 라는 생각을 한다.

  • {layers}/{slice}/ui/SomethingComponent/SomethingComponent.tsx
  • {layers}/{slice}/ui/SomethingComponent/lib/useSomethingHock.ts
  • {layers}/{slice}/ui/SomethingComponent/model/useSomethingStore.ts

이렇게 해두고 나머지 lib, model 들은 ui 세그먼트에서 외부로 export 하지 않으면 되니 외부에서 해당 컴포넌트를 이용 할 땐 여전히 {layers}/{slice}/ui/SomethingComponentimport 하여 사용 할 수 있을텐데 말이다.