미들웨어 사용
이전 절에서는 Next.js API 라우트 기본 동작과 GET/POST/PUT/DELETE 메서드 처리 방식을 살펴봤습니다.
이제는 API 라우트를 확장하고, 요청 처리를 중앙에서 관리하며 보안을 강화하는 핵심 도구인 미들웨어(Middleware)를 다루겠습니다.
App Router에서는 API 라우트 미들웨어를
별도 파일(src/middleware.ts)로 정의합니다.
이 미들웨어는 요청을 핸들러에 넘기기 전에 가로채
공통 작업을 수행할 수 있게 해줍니다.
개념적으로는 Express.js나 Koa.js의 미들웨어와 유사합니다.
API 라우트에서 미들웨어의 역할
미들웨어는 API 라우트가 실제 비즈니스 로직을 실행하기 전에 다양한 선행 작업을 수행할 수 있도록 합니다. 주요 역할은 다음과 같습니다.
- 인증 및 권한 부여: 로그인된 사용자만 특정 API에 접근하도록 하거나, 특정 역할을 가진 사용자만 접근하도록 제어합니다. (가장 일반적인 사용 사례)
- 로깅: 모든 API 요청에 대한 로그를 기록하여 디버깅, 모니터링, 감사 추적에 활용합니다.
- 요청 유효성 검사: 요청 헤더, 쿼리 파라미터, 본문 등의 유효성을 사전에 검증하여 잘못된 요청을 차단합니다.
- CORS (Cross-Origin Resource Sharing) 처리: 다른 도메인에서의 API 요청을 허용하거나 거부하는 정책을 설정합니다.
- 데이터 파싱 및 변환: 요청 본문을 특정 형식으로 파싱하거나, 데이터를 가공하는 등의 전처리 작업을 수행합니다.
- 속도 제한 (Rate Limiting): 특정 IP 주소나 사용자로부터 오는 요청 수를 제한하여 서비스 남용을 방지합니다.
- 헤더 조작: 응답 헤더를 추가, 수정, 삭제하여 캐싱 정책이나 보안 관련 설정을 적용합니다.
Next.js App Router에서의 미들웨어 설정
Next.js App Router의 미들웨어는 프로젝트의 루트에 있는 src/middleware.ts (또는 .js) 파일에 정의됩니다. 이 파일은 서버 사이드에서만 실행되며, 클라이언트 번들에 포함되지 않습니다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. 요청 처리 로직
console.log('미들웨어: 요청이 들어왔습니다.', request.url);
// 2. 응답 반환 또는 다음 요청 처리
// NextResponse.next(): 요청을 다음 미들웨어 또는 해당 라우트 핸들러로 전달
return NextResponse.next();
// 특정 조건 시 리다이렉트
// return NextResponse.redirect(new URL('/login', request.url));
// 특정 조건 시 응답 반환 (라우트 핸들러로 전달되지 않음)
// return new NextResponse('권한이 없습니다.', { status: 401 });
}
// 미들웨어가 적용될 라우트 경로를 정의합니다.
// matcher는 정규 표현식으로 여러 경로를 지정할 수 있습니다.
export const config = {
matcher: [
'/api/:path*', // /api/로 시작하는 모든 경로
'/dashboard/:path*', // /dashboard로 시작하는 모든 경로
],
};middleware 함수
request: NextRequest: 들어오는 HTTP 요청에 대한 정보를 담고 있는 객체입니다.-
반환 값
NextResponse.next(): 현재 요청을 다음 미들웨어 체인으로 전달하거나, 더 이상 미들웨어가 없으면 해당 라우트 핸들러(API 라우트 또는 페이지)로 전달합니다.NextResponse.redirect(url): 요청을 다른 URL로 리다이렉트합니다.NextResponse.rewrite(url): 클라이언트의 URL은 변경하지 않고 내부적으로 다른 경로의 콘텐츠를 렌더링합니다. (주로 인증/권한 없는 경우 커스텀 에러 페이지 렌더링 시 활용)new NextResponse(body, init): 요청을 중단하고 직접 응답을 반환합니다 (예: 401 Unauthorized, 404 Not Found 등).
config 객체
matcher: 미들웨어가 실행될 경로를 지정합니다. 배열 안에 문자열 또는 정규 표현식을 사용하여 여러 경로 패턴을 정의할 수 있습니다./api/:path*:/api/로 시작하는 모든 경로를 포함합니다./users/:id:/users/123과 같이 특정 동적 경로를 포함합니다./((?!_next|static|public).*): 특정 경로(_next,static,public등)를 제외한 모든 경로를 포함합니다.
미들웨어를 활용한 API 라우트 보호
가장 일반적인 미들웨어 사용 사례는 API 라우트에 대한 인증 및 권한 부여입니다. NextAuth.js의 withAuth 헬퍼 함수를 사용하면 이를 매우 쉽게 구현할 수 있습니다.
/api/admin 라우트 보호
NextAuth.js 설정 파일 (src/app/api/auth/[...nextauth]/route.ts)에 authOptions export:
미들웨어에서 authOptions를 사용하여 세션 정보를 가져와야 하므로, 이 파일을 다음과 같이 수정하여 authOptions를 export합니다.
// ... (기존 코드) ...
const authOptions = {
// ... (기존 설정: providers, session, callbacks 등) ...
};
const handler = NextAuth(authOptions);
// GET 및 POST 핸들러와 함께 authOptions도 export
export { handler as GET, handler as POST, authOptions };src/middleware.ts 파일 수정 (NextAuth.js withAuth 사용)// src/middleware.ts
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';
// withAuth는 NextAuth.js 세션을 요청 객체에 추가하고,
// 인증되지 않은 사용자를 signIn 페이지로 리다이렉트하는 기능을 제공합니다.
export default withAuth(
// 이 함수는 인증된 요청에 대해서만 실행됩니다.
// 토큰(`req.nextauth.token`)에 사용자의 역할 정보가 포함되어 있습니다.
async function middleware(req) {
const token = req.nextauth.token; // 토큰에 접근
const pathname = req.nextUrl.pathname;
console.log('미들웨어 (인증 후):', pathname, '토큰:', token?.email, '역할:', token?.role);
// 예시: /api/admin 경로에 대해 'admin' 역할만 허용
if (pathname.startsWith('/api/admin')) {
if (token?.role !== 'admin') {
console.log('미들웨어: admin 권한 없음 -> 403 Forbidden');
// 'Forbidden' 응답 (클라이언트에게 권한 없음을 명시)
return new NextResponse(JSON.stringify({ message: '접근 권한이 없습니다.' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
}
// 예시: /api/editor 경로에 대해 'admin' 또는 'editor' 역할만 허용
if (pathname.startsWith('/api/editor')) {
if (token?.role !== 'admin' && token?.role !== 'editor') {
console.log('미들웨어: editor 권한 없음 -> 403 Forbidden');
return new NextResponse(JSON.stringify({ message: '접근 권한이 없습니다.' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
}
// 그 외의 경우, 요청을 계속 처리합니다.
return NextResponse.next();
},
{
// 인증되지 않은 사용자를 위한 설정
pages: {
signIn: '/api/auth/signin', // NextAuth.js가 제공하는 로그인 페이지로 리다이렉트
},
// `authorized` 콜백은 요청을 계속 진행할지 여부만 결정합니다.
// 여기서는 토큰이 존재하면(즉, 인증되면) 요청을 진행하도록 설정합니다.
// 실제 역할 검사는 위의 `middleware` 함수에서 수행됩니다.
callbacks: {
authorized: ({ token }) => !!token, // 토큰이 있으면 true (인증됨)
},
}
);
// 미들웨어가 적용될 경로를 정의합니다.
export const config = {
matcher: [
'/api/admin/:path*', // '/api/admin'으로 시작하는 모든 API 경로
'/api/editor/:path*', // '/api/editor'로 시작하는 모든 API 경로
// 여기에 보호할 다른 API 라우트 경로를 추가합니다.
// 미들웨어로 페이지 라우트도 보호할 수 있습니다 (예: '/dashboard/:path*')
],
};보호된 API 라우트 생성 (예: src/app/api/admin/data/route.ts):
이 API 라우트는 미들웨어에 의해 보호되므로, 내부에서는 추가적인 인증/권한 검사 없이 바로 비즈니스 로직을 실행할 수 있습니다.
import { NextRequest, NextResponse } from 'next/server';
// 이 API는 미들웨어에 의해 'admin' 역할만 접근 가능하도록 보호됩니다.
export async function GET(request: NextRequest) {
// 미들웨어를 통과했다면, 요청자는 'admin' 권한을 가진 것으로 간주합니다.
const adminData = {
message: '환영합니다, 관리자님! 중요한 관리자 데이터입니다.',
usersCount: 100,
pendingApprovals: 5,
};
return NextResponse.json(adminData, { status: 200 });
}
export async function POST(request: NextRequest) {
const body = await request.json();
console.log('관리자 데이터 생성 요청:', body);
return NextResponse.json({ message: '관리자 데이터 생성 성공', data: body }, { status: 201 });
}src/app/api/editor/data/route.ts)import { NextRequest, NextResponse } from 'next/server';
// 이 API는 미들웨어에 의해 'admin' 또는 'editor' 역할만 접근 가능하도록 보호됩니다.
export async function GET(request: NextRequest) {
const editorData = {
message: '환영합니다, 편집자님! 편집 가능한 데이터입니다.',
articlesCount: 50,
drafts: 10,
};
return NextResponse.json(editorData, { status: 200 });
}NextAuth.js 설정이 완료되어 있고, src/app/api/auth/[...nextauth]/route.ts에서 authOptions를 export했는지 먼저 확인합니다.
또한 callbacks에서 사용자 역할(예: admin@example.com은 admin, editor@example.com은 editor, 나머지는 user)을 세션에 추가하도록 설정했는지 함께 점검합니다.
src/middleware.ts 파일을 위 예시와 같이 수정합니다.
src/app/api/admin/data/route.ts 및 src/app/api/editor/data/route.ts 파일을 생성합니다.
개발 서버(npm run dev)를 실행합니다.
-
로그아웃 상태
http://localhost:3000/api/admin/data또는http://localhost:3000/api/editor/data에 접근하면, NextAuth.js 로그인 페이지로 리다이렉트되는지 확인합니다.
-
일반 사용자 (
user역할)로 로그인 (예: GitHub 로그인)http://localhost:3000/api/admin/data에 접근하면403 Forbidden응답 (접근 권한이 없습니다.)을 받는지 확인합니다.http://localhost:3000/api/editor/data에 접근하면403 Forbidden응답을 받는지 확인합니다.
-
편집자 (
editor역할)로 로그인 (예:editor@example.com으로 설정한 계정)http://localhost:3000/api/editor/data에 접근하여 데이터를 성공적으로 받는지 확인합니다.http://localhost:3000/api/admin/data에 접근하면403 Forbidden응답을 받는지 확인합니다.
-
관리자 (
admin역할)로 로그인 (예:admin@example.com으로 설정한 계정)http://localhost:3000/api/admin/data에 접근하여 데이터를 성공적으로 받는지 확인합니다.http://localhost:3000/api/editor/data에 접근하여 데이터를 성공적으로 받는지 확인합니다.
미들웨어 사용 시 고려사항 및 팁
- 성능: 미들웨어는 모든 요청에 대해 실행되므로, 미들웨어 내에서 시간이 많이 소요되는 작업(예: 복잡한 데이터베이스 쿼리, 외부 API 호출)은 최소화해야 합니다. 꼭 필요한 경우에만 실행되도록
matcher를 잘 구성하는 것이 중요합니다. - 오류 처리: 미들웨어에서 오류가 발생하면 사용자에게 적절한 HTTP 상태 코드와 메시지를 반환해야 합니다.
- 토큰 정보 접근:
req.nextauth.token은 NextAuth.js의 JWT 세션 전략을 사용할 때만 유효합니다. 데이터베이스 세션 전략을 사용하는 경우, 미들웨어에서 직접 세션 정보를 가져와야 하거나,authorized콜백을 통해 더 복잡한 로직을 구현해야 할 수 있습니다. - 미들웨어 체이닝: Next.js App Router의 미들웨어는 단일 함수로 정의되므로, 여러 개의 미들웨어 함수를 순서대로 실행하고 싶다면, 해당 함수들을 하나의
middleware.ts파일 내에서 조합해야 합니다. (예:compose함수를 만들어 미들웨어들을 엮는 방식). - 라우트 핸들러 내에서의 추가 검증: 미들웨어에서 대부분의 인증/권한 검사를 수행하더라도, 중요한 API 라우트에서는 혹시 모를 상황에 대비하여 라우트 핸들러 내부에서
getServerSession을 통해 한 번 더 검증하는 것이 안전합니다. -
NextResponse.rewritevsNextResponse.redirectrewrite: 클라이언트의 URL은 그대로 유지한 채 내부적으로 다른 경로의 콘텐츠를 렌더링합니다. 주로 특정 조건에서 커스텀 오류 페이지를 보여주고 싶을 때 유용합니다.redirect: 클라이언트의 URL을 실제로 변경하여 다른 페이지로 이동시킵니다. 로그인되지 않았을 때 로그인 페이지로 보내는 등의 경우에 사용됩니다.
Next.js API 라우트와 미들웨어의 조합은 애플리케이션의 백엔드 로직을 안전하고 효율적으로 관리할 수 있는 강력한 도구를 제공합니다. 이를 통해 복잡한 인증 및 권한 요구사항을 유연하게 처리하고, 코드의 응집성을 높일 수 있습니다.