소스 폴더 구조
대부분의 코드는 src
폴더에 있으며 다음과 같은 구조를 가진다.
src
|
+-- app # 애플리케이션 레이어:
| |
| +-- routes # 애플리케이션 라우트 / 페이지
+-- app.tsx # 메인 애플리케이션 컴포넌트
+-- app-provider # 애플리케이션 제공자, 전체 애플리케이션을 글로벌 제공자로 감쌈
+-- assets # 정적 파일(이미지, 폰트 등)을 포함한 리소스 폴더
|
+-- components # 애플리케이션 전체에서 사용되는 공유 컴포넌트
|
+-- config # 글로벌 구성, 환경 변수 등
|
+-- features # 기능별 모듈
|
+-- hooks # 애플리케이션 전체에서 사용되는 공유 훅
|
+-- lib # 애플리케이션을 위해 사전 구성된 재사용 가능한 라이브러리
|
+-- stores # 글로벌 상태 저장소
|
+-- test # 테스트 유틸리티 및 Mock
|
+-- types # 애플리케이션 전체에서 사용되는 공유 타입
|
+-- utils # 공유 유틸리티 함수
확장성과 유지 보수성을 위해 대부분의 코드를 features 폴더 내에 구성한다. 각 기능 폴더는 해당 기능에 특정한 코드를 포함해 잘 분리된 상태를 유지한다. 이 접근 방식은 공유 컴포넌트와 기능 관련 코드를 섞지 않아, 코드베이스를 관리하고 유지하기 쉽게 만든다. 이를 통해 애플리케이션 아키텍처에서 협업, 가독성 및 확장성을 높일 수 있다.
features 폴더 구조
features 폴더는 다음고 같은 구조를 따를 수 있다.
src/features/awesome-feature
|
+-- api # 특정 기능과 관련된 API 요청 선언 및 API 훅
|
+-- assets # 특정 기능을 위한 정적 파일을 포함한 자산 폴더
|
+-- components # 특정 기능에 한정된 컴포넌트
|
+-- hooks # 특정 기능에 한정된 훅
|
+-- stores # 특정 기능을 위한 상태 저장소
|
+-- types # 기능 내에서 사용되는 타입스크립트 타입
|
+-- utils # 특정 기능을 위한 유틸리티 함수
모든 기능에 이 모든 폴더가 필요한 것은 아니다. 필요한 폴더만 포함하면 된다.
과거에는 barrel 파일을 사용해 기능에서 모든 파일을 내보내도록 권장했지만, 이는 Vite가 트리 셰이킹을 수행하는 데 문제를 일으켜 성능 문제로 이어질 수 있다. 따라서 파일을 직접 임포트하는 것이 좋다.
기능 간의 임포트는 피하는 것이 좋다. 대신 애플리케이션 레벨에서 다양한 기능을 조합한다. 이를 통해 각 기능이 독립적으로 유지되어 코드베이스가 덜 복잡해진다.
기능 간의 임포트를 금지하려면 다음과 같이 ESLint를 사용할 수 있다.
'import/no-restricted-paths': [
'error',
{
zones: [
// 기능 간 임포트를 비활성화:
// 예: src/features/discussions는 src/features/comments에서 임포트하지 않음
{
target: './src/features/auth',
from: './src/features',
except: ['./auth'],
},
{
target: './src/features/comments',
from: './src/features',
except: ['./comments'],
},
{
target: './src/features/discussions',
from: './src/features',
except: ['./discussions'],
},
{
target: './src/features/teams',
from: './src/features',
except: ['./teams'],
},
{
target: './src/features/users',
from: './src/features',
except: ['./users'],
},
// 등등...
],
},
],
단방향 코드베이스 아키텍처를 적용하는 것도 좋다. 이는 코드가 공유된 부분에서 애플리케이션으로 한 방향으로 흐르도록 하는 것이다(공유 -> 기능 -> 애플리케이션). 이는 코드베이스를 더 예측 가능하고 이해하기 쉽게 만든다.
단방향 코드베이스
공유된 부분은 코드베이스의 어느 부분에서도 사용할 수 있지만, 기능은 공유된 부분에서만 임포트할 수 있고, 애플리케이션은 기능과 공유된 부분에서 임포트할 수 있다.
다음과 같이 ESlint로 제한할 수 있다.
'import/no-restricted-paths': [
'error',
{
zones: [
// 단방향 코드베이스 강제:
// 예: src/app은 src/features에서 임포트할 수 있지만 반대는 안 됨
{
target: './src/features',
from: './src/app',
},
// 예: src/features와 src/app은 공유 모듈에서 임포트할 수 있지만 반대는 안 됨
{
target: [
'./src/components',
'./src/hooks',
'./src/lib',
'./src/types',
'./src/utils',
],
from: ['./src/features', './src/app'],
},
],
},
],
마무리
이러한 구성을 따르면 코드베이스를 잘 조직하고, 확장 가능하며, 유지 보수하기 쉽게 만들 수 있다. 이는 팀이 프로젝트에서 더 효율적이고 효과적으로 작업할 수 있도록 돕는다. 이 접근 방식은 Next.js, Remix 또는 React Native로 구축된 앱에 유사한 아키텍처를 적용하는 것도 더 쉽게 만든다.