아키텍처 가이드
Feature-Sliced Design (FSD)
이 프로젝트는 Feature-Sliced Design 아키텍처를 따릅니다.
핵심 원칙
- 레이어 기반 구조: 상위 레이어는 하위 레이어만 참조 가능
- 명확한 책임 분리: 각 레이어는 고유한 책임을 가짐
- 모듈 독립성: 같은 레벨의 모듈끼리는 독립적
프로젝트 구조
src/
├── app/ # 애플리케이션 초기화 및 전역 설정
├── pages/ # 페이지 컴포넌트
├── widgets/ # 독립적인 UI 위젯
├── features/ # 기능별 비즈니스 로직
├── entities/ # 비즈니스 엔티티 및 API
└── shared/ # 공통 유틸리티 및 UI 컴포넌트레이어별 역할
1. app/
역할: 애플리케이션 진입점 및 전역 설정
app/
├── index.tsx # 진입점
├── App.tsx # 메인 앱 컴포넌트
├── layouts/ # 레이아웃
│ ├── AuthLayout.tsx
│ └── BaseLayout.tsx
├── providers/ # 전역 프로바이더
│ ├── AppProvider.tsx
│ ├── ToastProvider.tsx
│ ├── ConfirmProvider.tsx
│ └── ThemeProvider.tsx
└── routers/ # 라우팅 설정
├── rootRouter.tsx
├── authRoutes.tsx
├── mainRoutes.tsx
└── guards/
└── AuthenticationGuard.tsx포함 내용:
- React Router 설정
- 전역 Context Provider
- 레이아웃 컴포넌트
- 인증 가드
규칙:
- 비즈니스 로직 작성 금지
- UI 컴포넌트는 레이아웃만
2. pages/
역할: URL 라우트와 1:1 매핑되는 페이지 컴포넌트
pages/
├── Dashboard/
│ ├── Dashboard.tsx
│ └── Dashboard.css.ts
├── UserManagement/
├── Processing/
├── Storage/
└── ...포함 내용:
- 페이지 레벨 컴포넌트
- widgets, features 조합
- 페이지 레벨 상태 관리
규칙:
- 비즈니스 로직은 features로 위임
- API 호출은 entities 사용
- 재사용 가능한 로직은 features로 분리
3. widgets/
역할: 페이지를 구성하는 독립적인 대형 UI 블록
포함 내용:
- 재사용 가능한 독립 UI 블록
- features와 entities 조합
- 자체 상태 관리
규칙:
- 다른 widgets 의존 금지
- features와 entities 사용 가능
- shared UI만 직접 사용
4. features/
역할: 사용자 시나리오와 비즈니스 로직
features/
├── user/
│ ├── ui/ # UI 컴포넌트
│ │ ├── SignInForm/
│ │ ├── SignUpForm/
│ │ └── UserTable/
│ ├── model/ # 비즈니스 로직 (커스텀 훅)
│ │ ├── useSignIn.ts
│ │ ├── useSignUp.ts
│ │ └── useCurrentUser.ts
│ ├── utils/ # 유틸리티 함수
│ │ └── validation.ts
│ └── constants/ # 상수
│ └── options.ts
├── processing/
│ ├── ui/
│ ├── model/
│ ├── utils/
│ └── constants/
└── ...포함 내용:
- 기능별 UI 컴포넌트
- 커스텀 훅 (비즈니스 로직)
- 기능별 유틸리티
- 기능별 상수
규칙:
- entities의 API 사용
- shared 사용 가능
- 다른 features 의존 금지
5. entities/
역할: 비즈니스 도메인 엔티티 및 API
entities/
├── auth/
│ ├── api/
│ │ ├── auth.api.ts
│ │ └── auth.dto.ts
│ └── model/
│ └── auth.model.ts
├── user/
│ ├── api/
│ │ ├── user.api.ts
│ │ └── user.dto.ts
│ └── model/
│ └── user.model.ts
└── ...포함 내용:
- API 함수
- DTO (Data Transfer Object)
- 도메인 모델
- 타입 정의
규칙:
- UI 컴포넌트 작성 금지
- 순수 데이터 레이어
- shared만 사용 가능
6. shared/
역할: 프로젝트 전반에서 재사용되는 코드
shared/
├── ui/ # 디자인 시스템
│ ├── atoms/ # Button, Input, Card 등
│ ├── molecules/ # Modal, Table, Select 등
│ └── organisms/ # VideoPlayer, Toast 등
├── hooks/ # 공통 커스텀 훅
│ ├── useForm.ts
│ ├── useToast.ts
│ └── useModal.ts
├── utils/ # 유틸리티 함수
│ ├── datetime.ts
│ ├── string.ts
│ └── sort.ts
├── api/ # API 클라이언트
│ ├── base.ts
│ └── error-handler.ts
├── types/ # 공통 타입
│ └── page.ts
├── config/ # 설정
│ ├── routes.ts
│ ├── queryKeys.ts
│ └── regex.ts
├── styles/ # 디자인 토큰
│ ├── theme.css.ts
│ ├── light.ts
│ └── dark.ts
└── lib/ # 라이브러리
├── websocket/
└── i18n/포함 내용:
- 디자인 시스템 (Atomic Design)
- 공통 훅
- 유틸리티 함수
- API 클라이언트
- 전역 설정
규칙:
- 비즈니스 로직 작성 금지
- 순수 함수 및 범용 컴포넌트만
- 다른 레이어 참조 금지
의존성 규칙
레이어 간 의존성
app
↓
pages
↓
widgets
↓
features
↓
entities
↓
shared규칙:
- 상위 레이어만 하위 레이어 참조 가능
- 하위 레이어는 상위 레이어 참조 금지
디렉토리 구조 예시
Feature 구조
features/user/
├── ui/
│ ├── SignInForm/
│ │ ├── SignInForm.tsx
│ │ ├── SignInForm.css.ts
│ │ └── SignInForm.test.tsx
│ ├── UserTable/
│ │ ├── UserTable.tsx
│ │ ├── UserTable.css.ts
│ │ └── UserTableColumns.tsx
│ └── index.ts
├── model/
│ ├── useSignIn.ts
│ ├── useSignUp.ts
│ ├── useCurrentUser.ts
│ └── index.ts
├── utils/
│ ├── validation.ts
│ └── index.ts
├── constants/
│ ├── options.ts
│ └── index.ts
└── index.tsEntity 구조
entities/user/
├── api/
│ ├── user.api.ts
│ ├── user.dto.ts
│ └── index.ts
├── model/
│ ├── user.model.ts
│ └── index.ts
└── index.tsPublic API (index.ts)
각 모듈은 index.ts로 public API를 노출합니다.
typescript
// features/user/index.ts
export { default as SignInForm } from './ui/SignInForm/SignInForm';
export { default as UserTable } from './ui/UserTable/UserTable';
export { useSignIn, useSignUp, useCurrentUser } from './model';typescript
// entities/user/index.ts
export * from './api/user.api';
export * from './model/user.model';
export type * from './api/user.dto';사용:
typescript
// ✅ Public API 사용
import { SignInForm, useSignIn } from '@features/user';
import { getCurrentUser, type UserDto } from '@entities/user';
// ❌ 직접 경로 사용 금지
import SignInForm from '@features/user/ui/SignInForm/SignInForm';실전 예시
페이지 구성
typescript
// pages/UserManagement/UserManagement.tsx
import { UserTable } from '@features/user';
import { useUserPage } from '@features/user';
import { SearchBar } from '@shared/ui';
export default function UserManagement() {
const { users, keyword, setKeyword, sort, setSort } = useUserPage();
return (
<div>
<SearchBar value={keyword} onChange={setKeyword} />
<UserTable users={users} sort={sort} onSort={setSort} />
</div>
);
}Feature 구현
typescript
// features/user/model/useUserPage.ts
import { useInfiniteQuery } from '@tanstack/react-query';
import { getUserPage } from '@entities/user';
import { queryKeys } from '@shared/config/queryKeys';
export function useUserPage() {
const [keyword, setKeyword] = useState('');
const query = useInfiniteQuery({
queryKey: queryKeys.user.list({ keyword }),
queryFn: ({ pageParam = 0 }) => getUserPage({ page: pageParam, keyword }),
getNextPageParam: (lastPage) => (lastPage.last ? undefined : lastPage.number + 1),
});
const users = query.data?.pages.flatMap((page) => page.content) || [];
return {
users,
keyword,
setKeyword,
...query,
};
}Entity 구현
typescript
// entities/user/api/user.api.ts
import { api } from '@shared/api/base';
import type { UserDto, PageRequest, PageResponse } from './user.dto';
export async function getUserPage(request: PageRequest): Promise<PageResponse<UserDto>> {
const { data } = await api.get<PageResponse<UserDto>>('/api/users', {
params: request,
});
return data;
}마이그레이션 가이드
기존 코드를 FSD로 마이그레이션하는 순서:
- shared 분리: 공통 UI, 유틸, API 클라이언트
- entities 생성: API 함수, DTO, 모델
- features 생성: 비즈니스 로직, UI 컴포넌트
- pages 정리: features 조합으로 단순화
- app 정리: 프로바이더, 라우팅만 유지