Ch5. Next.js App Router 엔터프라이즈 패턴
Multi-zone, Prisma 연동, ISR, Middleware, Server Actions, Edge Runtime
Multi-zone 아키텍처
대규모 서비스에서는 하나의 Next.js 앱이 모든 경로를 처리하지 않습니다. Multi-zone은 여러 Next.js 앱을 하나의 도메인 아래 결합합니다. API는 각 Next.js 앱의 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 설정으로 자신의 경로 접두사를 선언합니다.
ISR과 On-demand Revalidation
// app/products/[id]/page.tsx
export const revalidate = 3600; // 1시간 ISR
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
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 });
}| 전략 | 데이터 특성 | 설정 |
|---|---|---|
| 정적 생성 | 변경 없음 (약관, 소개) | revalidate 미설정 |
| ISR | 주기적 변경 (블로그, 상품) | revalidate = 3600 |
| On-demand | 이벤트 기반 변경 (재고, 가격) | revalidateTag() / revalidatePath() |
| 동적 렌더링 | 요청마다 다름 (대시보드) | export const dynamic = 'force-dynamic' |
Middleware 설계
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(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).*)'],
};Middleware는 Edge Runtime에서 실행됩니다. Node.js API를 사용할 수 없으므로 DB 직접 접근 대신 JWT 검증이나 쿠키 체크에 적합합니다.
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로 무효화 |
| 인증 | 미들웨어 + 토큰 검증 | 세션 쿠키 자동 전달 |
Server Actions + Prisma 패턴
// app/settings/actions.ts
'use server';
import { db } 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 };
}
await db.user.update({
where: { id: getCurrentUserId() },
data: parsed.data,
});
revalidatePath('/settings');
return { success: true };
}API Routes + Prisma 패턴 (외부 연동용)
// app/api/users/route.ts
import { db } from '@acme/db';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page') ?? '1');
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 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';Next.js 16 주요 변경 (2025.10)
use cache 지시어
Next.js 16은 기존 ISR/캐싱 모델을 use cache 지시어로 통합했습니다. 페이지, 컴포넌트, 함수 단위로 캐싱을 명시적으로 제어합니다:
// 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 경고와 함께 동작합니다.
비동기 전용 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 | use cache 지시어 |
| Middleware | middleware.ts | proxy.ts (리네임) |
| Request API | 동기/비동기 혼용 | 비동기 전용 |