미들웨어 사용
이전 절에서 Next.js API 라우트의 기본 작동 방식과 GET, POST, PUT, DELETE와 같은 HTTP 메서드를 처리하는 방법을 살펴보았습니다. 이제 API 라우트의 기능을 확장하고, 요청 처리 과정을 중앙 집중식으로 관리하며, 보안을 강화하는 데 필수적인 미들웨어(Middleware) 에 대해 알아보겠습니다.
Next.js App Router에서 API 라우트 미들웨어는 별도의 파일(src/middleware.ts
)로 존재하며, 들어오는 모든 요청(또는 특정 패턴의 요청)을 서버 컴포넌트나 API 라우트 핸들러가 처리하기 전에 가로채어 특정 작업을 수행할 수 있도록 합니다. 이는 Express.js나 Koa.js와 같은 Node.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
) 파일에 정의됩니다. 이 파일은 서버 사이드에서만 실행되며, 클라이언트 번들에 포함되지 않습니다.
기본 구조
// src/middleware.ts
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
헬퍼 함수를 사용하면 이를 매우 쉽게 구현할 수 있습니다.
실습: NextAuth.js와 미들웨어를 사용하여 /api/admin
라우트 보호
NextAuth.js 설정 파일 (src/app/api/auth/[...nextauth]/route.ts
)에 authOptions
export:
미들웨어에서 authOptions
를 사용하여 세션 정보를 가져와야 하므로, 이 파일을 다음과 같이 수정하여 authOptions
를 export
합니다.
// src/app/api/auth/[...nextauth]/route.ts
// ... (기존 코드) ...
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 라우트는 미들웨어에 의해 보호되므로, 내부에서는 추가적인 인증/권한 검사 없이 바로 비즈니스 로직을 실행할 수 있습니다.
// src/app/api/admin/data/route.ts
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 });
}
보호된 API 라우트 생성 (예: src/app/api/editor/data/route.ts
)
// 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.rewrite
vsNextResponse.redirect
rewrite
: 클라이언트의 URL은 그대로 유지한 채 내부적으로 다른 경로의 콘텐츠를 렌더링합니다. 주로 특정 조건에서 커스텀 오류 페이지를 보여주고 싶을 때 유용합니다.redirect
: 클라이언트의 URL을 실제로 변경하여 다른 페이지로 이동시킵니다. 로그인되지 않았을 때 로그인 페이지로 보내는 등의 경우에 사용됩니다.
Next.js API 라우트와 미들웨어의 조합은 애플리케이션의 백엔드 로직을 안전하고 효율적으로 관리할 수 있는 강력한 도구를 제공합니다. 이를 통해 복잡한 인증 및 권한 요구사항을 유연하게 처리하고, 코드의 응집성을 높일 수 있습니다.