← 개념 목록

Nextjs 앱 아키텍처

개념

TODO: 개념을 한 문장으로 설명

핵심 설계 철학

  1. App Router와 React Server Components를 기본값으로 사용
    1. page.tsxlayout.tsx는 서버 컴포넌트
    2. 상호작용이나 브라우저 API가 필요한 가장 작은 잎 노드만 클라이언트 컴포넌트로 전환
  2. 기능 우선
    1. components/, hooks/, utils/ 같은 “종류별” 폴더링을 최상위에서 지양하고, 도메인/기능 단위로 응집
  3. 단방향 의존성:
    1. 컴포넌트는 훅에 의존하고, 훅은 서비스에 의존하고, 서비스는 API에 의존
    2. 그 반대는 안 된다는 규칙을 계층 전체에 강제
  4. app/ 디렉토리는 라우팅 전용
    1. app/ 폴더는 URL, 메타데이터, 레이아웃, 오류·로딩 경계, HTTP 엔드포인트 어댑터만 둔다.
    2. 비즈니스 로직은 실제 도메인 기준의 features/ 디렉토리로 옮긴다.
  5. 함께 변경되는 코드를 기능 단위로 함께 배치:
    1. 전역 components/, hooks/, services/, utils/, types/, constants/ 폴더를 만들지 않는다.
  6. 함수형/데이터 지향 : 클래스 대신 순수 함수 + 불변 데이터, 부수효과와 순수 계산의 분리(Action-Calculation-Data).
  7. 선언적 추상화 : 반복되는 명령형 로직(오버레이, 노출 감지 등)은 오버레이를 띄우는 상황이 많을 때 이를 추상화한 useOverlay 같은 Hook처럼 커스텀 훅/컴포넌트로 캡슐화합니다.
  8. I/O는 명시적인 경계에만 둔다
    1. 데이터베이스·외부 API·세션·캐시·로그는 server/아래에서만 접근한다.
  9. Zod는 모든 신뢰 경계의 런타임 검증기:
    1. URL 파라미터, 검색 파라미터, FormData, JSON body, 외부 API 응답, 환경 변수, 웹훅을 unknown으로 받고 파싱한다.
  10. TanStack Query는 브라우저가 소유하는 변경 가능한 서버 상태에만 사용:
    1. . 서버 컴포넌트가 소유하는 읽기 데이터에는 사용하지 않는다.
  11. 케밥케이스 고정:
    1. 파일, 디렉토리, 라우트 세그먼트 모두 kebab-case.
  12. useEffect는 외부 시스템 동기화에만 사용:
    1. 파생 상태, 이벤트 처리, 일반 데이터 조회, prop 동기화에는 사용하지 않는다.
  13. 컴포지션을 기본 재사용 수단으로 사용: boolean prop을 계속 추가하는 거대 컴포넌트 대신 작은 서브 컴포넌트, children, slot, 제한된 compound component를 사용한다.
  14. 테스트는 사용자 행위와 공개 계약을 검증
    1. 순수 규칙은 Vitest, UI는 Testing Library, 네트워크는 MSW, 핵심 여정은 Playwright, 시각·접근성 상태는 Storybook으로 나눈다.
  15. 경계는 문서가 아니라 도구로 강제한다
    1. 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:

    • paramssearchParams를 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: Web Request를 기능 handler에 전달하고 Web Response를 반환하는 얇은 adapter다.


라우팅 구조

  • 세그먼트: 경로 계층 구조 정의
  • 레이아웃: 하위 트리에 대한 공유 UI와 공유 서버 로직 정의
  • 페이지: UI 엔드포인트 정의
  • loading.tsx, error.tsx: 경계 정의
  • 라우트 핸들러: HTTP 엔드포인트 정의