최종 업데이트: 2025-12-16
아키텍처 패턴: Headless Compound Pattern + Domain-Driven Design
src/
├── app/ # Next.js App Router (Controller)
│ ├── (auth)/ # 인증 관련 페이지
│ ├── record/ # 기록 관련 페이지
│ ├── myProfile/ # 프로필 관련 페이지
│ └── jjikboul/ # 찜볼 관련 페이지
│
├── domains/ # 도메인별 비즈니스 로직
│ ├── auth/ # 인증 도메인
│ │ ├── api/ # API 통신 (Repository)
│ │ ├── components/ # 도메인 전용 UI
│ │ ├── hooks/ # Headless Hook (비즈니스 로직)
│ │ └── types/ # Entity & Response 타입
│ ├── record/ # 기록 도메인
│ ├── profile/ # 프로필 도메인
│ ├── jjikboul/ # 찜볼 도메인
│ └── place/ # 장소 도메인
│ ├── api/
│ ├── components/
│ ├── constants/ # 도메인 상수
│ ├── hooks/
│ └── types/
│
├── components/ # 공통 UI 컴포넌트
│ └── common/ # 범용 컴포넌트
│ ├── Input/ # Compound Component
│ ├── LayerPopup/ # Compound Component
│ ├── NavigationHeader.tsx
│ ├── Avatar.tsx
│ ├── Loading.tsx
│ └── ...
│
├── hooks/ # 공통 Hooks (도메인 독립적)
│ ├── common.ts # useDebounce, useDidMountEffect, useRunOnce
│ ├── useIntersectionObserver.ts
│ └── getQueryClient.ts
│
├── lib/ # 공통 라이브러리
│ └── async/ # AsyncBoundary
│
├── store/ # 전역 상태 (Zustand)
│ └── user.tsx
│
├── types/ # 공통 타입 정의
│ ├── common.ts # Pagination
│ └── appScheme.ts # 앱 스킴 관련
│
├── utils/ # 유틸리티 함수
│ └── common.ts
│
└── api/ # API 설정
└── fetchClient.ts
- 위치:
app/ - 역할: Headless Hook 호출, UI 조합
- 규칙:
- 비즈니스 로직 포함 금지
- API 직접 호출 금지
- AsyncBoundary로 로딩/에러 처리
- 위치:
domains/[domain]/hooks/ - 역할: 비즈니스 로직, 상태 관리
- 규칙:
- UI 반환 금지
- API Hook 호출
- 순수 로직만 담당
- 위치:
domains/[domain]/api/ - 역할: API 통신, Transform 함수 호출
- 규칙:
- HTTP 요청/응답만 처리
- Response → Entity 변환
- 비즈니스 로직 금지
- 위치:
domains/[domain]/api/transform.ts - 역할: DTO → Entity 변환
- 규칙:
- 옵셔널 체이닝 + Nullish Coalescing 사용
- 완전한 Entity 객체 반환
- 위치:
components/common/,domains/[domain]/components/ - 역할: UI 렌더링
- 규칙:
- Compound Component 패턴 사용
- Context로 상태 공유
- 비즈니스 로직 금지
// domains/record/hooks/useRecordList.ts
export const useRecordList = (filter?: RecordFilter) => {
const { data } = useSuspenseQuery({
queryKey: ['records', filter],
queryFn: () => recordApi.getRecordList(filter),
});
return {
records: data,
// ... 상태와 액션만 반환
};
};// components/common/LayerPopup/LayerPopup.tsx
const LayerPopupContext = createContext<ReturnType<typeof useLayerPopup> | null>(null);
export const LayerPopup = ({ children }: { children: ReactNode }) => {
const popup = useLayerPopup();
return (
<LayerPopupContext.Provider value={popup}>
{children}
</LayerPopupContext.Provider>
);
};
LayerPopup.Trigger = ({ children }: { children: ReactNode }) => {
const { open } = useContext(LayerPopupContext)!;
return <button onClick={open}>{children}</button>;
};// domains/record/api/transform.ts
export const transformRecordResponseToEntity = (response: RecordResponse): Record => ({
recordId: response.recordId,
thumbnailUrl: response.thumbnailUrl ?? '',
videoUrl: response.videoUrl ?? '',
createTime: response.createTime ?? new Date().toISOString(),
});Controller (Page)
↓
Headless Hook
↓
Repository (API)
↓
Transform
금지된 의존성:
- Controller → Repository (직접 호출)
- Component → Repository (직접 호출)
- Repository → Headless Hook
// app/page.tsx
export default function HomePage() {
return (
<AsyncBoundary
pendingFallback={<Loading />}
rejectedFallback={<ErrorFallback />}
>
<RecordList />
</AsyncBoundary>
);
}
// RecordList.tsx
function RecordList() {
const { data } = useSuspenseQuery({
queryKey: ['records'],
queryFn: recordApi.getRecordList,
});
// data는 무조건 존재
return <ul>{data.map(...)}</ul>;
}- Headless Hook:
use[Domain][Action](예:useRecordCreate) - API Hook:
use[Domain][Action]Query/Mutation(예:useRecordListQuery)
- Root:
[Component].tsx(예:LayerPopup.tsx) - 하위:
[Component][Part].tsx(예:LayerPopupTrigger.tsx)
- Repository:
[domain]Api(예:recordApi) - Transform:
transform[Type]ResponseToEntity(예:transformRecordResponseToEntity)
- Entity: 도메인 엔티티 (예:
Record,User) - Response: API 응답 타입 (예:
RecordResponse)
domains/[domain]/types/- 타입 정의domains/[domain]/api/- API 함수 작성domains/[domain]/hooks/- Headless Hook 작성domains/[domain]/components/- UI 컴포넌트 작성app/- 페이지에서 조합
- Red: 실패하는 테스트 작성
- Green: 최소 코드로 테스트 통과
- Refactor: 코드 개선
- 레이어 분리 준수
- 의존성 방향 준수
- Headless Hook이 UI 반환하지 않음
- Compound Component가 비즈니스 로직 포함하지 않음
- AsyncBoundary 사용
- Transform 함수에서 완전한 Entity 반환
- 제거/이동된 파일: 47개
- 제거된 폴더: constants/, api/modules/, api/hooks/
- 제거된 코드: 약 2,700줄
- 전체 진행률: 87%
types/common.ts: Pagination (범용 타입)types/appScheme.ts: 앱 스킴 관련 (범용 타입)hooks/common.ts: useDebounce, useDidMountEffect, useRunOncehooks/useIntersectionObserver.ts: 무한 스크롤hooks/getQueryClient.ts: React Query 인프라