Next.js: middleware.ts에서 다중 쿠키 추출 및 조작 에러 해결법
- 원인: 미들웨어 안에서
request.cookies.set()이나request.headers.set()을 호출해도, 그 변경이 응답이나 다음 라우트로 자동 전파되지 않음 - 해결: 요청 헤더는
NextResponse.next({ request: { headers } })로, 브라우저에 내려줄 쿠키는 같은 응답의response.cookies.set()으로 각각 명시적으로 설정 - 주의:
Cookie헤더를 통째로 덮어쓰면 세션 토큰 외의 다른 쿠키가 전부 사라짐 — 기존 값에 이어붙여야 함
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.cookies.set('session-token', newSessionToken, { httpOnly: true });
↓ 왜 이런 에러가 나는지 원리 보기
Next.js App Router 환경에서 인증 토큰을 서버 미들웨어(middleware.ts) 수준에서 실시간으로 검증하고 갱신하려다, 새로 설정한 쿠키가 브라우저에 구워지지 않거나 다음 페이지에서 감지되지 않는 문제로 골머리를 앓으신 적이 있을 겁니다. 기존 Node.js 서버나 Route Handler를 짜던 감각으로 res.setHeader() 같은 Express식 객체를 찾거나 next/headers의 cookies().set()을 미들웨어에 그대로 쓰면, 애초에 미들웨어 함수에는 res 인자가 없고 cookies().set()도 미들웨어 밖에서만 허용되는 API라 즉시 에러가 나서 바로 알아챌 수 있습니다. 정작 까다로운 건 NextRequest/NextResponse의 올바른 API(request.cookies.set(), request.headers.set())를 그대로 썼는데도 에러 하나 없이 조용히 쿠키가 유실되는 경우입니다.
왜 미들웨어 안에서는 쿠키 조작이 마음대로 되지 않는가?
원인은 Next.js middleware.ts 파일이 기본적으로 실행되어 온 엣지 런타임(Edge Runtime)의 제약과 관련이 있습니다. 일반적인 서버 컴포넌트(page.tsx)나 Route Handler(route.ts)는 Node.js 풀 런타임 위에서 돌기 때문에 Node.js API를 자유롭게 쓸 수 있지만, 미들웨어는 오랫동안 Node.js 기능을 걷어낸 가벼운 V8 엔진 기반 웹 표준 API만 지원해 왔습니다.
버전 참고: Next.js 15.5부터 미들웨어에서 Node.js 런타임 사용이 정식 지원되었고, Next.js 16부터는 파일명이 proxy.ts로 바뀌면서 Node.js 런타임이 기본값이 되었습니다. 최신 버전을 쓰고 있다면 아래에서 설명하는 엣지 런타임 제약이 기본으로 적용되지 않을 수 있으니, 먼저 package.json의 Next.js 버전과 runtime 설정을 확인하세요.
진짜 문제는 미들웨어 안에서 요청 객체를 수정할 수 없다는 게 아니라, 그 수정이 자동으로 전파되지 않는다는 데 있습니다. request.cookies.set()이나 request.headers.set()을 호출하면 그 실행 컨텍스트 안의 요청 객체 자체는 바뀌지만, 이 변경 내용을 NextResponse.next({ request: { headers } }) 형태로 명시적으로 실어 보내지 않으면 이후 실행되는 페이지 컴포넌트나 API 핸들러로 전달되지 않습니다. 마찬가지로 브라우저에 새 쿠키를 내려주려면 반환하는 응답 객체에 response.cookies.set()을 별도로 호출해야 하며, 다운스트림 전달과 브라우저 응답이라는 이 두 경로는 서로 자동으로 동기화되지 않고 각각 명시적으로 연결해 줘야 합니다.
미들웨어 쿠키 흐름 및 유실 단절 메커니즘
request.cookies.set() 호출 -> 미들웨어 실행 컨텍스트 안에서만 존재 -> NextResponse.next()에 실어 보내지 않으면 다음 라우터/브라우저 어디에도 전달되지 않고 폐기됨.
NextResponse.next({ request: { headers } })로, 브라우저에 내려줄 쿠키는 같은 응답의 response.cookies.set()으로 각각 설정 -> 페이지 및 브라우저까지 정상 연동.
자주 나타나는 상황: 무정지 토큰 리프레시 미들웨어
만료 시간이 15분인 단기 액세스 토큰이 끝날 때마다, 미들웨어가 갱신 API를 뒤에서 호출해 새 쿠키를 클라이언트에 굽고 요청을 통과시키는 무정지 갱신 로직은 흔히 쓰는 패턴입니다.
예전에 외주로 맡았던 결제 대시보드 프로젝트에서 정확히 이 패턴을 썼다가 곤란해진 적이 있습니다. 분명 미들웨어 안에서 쿠키 재발급 로직을 호출했는데도 브라우저에는 신규 쿠키가 주입되지 않아, 다음 페이지로 넘어갈 때 로그아웃 튕김 현상이 연쇄적으로 터졌습니다. 처음엔 토큰 갱신 API 자체가 실패하는 줄 알고 서버 로그부터 뒤졌는데, API는 매번 정상 응답을 주고 있었습니다. 한참 헤매다가 원인이 NextResponse.next()로 다음 라우터에 흐름을 이어주며 만든 응답 객체에, 브라우저로 되돌아가는 최종 응답의 Set-Cookie를 실어 보내지 않았기 때문이라는 걸 알아챘습니다.
요청 헤더 갱신과 응답 쿠키 설정을 하나의 응답 객체 위에서 함께 처리하도록 정리해두면, 토큰 재갱신 흐름에서 이런 로그아웃 튕김 문제를 막을 수 있습니다.
실전 코드: 미들웨어 다중 쿠키 동기화 및 덮어쓰기
Next.js 미들웨어에서 요청 쿠키와 응답 쿠키의 바인딩 유실을 방지하고 데이터를 동기화시키는 미들웨어 전체 작성 가이드 코드 예시입니다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
// 1. 요청(Request) 객체로부터 쿠키 읽어오기
const oldSession = request.cookies.get('session-token')?.value;
// 참고: 실제 서비스에서는 문자열 단순 비교가 아니라 JWT의 exp 클레임을 디코딩하는 등
// 진짜 만료 여부를 검증해야 합니다. 아래 비교는 예시를 위한 단순화입니다.
if (oldSession === 'expired-token-value') {
const newSessionToken = 'refreshed-jwt-secure-token';
// A단계: 기존 요청 헤더를 복제하고, Cookie 헤더는 기존 값에 이어붙여 갱신
// (그냥 덮어쓰면 session-token 외의 다른 쿠키들이 전부 사라짐)
const requestHeaders = new Headers(request.headers);
const otherCookies = (requestHeaders.get('Cookie') ?? '')
.split('; ')
.filter((c) => c && !c.startsWith('session-token='))
.join('; ');
requestHeaders.set(
'Cookie',
[otherCookies, `session-token=${newSessionToken}`].filter(Boolean).join('; ')
);
// B단계: 갱신된 요청 헤더를 실어 응답을 하나만 생성
// (요청 헤더는 이후 서버 컴포넌트/Route Handler에서, 쿠키는 브라우저에서 각각 읽힘)
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// C단계: 브라우저에 내려줄 Set-Cookie는 response.cookies로 직접 설정
// (Set-Cookie 헤더 문자열을 그대로 복사하면 쿠키가 여러 개일 때 유실될 수 있음)
response.cookies.set({
name: 'session-token',
value: newSessionToken,
path: '/',
httpOnly: true,
secure: true,
sameSite: 'strict',
});
return response;
}
return NextResponse.next();
}
// 미들웨어 감시 제외 경로 (정적 리소스 필터링)
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
};
해결 코드의 라인별 동작 해설
위 해결 코드의 핵심은 요청 헤더 전파와 응답 쿠키 설정을 서로 다른 API로, 하지만 하나의 응답 객체 안에서 함께 처리하는 것입니다. request.cookies.set()이나 request.headers.set()으로 미들웨어 안의 요청 객체를 고쳐도, 그 변경 사항 자체는 어디에도 자동으로 전달되지 않습니다. NextResponse.next({ request: { headers } })에 명시적으로 실어 넘겨줘야만 이후 실행되는 page.tsx나 Route Handler의 cookies()/headers()에서 이 값을 읽을 수 있습니다.
이때 Cookie 헤더는 기존 값을 통째로 덮어쓰지 않고 이어붙여야 합니다. 그렇지 않으면 세션 토큰 외에 요청에 실려 있던 다른 쿠키(로케일, CSRF 토큰 등)가 전부 사라져 다운스트림 로직이 깨질 수 있습니다. 브라우저로 내려가는 Set-Cookie는 응답 헤더를 문자열로 복사하는 대신 response.cookies.set()을 직접 호출해서 설정합니다. Set-Cookie는 동시에 여러 개가 존재할 수 있는 헤더라, 문자열 하나로 합쳐서 복사하면 쿠키가 두 개 이상일 때 유실될 위험이 있기 때문입니다.
정리하면 미들웨어에서 요청 헤더만 고쳐서는 브라우저에도, 뒤이은 서버 컴포넌트에도 반영되지 않습니다. 응답 헤더까지 함께 동기화해야 양쪽 모두에 안전하게 반영됩니다. 그리고 최신 Next.js를 쓰고 있다면, 애초에 이 글에서 설명한 엣지 런타임 제약 자체가 기본으로 걸리지 않을 수 있다는 점도 함께 확인하시길 권합니다.
Next.js 미들웨어의 쿠키 다중 설정 및 수정 명세, 그리고 Next.js 16의 proxy.ts 전환 안내는 다음 공식 문서에서 확인하실 수 있습니다.
미들웨어(middleware.ts, Next.js 16부터는 proxy.ts)에서 양방향 쿠키 세팅을 수동으로 맞춰주는 패턴은 세션 인증 제어의 기본기입니다. JWT 같은 인증 방식의 기초부터 정리한 글은 [호마다의 IT 개발 입문 블로그] 에서 확인하실 수 있습니다.
댓글
댓글 쓰기