Next.js 프로젝트 시작하기
기초는 첫날에 다지는 것이 가장 싸게 먹힙니다. 나중에 모든 규칙을 이미 위반한 코드 위에 억지로 끼워 넣어야 하거든요. frontend-starter 템플릿이 있다면 템플릿 복사본으로 시작하세요: 템플릿에는 아래 단계가 이미 완료되고 검증되어 있으며, 이 문서는 결정에 대한 설명과 감사 체크리스트 역할을 합니다.
이 문서는 단계 순서와 최소 필수 항목을 정의합니다. 실제 코드 작성은 fsd-frontend 스킬(프로젝트에 있다면), 인증은 frontend-auth-session, 커밋 전 검사는 anti-slop-review 스킬을 참고하세요. 완성된 설정은 references/configs.md에, Next + FSD 구조와 코드 예제는 references/next-structure.md에 있습니다.
규칙 우선순위. 사용자의 명시적 지시와 프로젝트의 에이전트 파일(AGENTS.md, CLAUDE.md)이 이 문서보다 중요합니다. 라이브러리 버전은 변하므로, 설정을 복사하기 전에 설치된 버전의 문서와 대조하고 직접 실행해보기 전까지는 작동한다고 단정하지 마세요.
Next.js에 대한 사실은 이 문서나 기억이 아닌 패키지 내 문서에서 가져옵니다. 16.2부터 문서는 node_modules/next/dist/docs/에 있으며, 프로젝트의 AGENTS.md가 이를 참조합니다. 여기 예제와 차이가 있다면 설치된 버전의 문서가 옳습니다.
표기: MUST — 위반 불가; SHOULD — 기본적으로 예, 예외에는 이유가 필요함.
1. 코드 작성 전 결정
표의 각 결정은 README(‘결정’ 섹션) 또는 ADR에 이유와 함께 기록합니다. 이유 없이 기본값에서 벗어나는 것은 금지됩니다.
| 질문 | 기본값 | 다른 경우 |
|---|---|---|
| Next.js 또는 SPA | SSR/SEO, BFF(비밀, HttpOnly 쿠키), route handler 중 하나라도 필요하면 Next.js | 로그인이 필요하고 SEO와 BFF가 필요 없는 내부 앱 → Vite + React Router: 빌드와 배포가 더 간단함 |
| 라우터 | App Router | Pages Router — 기존 프로젝트에서만 |
| 모노레포 | 아니오, 단일 앱 | 두 번째 앱이나 패키지가 실제로 생기면 → pnpm workspaces; Nx/Turborepo — 빌드나 CI가 느려지기 시작할 때 |
| Multi-Zones, 마이크로프론트엔드 | 아니오 | 독립적인 팀이 독립적으로 릴리스할 때 |
| 서버 데이터 | 초기 로딩은 Server Components; 인터랙티브 클라이언트 데이터와 뮤테이션은 TanStack Query | 순수 클라이언트 앱 → TanStack Query만 |
| 텍스트 및 i18n | 첫날부터 번역 모듈(next-intl 또는 유사), 단일 로케일이라도 프로젝트가 fsd-frontend를 따르면 | README에 ‘i18n 없음’ 명시적 결정 기록 |
| 패키지 매니저 | pnpm, packageManager에 버전 명시 | — |
| 배포 | 즉시 결정: Vercel, Node 서버(output: "standalone") 또는 Docker | 이에 따라 env, 캐시, CI가 달라짐 |
MUST: ‘미래를 위해’ 기술을 미리 추가하지 마세요. 모노레포, 두 번째 라우터, Storybook, 상태 관리자, MSW(‘백엔드 대용’)는 첫 번째 실제 사용자가 생길 때 추가합니다.
2. 단계 순서
- 프로젝트 생성 (
pnpm create next-app@latest): TypeScript, ESLint, Tailwind, App Router.packageManager,engines.node(Next 16은 Node 20.9+ 필요) 및.nvmrc고정. - 생성기 템플릿 즉시 삭제: 시작 페이지,
public/의 SVG, 템플릿 README, 데모 스타일. README를references/configs.md의 골격으로 대체. ‘나중에’ 남겨둔 템플릿은 프로덕션까지 살아남음.create-next-app의AGENTS.md와CLAUDE.md는 삭제하지 마세요:<!-- BEGIN:nextjs-agent-rules -->와<!-- END:nextjs-agent-rules -->사이 블록이 설치된 버전의 문서를 에이전트가 참조하도록 하며,next dev가 이를 자동으로 유지합니다. 프로젝트 자체 규칙은 이 마커 바깥에 추가합니다. - 엄격한 TypeScript (
references/configs.md의 4절) 및typecheck스크립트 (tsc --noEmit). - Next 위의 FSD 구조 (아래 3절).
- 린터 및 포매팅:
eslint-config-next기반 ESLint flat config + type-aware 규칙 + 경계 린터 + Prettier. Next 16은next lint를 제거했고next build시 린트하지 않음 — 린터는 별도 스크립트와 CI에서 실행. - Env 설정 (4절).
- 에러 모델 및 API 클라이언트를
shared/api에 (5절). - 프로바이더를
_app계층에: QueryClient, 테마, i18n, 전역 서비스(toast, confirm)는shared의 포트 +_app에서 구현. - UI 기초 (6절).
- 테스트: Vitest + Testing Library + MSW, e2e는 Playwright (7절).
- Git 훅: husky + lint-staged (eslint --fix, prettier) + commitlint.
- CI: 모든 푸시와 PR에 lint, typecheck, test, build (8절).
- 기본 보안 (9절).
- README를 이미 사실인 내용으로 채우고 (10절), 완료 조건(Definition of Done) 통과 (11절).
1~3단계는 별도 커밋 ‘scaffold’로: 생성기가 한 일과 당신이 한 일을 기록에서 구분할 수 있습니다.
3. Next 라우팅 위의 FSD
공식 FSD 가이드(App Router)에 따라: Next의 서비스 폴더 app/은 프로젝트 루트에, FSD 계층은 src/에, app과 pages 계층은 Next 서비스 폴더와 혼동을 피하기 위해 _ 접두사를 붙입니다.
app/ Next 라우팅: 경로 파일만
├── layout.tsx 루트 레이아웃 → src/_app의 프로바이더
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF), 얇게
proxy.ts Next 16+ (이전에는 middleware.ts), app/ 옆에
src/
├── _app/ app 계층: 프로바이더, 전역 스타일, 서비스 구현
├── _pages/ pages 계층
├── widgets/ (프로젝트에서 사용하는 경우)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
app/의 파일은 얇게:src/_pages에서 페이지를 다시 내보내기,generateMetadata,params/searchParams파싱(Next 15+에서는Promise이므로await필요) 및 props 전달만. 로직 없음.- Route handler와 Server Actions는 얇게: 입력을 파싱하고 검증 →
src의 함수 호출 → 응답 반환. 각각은 공개 진입점이므로 스스로 인증을 확인하며proxy.ts에 의존하지 않음. src/app과src/pages를 생성하지 마세요: Next는 루트와src모두에서 서비스 폴더를 찾으며, 같은 이름의 폴더가 쉽게 두 번째 라우터가 될 수 있습니다.- 경계 린터는 접두사를 인식:
src/app,src/pages/*대신src/_app,src/_pages/*패턴 (설정은fsd-frontend,references/conventions.md, 11절). "use client"는 트리에서 가능한 한 아래에 배치. 페이지는 기본적으로 서버 컴포넌트, 클라이언트 컴포넌트는 인터랙션이 있는 리프 노드.- 비밀 모듈은
import "server-only"로 시작,window를 사용하는 모듈은import "client-only"로 시작.
자세한 내용과 코드: references/next-structure.md.
4. 환경 설정
MUST:
- 변수는 두 모듈에서 읽음:
shared/config/env.server.ts(import "server-only") 및shared/config/env.client.ts. 스키마는zod, 임포트 시 검증: 설정 오류는 첫 번째 요청이 아닌 시작 시점에 발생. 나머지 코드에서는process.env사용 금지 (린터 규칙). NEXT_PUBLIC_*는 빌드 시 번들에 포함되며process.env.NEXT_PUBLIC_X직접 접근 시에만: 구조 분해나process.env[name]사용 금지. 여러 환경에 다른 공개 값을 가진 단일 이미지는 이렇게 빌드할 수 없음 — 해당 값은 서버에서 읽어 아래로 전달.- 비밀 값은 절대
NEXT_PUBLIC_접두사 없음. .env.example은 모든 변수에 주석과 안전한 값을 포함;.env*는.gitignore에,.env.example은 제외. CI 빌드는.env.example의 값으로 통과.
5. 에러 모델 및 API 클라이언트
MUST:
- 에러는
shared/api경계에서 한 번kind를 가진AppError로 정규화 (모델은fsd-frontend,references/conventions.md, 1절).shared/api위에서는 아무도Response, 상태 코드, 에러 텍스트를 직접 분석하지 않음. fetch기반의 단일request()(또는 동일하게 래핑된 생성 클라이언트):signal전달, 네트워크 에러를network로 변환, HTTP 상태를kind로 변환, 응답 본문을 스키마로 검증. 예:references/next-structure.md.- 에러 유형은 코드 기반: 상태,
extensions.code(GraphQL은 200으로 에러 응답), 본문의code. 백엔드가 텍스트만 반환하면 단일 매퍼에서 텍스트 매핑, 이유 주석 및 테스트 포함. - 백엔드 응답은 경계에서 도메인 타입으로 매핑; DTO는 UI로 전달되지 않음.
- 사용자 데이터는 사용자 간 캐시되지 않음: 세션에 의존하는 응답에
"use cache"/unstable_cache사용 금지. - 스키마(OpenAPI, GraphQL)가 있으면 클라이언트는 스키마에서 생성. 스키마 또는 생성된 코드는 리포지토리에 포함: CI가 실제 백엔드에 접근하지 않아야 함.
SHOULD: 전송 계층 위의 인터페이스(AuthApi + 팩토리) — 두 번째 소비자가 생겼을 때만 (fsd-frontend, references/state-and-data.md, 2.1). type: "mock" 같은 설정 값으로 ‘구현되지 않음’을 던지는 것은 도입하지 않음.
6. UI 기초
- 디자인 토큰 —
:root(및 다크 테마용)의 CSS 변수, Tailwind v4에@theme로 연결. 컴포넌트에서는 토큰만 사용; 원시 HEX 및 임의 값(text-[#3a3a3a])은 주석과 함께 일회성 예외. - 클래스가 존재해야 함. shadcn/ui 및 예제에서 복사한 컴포넌트는 자체 토큰(
bg-card,text-muted-foreground)과 함께 제공됨. 해당 토큰을 정의하거나 클래스를 교체하세요 — 그렇지 않으면 컴포넌트가 조용히 스타일 없이 남음. shared/ui의 기본 컴포넌트는 접근성을 위해 headless 라이브러리(Radix, React Aria) 기반; 변형은cva, 클래스 병합은cn(clsx+tailwind-merge).- 폰트는
next/font, 이미지는 크기를 명시한next/image,images.remotePatterns는 명시적으로 나열. - Storybook — UI 키트가 여러 앱이나 팀에서 재사용될 때, 그 이전에는 도입하지 않음.
7. 테스트
- Vitest + Testing Library + jsdom + MSW (
onUnhandledFrame: "error"(MSW 3; MSW 2에서는onUnhandledRequest)). 설정:references/configs.md. - Vitest는
asyncServer Components를 렌더링할 수 없음. 로직은 함수로 추출하여 단위 테스트로 테스트하고, 페이지 자체는 e2e로 테스트. - Playwright — 3~10개의 중요 경로에 대해. CI의 e2e는 예측 가능한 백엔드(모크 서버, 테스트 DB가 있는 스탠드)를 대상으로 하며, 테스트가 다른 사람의 데이터를 변경하는 공용 스탠드는 대상으로 하지 않음.
- 첫 번째 테스트는 0일차에 작성: 테스트 인프라가 작동하는지 확인하고 샘플을 제공.
8. CI
MUST: 메인 브랜치에 푸시할 때마다, 모든 PR에 대해: lint, typecheck, test, build. GitHub Actions 템플릿은 references/configs.md. 공개 리포지토리에는 Actions 무료, 비공개에는 월별 분량 제한.
- CI 빌드는
.env.example로 진행, 비밀 값 없이. - e2e는 격리된 백엔드가 있을 때 별도 job으로; 그 전까지는 README에 CI에 e2e가 없는 이유를 명시적으로 표시.
- CI 배지는 workflow와 함께 README에 추가.
9. 기본 보안
poweredByHeader: false;next.config.ts의headers()를 통한 헤더:X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY(또는 CSP의frame-ancestors). CSP with nonce는 별도 작업이지만, ‘있음/없음 및 이유’ 결정은 즉시 기록.- Server Actions와 route handlers는 공개 엔드포인트: 스키마로 입력 검증, 각각에서 권한 확인.
dangerouslySetInnerHTML— sanitizer와 이유 주석이 있는 경우에만.- 개인 데이터와 토큰은 로깅하지 않음.
- 인증은
frontend-auth-session참조.
10. README
골격: references/configs.md. 규칙 하나: README에는 이 커밋의 코드에 실제로 해당하는 내용만. 각 섹션은 설명하는 코드와 같은 커밋에서 업데이트. 계획은 별도 ‘다음 단계’ 섹션에, 아직 완료되지 않았음을 표시.
11. 시작 완료 조건 (Definition of Done)
- 생성기 템플릿 삭제됨; README는 실제 내용으로 채워진 골격.
-
packageManager,engines.node,.nvmrc고정. - TS strict;
typecheck,lint,test,build로컬에서 통과 (실제 실행, 추정 아님). -
_app/_pages접두사를 사용한 FSD 구조; 경계 린터가 상위 계층 임포트 감지. - Env는 스키마가 있는 두 모듈로;
process.env외부 사용은 린터가 금지;.env.example완전함. -
shared/api에AppError및request()구현, 테스트로 커버 (네트워크, 상태, 잘못된 스키마). -
_app의 프로바이더; 서버에서 QueryClient는 사용자 간 공유되지 않음. - 디자인 토큰은 CSS 변수로; 사용된 모든 클래스가 존재.
- Vitest + MSW + Playwright 설정, 각각 하나의 실제 테스트.
- Git 훅과 commitlint 작동.
- CI 통과.
- 보안 헤더 및
poweredByHeader: false. - 1절의 결정 사항 기록됨.
12. 의심될 때
- ‘지금 추가’와 ‘필요할 때 추가’ 사이에서 → 필요할 때.
- ‘직접 설정’과 ‘create-next-app이 생성한 대로 유지’ 사이에서 → 변경할 이유가 생길 때까지 유지하고, 변경 시 이유 기록.
- ‘단일 앱’과 ‘모노레포’ 사이에서 → 단일 앱.
- ‘기억 속 설정’과 ‘현재 버전 문서의 설정’ 사이에서 → 문서.