작년 Next.js
를 공부하고 프로젝트를 하기 시작했을 때 프로젝트를 정리하기 위해서 다양한 폴더 구조들을 사용해 왔다. 스스로 이런 폴더 구조는 어떨까 하고 생각하며 적용하다 레퍼런스가 필요하다는 생각에 medium
을 살펴보았고 다음 글을 발견하게 되었다! FSD라고 불리는 아키텍쳐 이며 Feature Sliced
A Front-End Application Folder Structure that Makes Sense
이 글을 읽고 obsidian
에 정리해둔 채 유용하게 사용했고 현재까지도 이러한 구조로 프로젝트를 설계한다.
src
폴더: root 디렉토리
assets
components
: 앱 전체에서 공유되는 모든 컴포넌트hooks
: 앱 전체에서 공유되는 모든 hook config
: 앱 설정 파일features
: 앱의 features를 포함하는 폴더. 이 폴더 안에 대부분의 앱 코드가 저장됨lib
: 앱에서 사용되는 써드 파티 라이브러리 설정 파일services
: services를 담는 폴더stores
: 전역 state storestest
: mocks, helpers, utilities, configurations를 위한 테스트 코드types
: 공유되는 타입스크립트 타입utils
: 공유되는 utility 함수들providers
: Context 주입을 위해 사용되는 Provider들🎯 services는 애플리케이션의 핵심 비즈니스 로직이나 외부 API와의 통신, 데이터 변환, 유틸리티 함수 등의 기능을 의미한다.
여기서 나는 services
와 utils
의 정확한 차이점이 뭘까? 하는 생각이 들었다.
utils
와 services
의 차이점layout
이나 page
는 app
폴더에서 파일의 형태로 관리되기에 폴더 구조가 따로 필요 없다고 생각했다.
이 폴더 구조를 다음 명령어를 루트 디렉토리에서 실행하여 간단하게 만들어낼 수 있다.
mkdir -p src/{hooks,utils,assets,config,lib,services,test,components,features,stores,types, providers}
아직 잘 이해가 가지 않을 수 있다. 자세히 설명하겠다.
이 폴더 구조 적용시 유의해야할 중요한 점이 세가지가 있다.
알아둬야할 세가지 중요한 점:
features
폴더안에 위치시킨다. 모든 feature 폴더는 domain-specific 코드를 포함해야 한다.index.ts
파일을 통해서만 이루어져야 한다!앞서 언급한대로 앱의 대부분의 코드는 features 폴더안에 위치해야 한다.
api
: 모든 fetch
로직은 여기에 위치한다.components
: 특정 feature의 컴포넌트들hooks(==composables)
: 특정 feature의 훅들 (나는 composables
이 아닌 hooks
로 사용한다. Vue
에서 hook
을 composable
이라 부른다고 했던거 같다....)stores
: state managements 코드types
: features에 특정되는 타입 정의index.ts
: feature의 진입 포인트. feature의 public API로 동작한다, 또한 이 파일은 어플리케이션에서 공개적으로 사용될 부분만 export 해야한다.아래는 지양해야할 코드와 지향해야할 코드 예시이다.
# Bad 🚫 🚫 🚫
import { UserProfile } from '@/features/profile/components/UserProfile.tsx'
# Good ✅ ✅ ✅
import { UserProfile } from '@/features/profile'
-> 보면 알겠지만 features
에 있는 profiles
의 componets
에서 뭔가를 import
하면 안된다. components
에 뭔가 내가 꼭 export
해야할 컴포넌트가 있다면 profiles
폴더 안의 index.ts
파일에서 import
한 후 export
하고 그 index.ts
파일에서 불러와 사용해야 한다. 즉
# @/features/profile/index.ts
import { default as UserProfile } from '@/features/profile/components/UserProfile.tsx'
이렇게 index.ts
에 진입점을 만들고
# some components
import { UserProfile } from '@/features/profile/index.ts'
와 같은 형식이 되어야 한다는 의미이다.
그리고 편리하게도 이러한 룰을 ESLint
를 통해 강제할 수 있다.
rules: {
"no-restricted-imports": [
"error",
{
patterns: ["@/features/*/*"]
}
],
"import/no-cycle": "error",
...
}