Ch5. Next.js App Router 엔터프라이즈 패턴
Multi-zone, Cache Components, Proxy, Server Actions, Route Handlers, 런타임 선택
핵심 요약
- Multi-zone은
next.config.ts의rewrites(beforeFiles)로 여러 Next.js 앱을 한 도메인 아래 결합하며, API는 각 앱의 API Routes+Server Actions로 처리합니다. - Next.js 16의
cacheComponents+use cache로 "기본 동적, 안정적 조각만 캐시"하고cacheLife·cacheTag·revalidateTag로 수명과 무효화를 제어합니다. - API Routes는 외부 웹훅·모바일·서드파티 연동에, Server Actions는 폼 제출·뮤테이션·UI 연동에 쓰며 90%는 별도 API 서버 없이 충분합니다.
- Next.js 16에서
middleware.ts가proxy.ts로 리네임되고 기본 Node.js 런타임으로 고정되며, Proxy 안에서는runtime설정과 fetch 캐시 옵션이 효과 없습니다. - Edge는 콜드 스타트 0ms에 30초 제한, Node.js는 Prisma 등 전체 드라이버에 300초 제한이니 용도에 맞춰 런타임을 고릅니다.
Multi-zone 아키텍처
규모가 커지면 하나의 Next.js 앱이 모든 경로를 떠안기 어렵습니다. Multi-zone은 여러 Next.js 앱을 한 도메인 아래로 묶습니다. API는 각 앱의 API Routes + Server Actions가 처리하므로 별도 API 서버를 둘 필요가 없습니다.
// apps/web/next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
async rewrites() {
return {
beforeFiles: [
{
source: '/admin/:path*',
destination: `${process.env.ADMIN_URL}/admin/:path*`,
},
{
source: '/docs/:path*',
destination: `${process.env.DOCS_URL}/docs/:path*`,
},
],
};
},
};
export default config;각 zone은 따로 배포하고, basePath 설정으로 자기 경로 접두사를 선언합니다.
Cache Components와 On-demand Revalidation
Next.js 16에서는 cacheComponents를 켜고, 페이지·컴포넌트·함수 단위로 캐싱 의도를 드러냅니다. 정적과 동적 경계가 뒤섞인 엔터프라이즈 앱이라면 "기본은 동적, 안정적인 조각만 캐시"하는 방식이 안전합니다.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;// app/products/[id]/page.tsx
'use cache';
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
cacheLife('hours');
cacheTag(`product-${id}`);
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}On-demand Revalidation은 데이터가 변경된 시점에 캐시를 즉시 무효화합니다:
// app/api/webhook/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { type, id } = await request.json();
if (type === 'product.updated') {
revalidateTag(`product-${id}`); // 특정 태그만 무효화
}
return Response.json({ revalidated: true });
}| 전략 | 데이터 특성 | 설정 |
|---|---|---|
| 정적 조각 | 변경 없음 (약관, 소개) | cacheComponents + use cache |
| 주기 캐시 | 주기적 변경 (블로그, 상품) | cacheLife() |
| On-demand | 이벤트 기반 변경 (재고, 가격) | revalidateTag() / revalidatePath() |
| 동적 렌더링 | 요청마다 다름 (대시보드) | export const dynamic = 'force-dynamic' |
기존 revalidate = 3600과 ISR 패턴도 그대로 쓸 수 있지만, 새로 설계할 때는 cacheComponents를 기준으로 잡습니다.
Proxy 설계
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
// 1. 인증 체크
const token = request.cookies.get('session_token');
if (request.nextUrl.pathname.startsWith('/admin') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 2. A/B 테스트
const bucket = request.cookies.get('ab_bucket')?.value
?? (Math.random() < 0.5 ? 'a' : 'b');
const response = NextResponse.next();
if (!request.cookies.get('ab_bucket')) {
response.cookies.set('ab_bucket', bucket, { maxAge: 60 * 60 * 24 * 30 });
}
response.headers.set('x-ab-bucket', bucket);
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Next.js 16에서 middleware.ts는 proxy.ts로 이름이 바뀌었습니다. Proxy는 요청이 라우트에 닿기 전에 헤더, redirect, rewrite, 쿠키, A/B bucket 같은 네트워크 경계 작업을 맡습니다.
Proxy 사용 범위
Proxy는 느린 데이터 조회나 전체 세션 관리를 넣을 자리가 아닙니다. 단순 redirect는 next.config.ts의 redirects를 먼저 쓰고, Proxy에는 요청 데이터가 필요한 가벼운 라우팅·보호 로직만 둡니다. fetch 캐시 옵션과 next.revalidate/next.tags는 Proxy 안에서는 듣지 않습니다.
Next.js 16의 Proxy는 기본적으로 Node.js 런타임에서 돌고, runtime 설정으로 바꿀 수 없습니다. Edge Runtime을 계속 써야 하는 기존 프로젝트라면 middleware.ts를 유지할지 별도 ADR로 결정하세요.
API Routes vs Server Actions 선택 기준
Next.js 안에서 API를 처리하는 방법은 두 가지입니다. 별도 API 서버 없이 용도에 따라 골라 씁니다.
| 기준 | API Routes (app/api/) | Server Actions ('use server') |
|---|---|---|
| 호출 방식 | HTTP 요청 (GET/POST/PUT/DELETE) | 함수 호출 (RPC) |
| 적합 용도 | 외부 웹훅, 모바일 앱, 서드파티 연동 | 폼 제출, 데이터 뮤테이션, UI 연동 |
| 캐싱 | Route Handler 캐싱 가능 | revalidatePath/Tag로 무효화 |
| 인증 | Proxy + 토큰 검증 | 세션 쿠키 자동 전달 |
Server Actions + Prisma 패턴
// app/settings/actions.ts
'use server';
import { getDb } from '@acme/db';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function updateProfile(formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
const db = getDb();
await db.user.update({
where: { id: getCurrentUserId() },
data: parsed.data,
});
revalidatePath('/settings');
return { success: true };
}API Routes + Prisma 패턴 (외부 연동용)
// app/api/users/route.ts
import { getDb } from '@acme/db';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page') ?? '1');
const db = getDb();
const users = await db.user.findMany({
take: 20,
skip: (page - 1) * 20,
orderBy: { createdAt: 'desc' },
});
return Response.json({ data: users, page });
}왜 별도 API 서버를 두지 않는가?
Next.js의 Server Actions과 API Routes는 Node.js 런타임에서 돌기 때문에 Prisma에 바로 접근할 수 있습니다. 별도 Express/Hono 서버를 따로 운영하면 배포 파이프라인도, 환경 변수도, 모니터링도 두 벌씩 손봐야 합니다. 90%의 엔터프라이즈 웹 앱은 Next.js 내장 API만으로 충분합니다.
Edge vs Node.js Runtime 선택 기준
| 기준 | Edge Runtime | Node.js Runtime |
|---|---|---|
| 콜드 스타트 | ~0ms (즉시) | 250ms+ |
| Node.js API | 사용 불가 | 전체 사용 가능 |
| DB 접근 | HTTP 기반만 (Neon Serverless, PlanetScale) | Prisma 등 모든 드라이버 |
| 최대 실행 시간 | 30초 (Vercel) | 300초 (Vercel) |
| 적합 용도 | 인증, 리다이렉트, 경량 API | SSR, 데이터 처리, 파일 I/O |
// Edge Runtime 명시
export const runtime = 'edge';
// Node.js Runtime (기본값)
export const runtime = 'nodejs';Proxy 파일에서는 위 runtime 설정을 쓸 수 없습니다. 런타임 선택은 Route Handler, Server Component, Server Action에서만 명시하세요.
Next.js 16 주요 변경 (2025.10)
Cache Components와 use cache
Next.js 16은 cacheComponents 설정과 use cache 지시어로 캐싱 의도를 한층 또렷하게 드러냅니다. 페이지, 컴포넌트, 함수 단위로 캐싱을 제어하고 cacheLife, cacheTag로 수명과 무효화 단위를 관리합니다:
// app/products/[id]/page.tsx
'use cache'
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}Turbopack 기본 번들러
Next.js 16부터는 개발과 프로덕션 모두 Turbopack이 기본 번들러입니다. webpack 설정이 남아 있다면 마이그레이션해야 합니다.
middleware → proxy 리네임
middleware.ts 파일명이 proxy.ts로 바뀌었습니다. 네트워크 경계와 라우팅이라는 역할을 이름에서 분명히 드러내려는 변경입니다. 기존 middleware.ts도 deprecation 경고를 띄우며 그대로 동작합니다. 마이그레이션은 npx @next/codemod@canary middleware-to-proxy .로 시작하세요.
비동기 전용 Request API
cookies(), headers(), params, searchParams 같은 요청 관련 API가 완전히 비동기 전용으로 바뀌었습니다. 동기 접근은 사라졌습니다.
이미지 컴포넌트 간소화
next/image가 네이티브 브라우저 기능 중심으로 간결해졌습니다. layout, objectFit, objectPosition 같은 prop은 deprecated 처리됐고 CSS로 대체합니다.
| 항목 | Next.js 15 | Next.js 16 |
|---|---|---|
| 번들러 | Turbopack (옵션) | Turbopack (기본) |
| 캐싱 | ISR + revalidate | cacheComponents + use cache |
| Middleware | middleware.ts | proxy.ts (Node.js 런타임, 리네임) |
| Request API | 동기/비동기 혼용 | 비동기 전용 |