프로덕션 레벨의 확장 가능하고 유지보수하기 쉬운 React 애플리케이션 아키텍처
참고: alan2207/bulletproof-react
기존 방식은 파일 유형별로 폴더를 나눔:
src/
├── components/ # 모든 컴포넌트
├── hooks/ # 모든 훅
├── utils/ # 모든 유틸
└── types/ # 모든 타입
문제점: Calendar 기능 수정하려면 4개 폴더를 돌아다녀야 함
Bulletproof 방식은 기능(feature) 단위로 폴더를 나눔:
src/
├── features/
│ ├── calendar/ # Calendar 관련 모든 것
│ ├── auth/ # 인증 관련 모든 것
│ └── chat/ # 채팅 관련 모든 것
└── ...
장점: Calendar 수정할 때 features/calendar/ 폴더만 보면 됨
src/
├── app/ # 애플리케이션 레이어 (라우터, 프로바이더)
├── assets/ # 정적 파일 (이미지, 폰트)
├── components/ # 공용 컴포넌트 (Button, Modal, Input 등)
├── config/ # 환경 설정
├── features/ # ⭐ 기능별 모듈 (핵심!)
├── hooks/ # 공용 훅
├── libs/ # 외부 라이브러리 설정 (axios, dayjs 등)
├── stores/ # 전역 상태 관리
├── types/ # 공용 타입
└── utils/ # 공용 유틸 함수
features/calendar/
├── api/ # API 요청 함수, React Query 훅
│ ├── getCalendar.ts
│ ├── getBookings.ts
│ └── createBooking.ts
├── components/ # 이 기능에서만 쓰는 컴포넌트
│ ├── CalendarBody.tsx
│ ├── CalendarNavigator.tsx
│ └── BookingForm.tsx
├── hooks/ # 이 기능에서만 쓰는 훅
│ ├── useCalendarNavigation.ts
│ └── useBookings.ts
├── types/ # 이 기능에서만 쓰는 타입
│ └── index.ts
├── utils/ # 이 기능에서만 쓰는 유틸
│ ├── getCalendarDays.ts
│ └── checkAvailableDate.ts
└── index.ts # ⭐ Public API (배럴 파일)
shared(components, hooks, utils)
↓
features
↓
app
shared → 누구나 import 가능features → shared만 import 가능, 다른 feature import 금지app → 모든 것 import 가능왜?
// ❌ 나쁜 예: feature에서 다른 feature import
// features/calendar/components/CalendarBody.tsx
import { useAuth } from '~/features/auth/hooks/useAuth';
// ✅ 좋은 예: app 레벨에서 조합
// app/routes/calendar.tsx
import { useAuth } from '~/features/auth';
import { Calendar } from '~/features/calendar';
function CalendarPage() {
const { user } = useAuth();
return <Calendar userId={user.id} />;
}
각 feature의 index.ts에서 외부에 노출할 것만 export:
// features/calendar/index.ts
// 컴포넌트
export { Calendar } from './components/Calendar';
export { BookingForm } from './components/BookingForm';
// 훅
export { useCalendarNavigation } from './hooks/useCalendarNavigation';
export { useBookings } from './hooks/useBookings';
// 타입
export type { IBooking, ITimeSlot } from './types';
외부에서는 index.ts를 통해서만 import:
// ✅ 좋은 예
import { Calendar, useBookings } from '~/features/calendar';
// ❌ 나쁜 예: 내부 경로 직접 import
import { Calendar } from '~/features/calendar/components/Calendar';
.eslintrc.js에 추가:
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// features는 다른 features를 import 불가
{
target: './src/features',
from: './src/features',
except: ['./'],
},
// features는 app을 import 불가
{
target: './src/features',
from: './src/app',
},
],
},
],
},
};
src/
├── components/calendar/
│ ├── Body.tsx
│ └── Navigator.tsx
├── hooks/
│ ├── useBookings.ts
│ └── useCalendarNavigation.ts
├── libs/
│ └── bookings.ts
└── types/
└── booking.d.ts
src/
├── features/
│ └── calendar/
│ ├── api/
│ │ └── bookings.ts
│ ├── components/
│ │ ├── Body.tsx
│ │ └── Navigator.tsx
│ ├── hooks/
│ │ ├── useBookings.ts
│ │ └── useCalendarNavigation.ts
│ ├── types/
│ │ └── index.ts
│ └── index.ts
├── components/ # 공용만 남김
├── hooks/ # 공용만 남김
└── types/
└── base.d.ts # 공용 타입만 남김
| 상황 | 추천 |
|---|---|
| 작은 프로젝트 (5개 이하 페이지) | 굳이 안 써도 됨 |
| 중간 규모 (5-20개 페이지) | 적용 추천 |
| 대규모 (20개+ 페이지, 팀 협업) | 필수 |