- #Project
- #Burgerput
폴더 구조 리팩토링하기.. 어 나야 FSD
Prolip
2024-10-02
Feature-Sliced Design (FSD)에 맞게 폴더 구조 리팩토링하기..
시작..
사실 최근 들어 큰 문제가 하나 생겼습니다.
프로젝트에서 사용하는 컴포넌트의 수가 점점 커짐에 따라 컴포넌트들이 점점 복잡해지고 기능을 수정할 때 이리저리 찾아다니는 일이 많아지게 되었습니다..
이는 곧 유지보수나 기능 확장이 어려운 상황을 초래할 수 있어 폴더 구조 변경을 진행하게 되었습니다..
FSD(Feature-Sliced Design)
FSD는 프로젝트를 도메인별로 나누어 각 레이어를 독립적으로 관리하는 방법론입니다.
사실 이 폴더 구조라는게 진짜 정답이 없다고 생각을 합니다.
근데 유튜브에 FSD에 대한 내용을 다루는 영상을 보고 꽤 매력적이라 시도하게 되었습니다..
Overview | Feature-Sliced Design
FSD는 크게 레이어, 슬라이스, 세그먼트로 이루어진 계층 구조를 따릅니다.
1. 레이어 (Layers)
레이어는 프로젝트에서 주요한 논리적 분할을 제공하는데 총 7가지가 있으며 각각 app, pages, widgets, features, entities, shared로 구성됩니다.
processes는 deprecated 됐다고 합니다.
이들은 프로젝트의 전반적인 구성을 정의하는데 각 레이어는 그 하위에 있는 레이어의 내용만 참조할 수 있습니다.
즉, shared에선 entities나 features를 참조할 수 없는데 반대로 entities나 features는 shared를 참조할 수 있는 것입니다.
2. 슬라이스 (Slices)
슬라이스는 코드베이스를 비즈니스 도메인별로 분할한 것으로 위의 예시를 볼 때 user, post, comment 같은 이름으로 슬라이스를 정의할 수 있습니다.
슬라이스는 같은 레이어 내에서 다른 슬라이스를 참조할 수 없으며 이를 통해 높은 응집력과 낮은 결합도를 유지할 수 있습니다.
3. 세그먼트 (Segments)
슬라이스 및 레이어는 세그먼트로 나뉘어 코드의 목적에 따라 그룹화됩니다. 세그먼트에는 주로 ui(UI 관련 코드), api(서버와의 상호작용), model(데이터 모델과 비즈니스 로직) 같은 이름이 사용됩니다.
레이어 (Layers)
더 자세히 알아보자면 레이어의 목적은 코드의 책임과 다른 모듈에 대한 의존성에 따라 코드를 분리하는 것입니다.
위에서 설명한 6개의 레이어가 사용되며 가장 많은 책임과 의존성을 가진 레이어부터 가장 적은 레이어 순으로 배열됩니다.
레이어는 프로젝트에서 필요한 경우에만 추가하면 되며 모든 레이어를 반드시 사용할 필요는 없다고 합니다.
각 레이어에 대해 설명하자면
App (앱 레이어)
앱 전역과 관련된 모든 사항을 처리합니다. 여기엔 기술적인 의미와 비즈니스적인 의미 모두 포함됩니다. 이 레이어는 슬라이스 대신 세그먼트로 구성됩니다.
예시로 전역 스타일, 라우팅, 프로바이더 등이 포함됩니다.
Processes (프로세스 레이어로 현재 사용되지 않습니다.)
프로세스는 복잡한 다중 페이지 상호작용을 처리하는 레이어였으나 현재는 사용 중단 되었습니다.
Pages (페이지 레이어)
웹 사이트와 같은 페이지 기반 어플리케이션의 페이지 혹은 모바일 어플리케이션의 화면을 구성하는 레이어입니다.
이 레이어의 슬라이스는 라우터에 바로 연결할 수 있는 UI 컴포넌트를 포함하고 데이터 패칭 및 에러 처리 로직도 포함될 수 있습니다.
Widgets (위젯 레이어)
엔티티와 기능을 조합해 구성된 독립적인 UI 블록입니다.
이 레이어의 슬라이스는 보통 비즈니스 로직 없이 사용할 수 있는 완성된 UI 컴포넌트를 포함해 제스처나 키보드 인터랙션 같은 비즈니스와 무관한 로직이 포함될 수 있습니다.
근데 전 버거풋 리팩토링할 때 기능적인 UI 요소로 특정 페이지에서만 사용되면 widgets 레이어로 빼버렸습니다.
Features (기능 레이어)
사용자가 어플리케이션에서 비즈니스 엔티티와 상호작용해 가치 있는 결과를 얻을 수 있도록 하는 기능을 포함합니다.
이 레이어의 슬라이스는 인터랙티브 UI 요소, 내부 상태 및 API 호출을 포함할 수 있습니다.
Entities (엔티티 레이어)
보통 제품과 관련된 용어를 사용해 슬라이스를 정의하는데 보통 store나 특정 기능이나 페이지에서 사용되는 query 데이터를 관리하는 레이어로 사용되는 것 같습니다..
저도 그렇게 썼습니다.
Shared (공유 레이어)
비즈니스 로직과 분리된 UI 컴포넌트, 여러 컴포넌트에서 사용되는 커스텀 훅이 해당 레이어에 포함됩니다.
슬라이스 (Slices)
FSD의 두번째 depth로 각 레이어에서 중요한 역할을 하는 코드를 그룹화하는 것이 주된 목적입니다.
일반적으로 슬라이스의 이름은 표준화되지 않고 어플리케이션의 비즈니스 도메인에 따라 결정됩니다.
만약 앨범에 관련된 어플리케이션이라면 photo, creaet-album, gallery-page와 같은 슬라이스가 있을 수 있고,
sns라면 post, add-user-to-friends, news-feed 등의 슬라이스가 있을 것입니다.
비슷한 슬라이스끼리 하나의 폴더에 그룹화할 수 있으나 해당 폴더 안에서도 다른 슬라이스처럼 서로 격리된 상태로 유지해야만 하며 코드 공유는 불가합니다.
세그먼트 (Segments)
세그먼트는 계층 구조의 마지막 depth로 코드를 기술적인 성격에 따라 그룹화하는 역할을 수행합니다.
몇 개의 표준화된 세그먼트 이름이 있는데 각각의 세그먼트는 다음과 같은 역할을 합니다.
- ui: UI 컴포넌트
- model: 비즈니스 로직과 데이터 저장소, 해당 데이터를 조작하는 함수
- lib: 보조적이고 인프라와 관련된 코드, 커스텀 훅 여기에 넣었습니다..
- api: 외부 API와의 통신에 사용되는 메서드
새로운 세그먼트를 정의하는 것도 가능하며 전 상수를 관리하는 consts 폴더도 정의했습니다.
공식 문서의 각 레이어별 세그먼트 예시는 다음과 같습니다.
- Shared 레이어
- ui: UI 키트
- model: 보통 사용되지 않음
- lib: 여러 관련 파일의 유틸리티 모듈
- api: 인증 또는 캐싱 같은 추가 기능을 포함한 간단한 API 클라이언트
- Entities 레이어
- ui: 비즈니스 엔티티의 스켈레톤 UI, 인터랙티브 요소를 위한 슬롯 포함
- model: 서버 측 데이터를 포함한 인스턴스의 저장소 및 데이터를 조작하는 함수
- lib: 저장과 관련되지 않은 인스턴스를 조작하는 함수
- api: 백엔드와 통신하기 위해
Shared
레이어의 API 클라이언트를 사용하는 API 메서드
- Features 레이어
- ui: 기능을 사용자가 사용할 수 있도록 하는 인터랙티브 요소
- model: 비즈니스 로직과 데이터 저장소, 주로 사용자가 가치를 얻을 수 있는 코드
- lib: 비즈니스 로직을 설명하는 인프라 코드
- api: 이 기능을 백엔드에서 나타내는 API 메서드
- Widgets 레이어
- ui: 엔티티와 기능을 결합한 독립적인 UI 블록
- model: 필요한 경우 인프라 데이터 저장소
- lib: 제스처와 같은 비즈니스와 무관한 상호작용 코드
- api: 보통 사용되지 않음, 단 Remix와 같은 중첩 라우팅 컨텍스트에서는 데이터 로더로 사용할 수 있음
- Pages 레이어
- ui: 엔티티, 기능, 위젯을 결합해 완전한 페이지를 구성
- model: 보통 사용되지 않음
- lib: 비즈니스와 무관한 상호작용 코드
- api: SSR(서버 사이드 렌더링) 프레임워크를 위한 데이터 로더
그래서 리팩토링 전의 구조는 어땠을까요?
│ App.jsx
│ App.module.css
│ index.css
│ main.jsx
│
├─api
│ index.js
│ Loading.js
│ Managers.js
│ Products.js
│ user.js
│
├─components
│ │ Banner.jsx
│ │ Banner.module.css
│ │ Button.jsx
│ │ Button.module.css
│ │ ChatLogs.jsx
│ │ ChatLogs.module.css
│ │ ChatWindow.jsx
│ │ ChatWindow.module.css
│ │ Confirm.jsx
│ │ Confirm.module.css
│ │ CustomProduct.jsx
│ │ CustomProducts.module.css
│ │ Footer.jsx
│ │ Footer.module.css
│ │ HowToUse.jsx
│ │ HowToUse.module.css
│ │ InputChatMessage.jsx
│ │ InputChatMessage.module.css
│ │ InputTemp.jsx
│ │ InputTemp.module.css
│ │ InputTempForm.jsx
│ │ InputTempForm.module.css
│ │ InputUserName.jsx
│ │ InputUserName.module.css
│ │ ManagerList.jsx
│ │ ManagerList.module.css
│ │ Modal.jsx
│ │ Modal.module.css
│ │ Navbar.jsx
│ │ Navbar.module.css
│ │ RandomTemp.jsx
│ │ RandomTemp.module.css
│ │ RandomTempForm.jsx
│ │ RandomTempForm.module.css
│ │ SignInForm.jsx
│ │ SignInForm.module.css
│ │ UpdatedValueChecker.jsx
│ │ UpdatedValueChecker.module.css
│ │
│ └─ui
│ │ BackDrop.jsx
│ │ BackDrop.module.css
│ │ ChatMessage.jsx
│ │ DdayCounter.jsx
│ │ DdayCounter.module.css
│ │ InfoMessage.jsx
│ │ LoadingBarSpinner.jsx
│ │ LoadingSpinner.module.css
│ │ message.module.css
│ │ Modal.jsx
│ │ Modal.module.css
│ │ PacmanSpinner.jsx
│ │ PacmanSpinner.module.css
│ │
│ └─diffValues
│ AddedOrRemovedItem.jsx
│ AddedOrRemovedItem.module.css
│ EditItem.jsx
│ EditItem.module.css
│
├─hooks
│ Admins.jsx
│ customProducts.jsx
│ managerList.jsx
│ products.jsx
│ RandomTemp.jsx
│ useDateCheck.jsx
│ useDebounce.jsx
│ useGenerateRandomTemp.jsx
│ useModal.jsx
│ useNavigator.jsx
│ useOutletDisplay.jsx
│
├─pages
│ AdminProfile.jsx
│ AdminProfile.module.css
│ CheatModeGuide.jsx
│ CheatModeGuide.module.css
│ CustomFoods.jsx
│ CustomMachines.jsx
│ CustomPage.module.css
│ EditManagers.jsx
│ EditManagers.module.css
│ InputFoodTemp.jsx
│ InputMachineTemp.jsx
│ InputTemp.module.css
│ Main.jsx
│ NotFound.jsx
│ RandomFoodTemp.jsx
│ RandomMachineTemp.jsx
│ RandomTemp.module.css
│ SiginIn.module.css
│ SignIn.jsx
│ SignIn.module.css
│
├─store
│ manager.js
│ products.js
│ uiState.js
│ user.js
│
└─utils
server.js
끔찍했습니다.
뒤죽박죽 이게 정확히 어떤 기능을 하는 컴포넌트인지 파일명만 보고는 확실히 알 수 없었고, 무엇보다 같은 기능을 수행하는 컴포넌트가 묶여있지 않아 굉장히 불편했습니다.
어떻게 변했을까요?
App 레이어
/app
├─providers
└─routes
Pages 레이어
/pages
├─edit-admin-profile
├─edit-managers
├─input-temp
│ ├─food
│ └─machine
├─not-found
│ └─ui
├─random-temp
│ ├─food
│ ├─guide
│ └─machine
├─select-products
│ ├─food
│ └─machine
└─sign-in
Widgets 레이어
/widgets
├─chat-window
│ ├─chat-logs
│ ├─input-chat-message
│ └─ui
├─custom-product
│ └─ui
├─edit-admin-form
│ ├─consts
│ └─ui
├─Footer
│ └─ui
├─input-temp-form
│ └─ui
├─input-user-name
│ └─ui
├─Navbar
│ ├─consts
│ └─ui
│ ├─DropDownContact
│ ├─DropDownMenu
│ └─Navbar
├─random-temp-form
│ └─ui
└─sign-in-form
├─api
└─ui
Features 레이어
/features
├─alert
│ ├─error-alert
│ └─success-alert
├─dday-counter
│ └─ui
│ └─DdayCounter
├─generate-random-temp
│ └─model
├─how-to-use
│ └─ui
│ └─HowToUse
├─input-temp
│ └─ui
├─random-temp
│ ├─lib
│ │ └─hooks
│ └─ui
├─select-manager
│ ├─model
│ └─ui
└─updated-value-check
├─model
└─ui
├─AddedOrRemovedItem
├─EditItem
└─UpdatedValueChecker
Entities 레이어
/entities
├─custom-products
│ ├─input-temp
│ │ ├─api
│ │ └─model
│ └─random-temp
│ ├─api
│ └─model
├─manager
│ ├─api
│ ├─consts
│ └─model
├─select-products
│ ├─api
│ └─model
├─socket
│ ├─lib
│ └─model
├─ui-state
│ └─model
└─user
├─api
└─model
Shared 레이어
/shared
├─api
├─lib
│ └─hooks
└─ui
├─Banner
│ └─ui
├─Button
│ └─ui
├─LoadingSpinner
│ ├─BarSpinner
│ │ └─ui
│ └─PacmanSpinner
│ └─ui
└─Modal
└─ui
├─BackDrop
└─Modal
마치며..
트리 구조에 파일은 안 보이게 했습니다.. 파일까지 넣기엔 너무 길어져서요.
공식 문서의 예제를 참고하며 최대한 구조를 지키려고 노력은 했습니다..만 사실 어느정도 수정은 하게 된 거 같습니다.
혹시라도 해당 폴더 구조에 어떤 파일들이 담기는지 궁금하신 분들을 위해
링크 남겨드립니다…
.
.
.
감사합니다..