핵심 설계 철학
- App Router와 React Server Components를 기본값으로 사용
page.tsx와layout.tsx는 서버 컴포넌트- 상호작용이나 브라우저 API가 필요한 가장 작은 잎 노드만 클라이언트 컴포넌트로 전환
- 기능 우선
components/,hooks/,utils/같은 “종류별” 폴더링을 최상위에서 지양하고, 도메인/기능 단위로 응집
- 단방향 의존성:
- 컴포넌트는 훅에 의존하고, 훅은 서비스에 의존하고, 서비스는 API에 의존
- 그 반대는 안 된다는 규칙을 계층 전체에 강제
app/디렉토리는 라우팅 전용- app/ 폴더는 URL, 메타데이터, 레이아웃, 오류·로딩 경계, HTTP 엔드포인트 어댑터만 둔다.
- 비즈니스 로직은 실제 도메인 기준의 features/ 디렉토리로 옮긴다.
- 함께 변경되는 코드를 기능 단위로 함께 배치:
- 전역
components/,hooks/,services/,utils/,types/,constants/폴더를 만들지 않는다.
- 전역
- 함수형/데이터 지향 : 클래스 대신 순수 함수 + 불변 데이터, 부수효과와 순수 계산의 분리(Action-Calculation-Data).
- 선언적 추상화 : 반복되는 명령형 로직(오버레이, 노출 감지 등)은 오버레이를 띄우는 상황이 많을 때 이를 추상화한 useOverlay 같은 Hook처럼 커스텀 훅/컴포넌트로 캡슐화합니다.
- I/O는 명시적인 경계에만 둔다
- 데이터베이스·외부 API·세션·캐시·로그는
server/아래에서만 접근한다.
- 데이터베이스·외부 API·세션·캐시·로그는
- Zod는 모든 신뢰 경계의 런타임 검증기:
- URL 파라미터, 검색 파라미터,
FormData, JSON body, 외부 API 응답, 환경 변수, 웹훅을unknown으로 받고 파싱한다.
- URL 파라미터, 검색 파라미터,
- TanStack Query는 브라우저가 소유하는 변경 가능한 서버 상태에만 사용:
- . 서버 컴포넌트가 소유하는 읽기 데이터에는 사용하지 않는다.
- 케밥케이스 고정:
- 파일, 디렉토리, 라우트 세그먼트 모두 kebab-case.
useEffect는 외부 시스템 동기화에만 사용:- 파생 상태, 이벤트 처리, 일반 데이터 조회, prop 동기화에는 사용하지 않는다.
- 컴포지션을 기본 재사용 수단으로 사용: boolean prop을 계속 추가하는 거대 컴포넌트 대신 작은 서브 컴포넌트,
children, slot, 제한된 compound component를 사용한다. - 테스트는 사용자 행위와 공개 계약을 검증
- 순수 규칙은 Vitest, UI는 Testing Library, 네트워크는 MSW, 핵심 여정은 Playwright, 시각·접근성 상태는 Storybook으로 나눈다.
- 경계는 문서가 아니라 도구로 강제한다
- ESLint, TypeScript, dependency-cruiser, Knip 등을 활용하여 금지 의존성과 순환 의존성을 차단한다.
목표와 비목표
목표
- 팀과 기능이 늘어도 변경 범위를 빠르게 추론할 수 있다.
- 서버 전용 코드와 브라우저 번들이 구조적으로 섞이지 않는다.
- 비즈니스 규칙을 프레임워크 없이 단위 테스트할 수 있다.
- 한 기능을 삭제하면 관련 코드와 테스트를 한 디렉터리에서 대부분 제거할 수 있다.
- 동일한 기능이 Server Action, Route Handler, 배치 작업 등 여러 진입점을 가져도 핵심 규칙을 재사용한다.
- 데이터 캐시, 브라우저 캐시, URL 상태, 폼 상태, 로컬 상태의 소유권이 충돌하지 않는다.
- 조직 규모가 커질 때 앱·패키지를 분리할 수 있지만 초기부터 분산 시스템 비용을 지불하지 않는다.
비목표
- 모든 프로젝트에 동일한 인증·데이터베이스·CMS를 강제하지 않는다.
- Clean Architecture의 모든 추상 계층을 기계적으로 구현하지 않는다.
- 단순 CRUD마다 repository interface, DTO, mapper, factory를 의무적으로 만들지 않는다.
- 모든 코드를 공통 패키지로 추출하지 않는다.
- 모든 상태를 전역 store에 넣지 않는다.
- 모든 요청을 Next.js Route Handler를 거쳐 보내지 않는다.
프로젝트 디렉토리 구조
apps/web/
├── public/
│ ├── icons/
│ └── images/
├── e2e/
│ ├── fixtures/
│ ├── pages/
│ └── journeys/
├── src/
│ ├── app/
│ ├── entities/
│ ├── features/
│ ├── server/
│ ├── shared/
│ ├── instrumentation.ts
│ ├── instrumentation-client.ts
│ └── proxy.ts
├── .env.example
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── playwright.config.ts
├── postcss.config.mjs
├── tsconfig.json
├── vitest.config.ts
└── readme.md
/app디렉토리를 얇게 만드세요. 라우트는 기능과 위젯을 조합하는 역할을 합니다- 비즈니스 로직은 비즈니스 개념과 밀접하게 연관시키세요. 복잡성은 레이아웃이나 라우트 파일이 아닌, 기능과 엔티티에 집중해야 합니다.
- 경계를 계약처럼 다루세요. 서버/클라이언트 경계, 모듈 경계, 공개 API는 명확하고 안정적이어야 합니다.
app 계층 구조
_로 시작하는 디렉토리는 private 디렉토리로 다른 route에서 재사용하지 않는 것들을 둔다
src/app/
├── (public)/
│ ├── sign-in/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _views/
│ │ └── sign-in-page.tsx
│ └── layout.tsx
├── (authenticated)/
│ ├── layout.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ └── _views/
│ │ └── dashboard-page.tsx
│ └── orders/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── error.tsx
│ ├── _views/
│ │ └── order-list-page.tsx
│ └── [id]/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── not-found.tsx
│ └── _views/
│ └── order-detail-page.tsx
├── api/
│ ├── health/
│ │ └── route.ts
│ ├── orders/
│ │ └── [id]/
│ │ ├── route.ts
│ │ └── cancel/
│ │ └── route.ts
│ └── webhooks/
│ └── payment-provider/
│ └── route.ts
├── _providers/
│ ├── app-providers.tsx
│ └── query-provider.tsx
├── _styles/
│ └── globals.css
├── error.tsx
├── global-error.tsx
├── layout.tsx
├── manifest.ts
├── not-found.tsx
├── robots.ts
└── sitemap.ts
파일 규칙
-
page.tsx:params와searchParams를 Zod로 파싱한다.- 인증된 actor 또는 tenant context를 얻는다.
- 서버 데이터 소유권인 경우 DAL·외부 API를 직접 호출한다.
- route-private view와 feature UI를 조합한다.
notFound, redirect, metadata 같은 라우팅 결정을 내린다.
-
_views/: 특정 URL 화면의 조합이며 다른 route에서 재사용할 의미가 없는 컴포넌트를 둔다 -
layout.tsx: 지속되는 shell과 provider 조립. 데이터 요청을 무조건 넣지 말고, 모든 하위 route가 필요할 때만 둔다. -
loading.tsx: 화면 전체를 복제하지 않고 route shell에 맞춘 skeleton만 둔다. -
error.tsx: 예상하지 못한 오류의 복구 UI다. 반드시 client component이며 도메인 오류 표시용으로 사용하지 않는다. -
not-found.tsx: 리소스 부재를 표현한다. -
template.tsx: navigation마다 subtree를 재마운트해야 하는 명확한 이유가 있을 때만 사용한다. -
route.ts: WebRequest를 기능 handler에 전달하고 WebResponse를 반환하는 얇은 adapter다.
라우팅 구조
- 세그먼트: 경로 계층 구조 정의
- 레이아웃: 하위 트리에 대한 공유 UI와 공유 서버 로직 정의
- 페이지: UI 엔드포인트 정의
- loading.tsx, error.tsx: 경계 정의
- 라우트 핸들러: HTTP 엔드포인트 정의