FSD에 대해서는 다음과 같이 공식문서에서 설명하고 있다.
Feature-Sliced Design (FSD)는 프론트엔드 애플리케이션의 구조를 잡는 아키텍처 방법론입니다. 간단히 말해, 코드 구성에 관한 규칙과 관례를 모아놓은 것입니다. 이 방법론의 주요 목적은 계속 변화하는 비즈니스 요구사항에 직면했을 때 프로젝트를 더 이해하기 쉽고 안정적으로 만드는 것입니다.
소프트웨어 아키텍처란 소프트웨어 시스템과 그러한 구조와 시스템을 만드는 학문에 대해 추론하는 데 필요한 구조의 집합이다.
소프트웨어 아키텍처 패턴은 시스템 수준에서 반복되는 문제에 대한 재사용 가능하고 검증된 솔루션을 말하며, 시스템의 전반적인 구조, 구성 요소 상호 작용 및 품질 속성과 관련된 문제를 해결합니다. 소프트웨어 아키텍처 패턴은 소프트웨어 설계 패턴 보다 더 높은 수준의 추상화에서 작동하여 더 광범위한 시스템 수준의 과제를 해결합니다.
소프트웨어 아키텍처 원칙은 소프트웨어 설계 및 구현 과정에서 발생하는 복잡성을 관리하고, 유지 보수와 확장을 용이하게 해주는 지침을 말합니다. 소프트웨어 아키텍처 원칙에는 다양한 종류가 있지만, 기본적으로 관심사 분리, 모듈화, 추상화, 캡슐화 등을 들 수 있습니다.
실제로 프론트엔드를 처음 배웠을 때도 코드 분리 방법을 Atomic design에 따라 배웠는데, FSD도 Atomic Design과 비슷한 개념으로 이해할 수 있다.
Layers
→ Slices
→ Segment
에 해당하는 수직구조를 가짐.
모든 FSD 프로젝트에서 표준화 되어 있으며, 모든 레이어를 사용할 필요는 없으나 이름은 지켜야 한다.
주의
- App과 Shared는 슬라이스 없이, 직접 세그먼트로 구성!
- Layer는 계층적 구조에 해당하므로 import에 유의
슬라이스는 각 레이어 내에서 특정 기능 영역이나 도메인을 나타내는 하위 디렉토리로 비즈니스 도메인별로 코드를 분할한다.
기본적으로 같은 레이어 안에서 다른 슬라이스를 참조할 수 없다는 점에 유의하기!
// features와 entities Layer에서의 Slice 분리 예시
src/
features/
auth/ // 인증 관련 슬라이스
product-catalog/ // 상품 목록 관련 슬라이스
shopping-cart/ // 장바구니 관련 슬라이스
entities/
user/ // 사용자 엔티티 슬라이스
product/ // 상품 엔티티 슬라이스
출처 : https://j-ho.dev/28/
관례적으로 아래 폴더 이름을 사용하지만 반드시 제한되는 것은 아니다.
ui
- UI와 관련된 모든 것: UI 컴포넌트, 날짜 포맷터, 스타일 등api
- 백엔드 상호작용: request 함수, 데이터 타입, mapper 등model
- 데이터 모델: 스키마, 인터페이스, 스토어, 비즈니스 로직(상태관리, 액션, 셀렉터…)lib
- 슬라이스 안에 있는 다른 모듈이 필요로 하는 라이브러리 코드(유틸리티 함수)config
- 설정 파일과 기능 플래그, 상수// features Layer 내부의 auth Slice에서의 Segments 분할 예시
features/
auth/
ui/
LoginForm.tsx
RegisterForm.tsx
model/
authSlice.ts
authSelectors.ts
api/
authApi.ts
lib/
passwordValidation.ts
config/
authConstants.ts
출처 : https://j-ho.dev/28/
그렇다면 만약 기존 React 프로젝트에서 FSD 구조로 마이그레이션을 하게 될 경우 어떻게 해야할까? 이에 대해서도 공식문서에서 가이드라인을 제공하고 있다.
공식문서 마이그레이션 예시
App
과Shared
레이어를 모듈별로 천천히 구성하여 기반을 만드는 것으로 시작하세요.- 기존의 모든 UI를
Widgets
와Pages
에 큰 틀에서 분배하세요.
FSD 규칙을 위반하는 의존성이 있더라도 괜찮습니다.- 점진적으로 임포트 위반을 해결하고,
Entities
와 가능하다면Features
도 추출하기 시작하세요.⚠️ 리팩토링 중이거나 프로젝트의 특정 부분만 리팩토링할 때는 새로운 대규모 엔티티를 추가하지 않는 것이 좋습니다.
실제 회사에서는 atomic
기반의 아키텍처 설계를 통해 이용하고 있었고, 프로젝트가 커질수록 코드를 작성하는 과정이 너무 힘들었다. 한 가지 예를 들자면 api
폴더와 hook
폴더가 나누어져 있어, hook
에서 api
를 호출해 사용할 때면 매번 api
폴더 내에서 해당 코드를 찾아야 하는 등의 귀찮음이 있었다…
그래서 해당 구조를 적용하기로 제안했고 지금은 FSD로 마이그레이션을 완료한 상태로 개발 진행 중이다!
기본적으로 위에서 언급한 Layer의 규칙을 따르지만 일부는 변형하거나 이용하지 않았다.
사용한 Layer
App, Pages, Widgets, Shared
딱 공식문서에서 제안한 2단계까지 진행하였고, Entities
와 Feature
같은 경우 지금도 대규모 기능들이 일부 수정되거나 하는 문제가 있어 당장 분리하기에 현실적으로 쉽지 않았다(😢) 이거 쓰면서 리팩토링 해야지 생각 중…
실제로 프로젝트에 적용한 여러 사례들을 찾아보았는데, 모든 걸 FSD에 맞출 필요는 없다.가 공통적인 의견이고, 공식문서에서도 단계적으로 적용하는 것을 권장하고 있어 개발상 편의를 위해 우선적으로 적용 가능한 정도만 적용해 이용 중이다!
📦src
┣ 📂apis
┣ 📂components
┃ ┗ 📂common //공통 컴포넌트
┣ 📂data //static value 값, 조건 json 등등
┣ 📂fonts
┣ 📂hooks //react-hook
┣ 📂mocks
┣ 📂pages
┣ 📂recoil
┣ 📂routes
┣ 📂styles
┣ 📂themes
┣ 📂types
┗ 📂utils //각종 유틸 함수
기본적으로 components/
와 pages/
의 경우 라우팅 단위로 관리되었다.
app
의 경우 기존 프로젝트의 theme
, router
, styles
, recoil
일부를 가져와 구성하였고, pages
의 경우 이미 index.ts
를 이용해 FSD와 유사하게 구축한 상태였으므로 그대로 두었다.
📦app
┣ 📂provider
┃ ┣ 📂themes
┃ ┗ 📜GlobalStyle.ts
┣ 📂recoil
┃ ┣ 📜atom.ts
┃ ┗ 📜selector.ts
┗ 📂routes
┗ ┗ 📜router.tsx
기존에도 common/
이라는 폴더에서 공통 컴포넌트를 관리하고 있었으나 완전하진 않았다. 그리고 위의 data/
폴더나 hooks/
폴더, types/
와 utils/
는 별도로 관리하는 부분과 공통적으로 관리하는 부분으로 나눌 수 있었음에도 불구하고 비즈니스 의존성과 관련 없이 분리된 코드에 위치하고 있어 개발 중 확인하기에 쉽지 않았고, 각 의존성을 판단하기도 어려워 자꾸만 있던 함수가 생기고… 또 다른 이름으로 생기고… 가 반복되었다.
따라서 shared/
폴더에 위의 Slices 분리 방법을 이용해 공통 파일들을 모두 찾아내어 구성하는 작업을 거쳤다.
공식 문서 내 Slices 정의
ui
- UI와 관련된 모든 것: UI 컴포넌트, 날짜 포맷터, 스타일 등api
- 백엔드 상호작용: request 함수, 데이터 타입, mapper 등model
- 데이터 모델: 스키마, 인터페이스, 스토어, 비즈니스 로직(상태관리, 액션, 셀렉터…)lib
- 슬라이스 안에 있는 다른 모듈이 필요로 하는 라이브러리 코드(유틸리티 함수)config
- 설정 파일과 기능 플래그, 상수
위에서 언급했듯, 공식 문서에서는 이렇게 정의하고 있는데 마이그레이션 과정 중에는 이미 모듈화된 부분을 분리해야 했으므로 조금 수정해서 다음과 같은 기준으로 분리하였다. Slices의 경우 해당하는 항목이 있으면 폴더가 존재하고, 해당하는 항목이 없을 경우 한 가지 폴더만 존재할 수도 있다. (ex. ui/
만 존재할 수도 있음)
프로젝트 내 Slices 분리 방법
ui
- 기존components/
폴더 안 파일 중 해당하는 부분api
-apis/
폴더 안의 request 함수model
-hooks/
폴더 안 함수lib
-utils/
내부에 있던 함수config
-data/
내부의 함수type
-type
을 따로 관리하는 것이 좋을 것 같아 별도의 폴더를 생성하였다.
기본적으로 shared 내의 ui는 components/common/
내에서도 각자의 유형에 따라 다른 폴더에서 관리 중이었으므로(ex. Modal/Modal.tsx
, Switch/IOSSwitch.tsx
)그대로 옮겨와 이용하였다.
Widgets 구조 작업은 거의 components/
내에서 common/
을 제외한, 라우팅 단위로 분리된 코드를 가져오는 작업이었으나 가장 힘들었던 부분이 기존의 apis
, hooks
, utils
, data
폴더에서 각 라우팅 혹은 기능별로 의존성을 가지는 부분을 분리하는 것이었다.
사실상 3단계로 작성하였으나, 거의 2단계와 동일하게 진행되었고 단계를 나눈 이유는 이미 공통 컴포넌트를 관리하는 폴더가 존재했으므로,
공통 컴포넌트 우선 이동 → 의존성 파악 및 파일 분리 → Widgets 구성
단계로 진행되었다. 기본적으로 Slices
분리 방법은 Shared
와 동일하다!
이미 FSD를 적용해 코드를 분리한지도 2달이 다 되어가는데, 내부에서는 만족도가 아주 높은 편이고 실제로 내가 참여하지 않는 다른 프로젝트의 코드 구조도 FSD로 변경되었다(ㅋㅋㅋ)
솔직한 후기를 작성해보자면…
파일 옮기는게 생각보다 너무 빡센 작업이고, 사실 설계가 어느 정도 잡혀 있었다면 이만큼 어렵지는 않았을 것 같은데 프로젝트가 자꾸 변경되다 보니 설계가 엉망이 된 지 오래라 분리 작업이 정말 시간이 오래 걸렸다.
Shared
분리 과정 중, 정말 많은 중복 코드를 발견했다. 이런 부분들을 제거하고 통합하는 과정에서 코드 구조나 가독성이 훨씬 좋아졌고 이후에 코드 작성을 진행할 때도 확인할 부분들을 한 눈에 볼 수 있어서 좋았다. (물론… Storybook을 구성하는게 먼저일 것 같긴 하다는 점)
기존에 사용하던 아키텍처 구조에 비해 코드 작성이 너무 쉬워졌고, 리팩토링이나 기능 개선 작업이 훨씬 편리해졌다. 한 폴더 내에서 기본적인 로직이 관리 가능하기 때문인 것 같다.
모두가 말하는 것처럼, 무조건 FSD를 적용해야 한다! 라는 법은 없지만, 지금 아키텍처에 불편을 느끼고 있거나 FSD 아키텍처를 경험해보고 싶다면 한 번 시도해볼 법한 구조인 것 같다👍🏻
Widget
이나 Feature
도 리팩토링 어서 들어가서 분리해본 뒤 다시 찾아오겠습니다😢