API Proxy
Next.js 16 Proxy의 실행 위치와 matcher를 이해하고 인증·공통 헤더·요청 차단 경계를 설계합니다.
Next.js 16의 Proxy는 요청이 페이지나 라우트 핸들러에 도달하기 전에 실행되는 코드입니다.
공통 헤더 추가, 경로 변경, 간단한 인증 확인처럼 여러 경로에 반복되는 진입 규칙을 처리할 수 있습니다.
Proxy가 모든 업무 로직을 대신하는 것은 아닙니다.
데이터베이스를 조회해야 하는 소유권 판단과 실제 데이터 변경 권한은 라우트 핸들러에서 다시 확인합니다.
Proxy의 역할
Proxy는 요청 URL과 쿠키, 헤더를 읽고 다음 동작을 선택합니다.
- 요청을 다음 처리 단계로 보냅니다.
- 다른 URL로 리다이렉트합니다.
- 브라우저 주소는 유지한 채 내부 대상을 다시 씁니다.
- 오류나 JSON 응답을 즉시 반환합니다.
실행 범위가 넓을수록 모든 요청의 비용이 늘어납니다.
따라서 필요한 경로만 matcher로 선택합니다.
기본 Proxy 작성
프로젝트 루트의 src/proxy.ts 파일에서 요청을 받아 응답을 반환합니다.
import { NextResponse, type NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-request-path', request.nextUrl.pathname);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: ['/api/:path*'],
};이 Proxy는 /api/로 시작하는 요청에만 실행됩니다.
요청 헤더를 변경한 복사본을 다음 라우트 핸들러로 전달합니다.
응답 헤더와 요청 헤더는 용도가 다르므로 어느 방향에 값을 추가하는지 구분해야 합니다.
matcher 범위 설계
matcher는 Proxy가 실행될 경로를 정합니다.
export const config = {
matcher: ['/dashboard/:path*', '/api/admin/:path*'],
};/dashboard/:path*는 대시보드 아래의 모든 페이지를 포함합니다.
/api/admin/:path*는 관리자 API 아래의 모든 라우트를 포함합니다.
이미지와 빌드 산출물까지 모든 요청을 가로채면 불필요한 비용과 예외 처리가 늘어납니다.
보호할 URL 경계를 먼저 정한 뒤 가장 좁은 패턴을 사용합니다.
Auth.js Proxy 연결
인증이 필요한 여러 경로에는 src/auth.ts에서 만든 auth를 Proxy로 내보냅니다.
export { auth as proxy } from '@/auth';
export const config = {
matcher: ['/dashboard/:path*'],
};허용 여부는 중앙 Auth.js 설정의 authorized 콜백에서 판단합니다.
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
session: { strategy: 'jwt' },
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id ?? token.sub!;
token.role = user.role ?? 'member';
}
return token;
},
session({ session, token }) {
session.user.id = token.id;
session.user.role = token.role;
return session;
},
authorized({ auth }) {
return Boolean(auth?.user);
},
},
});로그인하지 않은 페이지 요청은 로그인 화면으로 이동할 수 있습니다.
API 요청은 리다이렉트보다 명확한 401 또는 403 JSON 응답이 필요한 경우가 많습니다.
따라서 이 예제는 관리자 API를 Proxy matcher에 넣지 않고, 라우트 핸들러에서 auth로 요청을 감싸 응답을 직접 구분합니다.
라우트 핸들러에서 최종 검사
Proxy를 통과했다는 사실만 믿고 중요한 API의 검사를 생략하지 않습니다.
먼저 예제를 독립적으로 실행할 수 있도록 신고 게시물 조회 계약을 가진 저장소 대역을 만듭니다.
export type ReportedPostSummary = {
id: string;
title: string;
reportCount: number;
};
const reportedPosts: ReportedPostSummary[] = [
{ id: 'post-1', title: '검토가 필요한 게시물', reportCount: 3 },
];
export async function findReportedPosts(): Promise<ReportedPostSummary[]> {
return reportedPosts;
}운영 코드에서는 같은 반환 계약을 유지한 채 메모리 배열을 데이터베이스 조회로 교체합니다.
import { auth } from '@/auth';
import { findReportedPosts } from '@/repositories/posts';
export const GET = auth(async (request) => {
const user = request.auth?.user;
if (!user) {
return Response.json({ message: '로그인이 필요합니다.' }, { status: 401 });
}
if (user.role !== 'admin') {
return Response.json({ message: '접근 권한이 없습니다.' }, { status: 403 });
}
const posts = await findReportedPosts();
return Response.json(posts);
});이 구조는 Proxy 범위가 바뀌더라도 API 자체의 권한 규칙을 지킵니다.
또한 인증 실패와 권한 부족을 서로 다른 상태 코드로 표현합니다.
리다이렉트와 rewrite
NextResponse.redirect()는 브라우저의 URL을 실제로 바꿉니다.
로그인하지 않은 사용자를 로그인 페이지로 보낼 때 사용할 수 있습니다.
NextResponse.rewrite()는 브라우저 주소를 유지하고 내부에서 다른 페이지나 핸들러를 실행합니다.
점검 화면이나 지역별 콘텐츠를 내부적으로 선택할 때 유용합니다.
API 오류를 로그인 HTML로 rewrite하면 클라이언트가 응답 형식을 오해할 수 있습니다.
API는 상태 코드와 JSON 계약을 유지하는 편이 안전합니다.
Proxy 운영 기준
Proxy에서는 긴 데이터베이스 조회와 외부 API 호출을 피합니다.
실행 경로는 matcher로 좁게 제한합니다.
인증 여부처럼 빠른 진입 판단만 수행하고 복잡한 업무 권한은 데이터 경계에서 확인합니다.
로그에 쿠키와 토큰 원문을 남기지 않습니다.
라우트 핸들러는 Proxy와 독립적으로 401, 403, 입력 오류를 반환할 수 있어야 합니다.
Proxy는 공통 진입 규칙을 단순하게 유지할 때 가장 효과적입니다.