📅 2025-12-11
➡️ Middleware, NextRequest/NextResponse, Auth, Cookie에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
요청(Request)이 실제 페이지 또는 API Route에 도달하기 전에 먼저 실행되는 함수
즉 라우팅 전에 선별 작업을 하는 입구 관리자 같은 역할
예시
권한 체크
→ 로그인 안 한 사용자가 /mypage 접근 시 /sign-in으로 보내기
요청 차단
→ 특정 User-Agent, 특정 국가 IP 차단
경로 재작성(Rewrite) / 리다이렉트(Redirect)
→ /old → /new
middleware.ts 생성// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
export const config = {
matcher: '/about/:path*',
};
middleware에서 사용하는 두 가지 핵심 객체
| 객체 | 역할 |
|---|---|
| NextRequest | 들어온 요청 정보를 읽는 객체 |
| NextResponse | 요청을 어떻게 처리할지 결정하는 응답 객체 |
둘 다 표준 Web Request/Response 기반이지만, Next.js에서 미들웨어용으로 기능이 확장된 버전
NextResponse 4가지 패턴
// 1) 정상 진행
return NextResponse.next();
// 2) 다른 URL로 리다이렉트
return NextResponse.redirect(new URL("/about", request.url));
// 3) URL은 그대로 두고, 다른 페이지 컴포넌트 실행 (rewrite)
return NextResponse.rewrite(new URL("/faq", request.url));
// 4) JSON 바로 응답 (주로 API 권한 체크에 사용)
return NextResponse.json({ message: "권한이 없습니다." });
예시
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url));
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url));
}
}
주요 특징
request.nextUrl → pathname, params, searchParams 쉽게 가져오기request.cookies → 쿠키 읽기예시
export function middleware(request: NextRequest) {
const { pathname, searchParams } = request.nextUrl;
const token = request.cookies.get("token")?.value;
const lang = request.cookies.get("lang")?.value ?? "ko";
console.log("요청 경로:", pathname);
console.log("쿼리:", searchParams.toString());
console.log("토큰:", token);
console.log("언어 설정:", lang);
return NextResponse.next();
}
| 구분 | 설명 |
|---|---|
| 표준 Request | 브라우저 fetch, Web API에서 쓰는 기본 요청 객체 URL/헤더/body 조회용 읽기 전용 |
| NextRequest | Next.js 확장 요청 객체 nextUrl, cookies, IP 등 프레임워크 친화적 기능 추가 주로 middleware와 route handler에서 사용 |
| 표준 Response | new Response()로 생성하는 기본 응답 객체상태코드/헤더/body만 제공 라우팅 제어 없음 |
| NextResponse | Next.js 확장 응답 객체 next/redirect/rewrite/json, 쿠키 설정 등 라우팅 + 응답 제어에 특화 |
→ 즉, 미들웨어에선 사실상 NextRequest / NextResponse가 표준처럼 쓰임
미들웨어가 어떤 경로에서만 실행될지 지정하는 옵션
단일 경로
export const config = {
matcher: '/about/:path*',
};
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
};export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};❗ matcher는 빌드 타임에 결정됨
→ 따라서 동적 값 사용 불가, 동적 매칭은 무시됨
| 개념 | 의미 |
|---|---|
| Authentication (인증) | 사용자가 누구인지 확인하는 과정 |
| Authorization (인가) | 인증된 사용자가 해당 기능/페이지에 접근할 권한이 있는지 검증 |
OAuth / OpenID Connect (OIDC)
사용자 자격 증명 기반 로그인
비밀번호 없는 인증 (토큰 기반)
json-server 위에 인증 기능을 추가한 버전설치 및 설정
npm i json-server-auth -Dauth-db.json 작성{
"users": [
{
"email": "test@test.com",
"password": "$2a$10$eHUzzsY1tbOmyGVqPwVEsuYRQb4LP2Hw/dMXgxp5p8eYE4UgcZMA.", // password
"id": 1
}
]
}package.json)"auth": "json-server-auth --watch auth-db.json --port 8000"npm run auth로 실행쿠키의 대표적 사용
| 위치 | 읽기 | 쓰기 |
|---|---|---|
| 서버 컴포넌트 / 라우트 핸들러 | cookies() | response.cookies.set() |
| 미들웨어 | request.cookies | NextResponse().cookies.set() |
// app/api/me/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const cookieStore = cookies();
const accessToken = cookieStore.get("accessToken"); // 없으면 undefined
console.log("accessToken:", accessToken);
return NextResponse.json({
loggedIn: !!accessToken,
});
}
cookies().get(name) → 없으면 undefined// app/api/auth/login/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const payload = await request.json();
const res = await fetch("http://localhost:8000/signin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
// data.accessToken 이 있다고 가정
const response = NextResponse.json({ success: true });
response.cookies.set("accessToken", data.accessToken, {
httpOnly: true,
path: "/",
// 선택 옵션들:
// maxAge: 60 * 60 * 24, // 1일 (초 단위)
// secure: true, // https에서만 전송
// sameSite: "lax", // CSRF 방지에 도움
});
return response;
}
자주 쓰는 쿠키 옵션
| 옵션 | 설명 |
|---|---|
httpOnly | JS로 읽기 불가능 → XSS 방지 |
secure | HTTPS에서만 전송 |
path | 해당 경로 요청 시에만 쿠키 전송. 전체 적용은 "/" |
maxAge | 쿠키 유지 시간(초 단위) |
sameSite | CSRF 공격 방지에 도움 |
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const res = NextResponse.next();
// 예: 처음 방문한 시간 기록
if (!request.cookies.get("firstVisitAt")) {
res.cookies.set("firstVisitAt", new Date().toISOString(), {
httpOnly: true,
});
}
return res;
}
export const config = {
matcher: "/:path*",
};
request.cookies → 읽기 전용NextResponse 객체에 set