Next.js 프로덕션 사용 가이드
Next.js 16 App Router 애플리케이션의 모든 코드에 사용됩니다. 보안 관련 규칙(액션 내 인증, CSP, 헤더, taint)은 nextjs-security에, 컨테이너 규칙은 docker-nextjs에 있습니다. Claude Code에 내장된 next-best-practices 스킬은 일반적인 파일 규칙, 메타데이터, 이미지 및 폰트 최적화를 자세히 다루며, 이 스킬은 Nuvraxis의 구축 및 배포 방식에 특화된 내용만 추가합니다.
기본 설정: Next 16.4, React 19.3, Turbopack (dev 및 build의 기본값), cacheComponents: true (부분 프리페칭 활성화), output: "standalone", typedRoutes: true. 템플릿의 next.config.ts가 최종 진리(Source of Truth)입니다.
규칙
-
기본적으로 서버 컴포넌트;
"use client"는 상태, 효과(effect), 브라우저 API 또는 이벤트 핸들러에만 사용합니다. 지시어는 필요한 가장 작은 리프(leat) 컴포넌트에 배치하세요. nuvui를 임포트한다고 해서 파일에 추가하지 마십시오. nuvui의 상태ful 부분은 자체 지시어를 가지고 있습니다 (nuvui스킬의rules/server-components.md참고).// 잘못된 예: 하나의 핸들러 때문에 전체 페이지가 클라이언트가 됨 "use client"; export default function Page() { … <Select onValueChange={…} /> … } // 올바른 예: 페이지는 서버에 남아 있고, 핸들러는 리프에 위치 export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx에 "use client" 있음 }클라이언트 컴포넌트로 전달되는 props는 직렬화 가능해야 합니다: 함수(서버 액션 제외)나 클래스 인스턴스는 전달할 수 없습니다.
-
데이터가 필요한 컴포넌트에서 서버 측에서 fetch 하세요.
useEffect를 이용한 fetching이나, 페이지 자체에 데이터를 공급하기 위한 라우트 핸들러는 사용하지 마십시오. 요청별 중복 제거는 Reactcache()를 사용하고, 요청 간 캐싱은 규칙 3을 따르세요. -
"use cache"+cacheLife+cacheTag로 캐싱하고, 그 외에는 모두 동적(dynamic)입니다. Cache Components에서는 명시하지 않으면 아무것도 캐싱되지 않으며,fetch는 암시적 캐시를 갖지 않습니다.import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }이전의 세그먼트 설정(
export const dynamic,revalidate,fetchCache)은 사용하지 마십시오. Cache Components에서 오류가 발생합니다. 인수와 클로저로 감싸진 값은 캐시 키의 일부가 되므로, 전체 요청 객체가 아닌 ID를 전달하세요. 자세한 내용, 프로파일 및"use cache: private"에 대해서는 참조/캐싱.md를 참고하세요. -
서버 액션에서는
updateTag로, 그 외의 다른 곳에서는revalidateTag(tag, "max")로 무효화합니다.updateTag(tag)는 자신의 쓰기를 읽을 수 있는(read-your-own-writes) 동작을 제공하며 서버 액션 내에서만 작동합니다. 웹훅이나 라우트 핸들러에서는revalidateTag(tag, "max")(stale-while-revalidate) 또는revalidateTag(tag, { expire: 0 })를 사용하세요. 인수가 하나인revalidateTag(tag)는 더 이상 사용되지 않습니다(deprecated). -
요청 시 데이터를
<Suspense>로 감싸지 않으면 빌드가 실패합니다.cookies(),headers(),searchParams,connection()및 캐싱되지 않은 I/O가 Suspense 경계 밖에 있으면 "uncached or runtime data during prerendering" 오류로 프리렌더링이 중단됩니다. 스켈레톤 폴백과 함께 경계를 설정하고 캐싱하거나, (루트 레이아웃만, 규칙 14 참고)export const instant = false로 경로 차단을 표시하세요.params,searchParams,cookies(),headers()는 모두 비동기입니다:await하세요. -
모든 서버 액션은 자체적으로 입력을 검증하고 권한을 확인합니다. 서버 액션은 어떤 페이지에 작성되었든 공개 POST 엔드포인트입니다.
typescript-production의 스키마(valibot)를 사용하여FormData를 파싱한 다음, 권한을 확인하고, 변경(mutate)한 후,updateTag를 호출하세요. 예상되는 실패에는 타입이 지정된 결과를 반환하고, 버그에 대해서만 throw 하세요. 전체 규칙은nextjs-security에 있습니다."use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 없으면 401 const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 허용되지 않으면 403 await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
라우트 핸들러는 다른 클라이언트(웹훅, 모바일, 상태 확인)용이며, 자체 페이지에서는 사용하지 마십시오.
app/api/아래에 배치하세요. 액션과 마찬가지로 본문을 검증하고,Response.json(...)을 반환하며, 상태 코드를 의도적으로 설정하세요. -
모든 앱은 동적이고 의존성 없는
GET /api/health를 가져야 합니다. DockerHEALTHCHECK와 쿠버네티스 프로브가 이 엔드포인트를 호출합니다. 데이터베이스에 접근하지 않고 응답해야 하므로 느린 의존성으로 인해 모든 파드가 재시작되는 것을 방지합니다.await connection()을 사용하여 정적 파일로 프리렌더링되는 것을 방지하세요. 템플릿의src/app/api/health/route.ts에 있으며,proxy.ts는/api/health를 매처에서 제외합니다. -
error.tsx,loading.tsx및not-found.tsx는 루트에, 그리고 UX가 다른 세그먼트별로 배치합니다.error.tsx는 클라이언트 컴포넌트이며,instrumentation.ts의onRequestError를 통해 로깅하고,reset과 함께 재시도를 표시하며, 오류 메시지를 표시하지 마십시오. 빈 상태를 렌더링하는 대신 누락된 레코드에 대해notFound()를 호출하세요. -
NEXT_PUBLIC_*은 빌드 시에 고정되며, 그 외의 모든 것은 서버에서 런타임에 읽힙니다. 하나의 이미지가 메인에서 프로덕션으로 변경 없이 승격되므로,NEXT_PUBLIC_값은 모든 환경에서 동일합니다. 빌드 타임 상수(앱 이름, 모든 환경에서 동일한 공개 Sentry DSN)만 여기에 넣으세요. 환경별 설정(API URL, 기능 플래그, 비밀)은 서버 측process.env.X이며, 서버 컴포넌트나 액션에서 읽고 필요한 경우 클라이언트에 props로 전달됩니다.src/env.ts(valibot)에서 한 번 환경 변수를 검증하고 그곳에서 임포트하세요.next.config.ts에서env키를 절대 사용하지 마십시오: 클라이언트 번들에 인라인 처리됩니다. -
서버 전용 모듈은
import "server-only"로 표시하세요. 비밀을 읽거나, DB와 통신하거나, 자격 증명이 있는 SDK를 임포트하는 모듈에 사용하세요. 클라이언트에서 이를 임포트하면 누출 대신 빌드가 실패합니다. -
after()는 응답을 지연시켜서는 안 되는 작업(감사 로그, 분석, 캐시 워밍업)에 사용합니다.after()를 호출하기 전에cookies()/headers()를 읽고, 내부에서는 읽지 마세요. 동일한 프로세스에서 실행되므로 파드는 SIGTERM 유예 기간이 필요합니다 (차트에서terminationGracePeriodSeconds설정). -
src/instrumentation.ts를 사용하여 계측:registerOTel+onRequestError.@vercel/otel을 통한 OpenTelemetry, 런타임에 표준OTEL_EXPORTER_OTLP_*환경 변수로 구성된 익스포터. 참조/관측성.md 참고. -
output: "standalone"을 유지하고 워크스페이스 외부에서 절대 임포트하지 마십시오. Docker 러너는.next/standalone,.next/static및public만 복사합니다. 모노레포에서server.js는.next/standalone/apps/<app>/server.js에 위치하며, 워크스페이스 루트는pnpm-lock.yaml에서 찾으므로outputFileTracingRoot나turbopack.root를 설정하지 마십시오 (빌드에서 경고하지 않는 한). 자체 호스팅 세부 정보 (멀티 레플리카, 서버 액션 키,deploymentId): 참조/자체-호스팅.md 참고. -
루트 레이아웃은 CSP 논스를 읽으므로 앱은 요청별로 렌더링됩니다. 템플릿의
proxy.ts는 논스 기반 CSP를 설정하고src/app/layout.tsx에는export const instant = false가 있습니다. 의도적인 절충입니다: 논스와 프리렌더링된 정적 셸은 공존할 수 없으며, 로그인된 비즈니스 앱의 경우 엄격한 CSP가 승리합니다."use cache"를 사용한 데이터 캐싱은 여전히 작동합니다.nextjs-security규칙 4를 읽지 않고 두 부분 중 하나를 제거하지 마십시오. -
스타일: 글로벌 BEM SCSS, 컴포넌트에서 임포트합니다. Turbopack은
sass패키지로 Sass를 컴파일합니다;sassOptions.loadPaths: ["src/styles"]는 어떤 깊이에서도@use "abstracts"가 작동하게 합니다.~임포트나sassOptions.functions는 사용하지 마십시오. 규칙은scss-bem에 있습니다. -
이미지는
next/image를 통해, 폰트는next/font를 통해 사용합니다.sharp는 최적화 도구로 Next와 함께 제공되며, 러너 이미지에 추가 패키지가 필요하지 않습니다. 원격 이미지는 정확한 호스트 이름과 함께images.remotePatterns가 필요합니다. -
pnpm next analyze로 번들 크기를 확인하세요. (Turbopack의 분석기;@next/bundle-analyzer는 웹팩 전용입니다). 무거운 클라이언트 위젯(아래 접힌 부분의 nuvui 편집기, 지도, 차트)은next/dynamic으로 지연 로드하세요. -
타입이 지정된 라우트와 생성된 타입을 사용하세요.
typedRoutes: true는<Link href>를 확인합니다;PageProps<"/invoices/[id]">및LayoutProps는next typegen이후 전역에서 사용 가능합니다 (next dev/next build에 의해 자동 실행).params타입을 수동으로 작성하지 마십시오. -
next lint는 사용하지 마십시오. 16버전에서 제거되었습니다. Biome이 린트를 수행하고 (참조:biome), TypeScript는 패키지별로tsc --noEmit으로 검사합니다.
검증
pnpm typecheck
pnpm lint
pnpm build # turbo run build: 프리렌더 오류, 타입 오류, Sass 오류 시 실패
pnpm --filter <app> start & # 그런 다음:
curl -fsS localhost:3000/api/health
출처
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy