NextResponse는 HTTP 응답을 생성하고 조작하는 데 사용되는 클래스이고,
NextRequest는 HTTP 요청을 나타내며, 요청의 다양한 속성에 접근할 수 있게 해줍니다.
요컨대 어떠한 통신을 해야할때 미들웨어를 거치니까 그 사이에 필요한 것들을 하면 된다. NextRequest로 속성을 조작하고 NextResponse로 응답을 보낸다!
next에서 클라이언트와 서버간 통신을 할때 순서는 다음과 같다.
1) 클라이언트 요청: 클라이언트가 서버에 HTTP 요청을 보냅니다.
2) NextRequest 처리: 서버는 NextRequest 객체를 사용하여 요청의 속성을 확인하고 필요한 경우 조작합니다.
3) NextResponse 생성: 서버는 NextResponse 객체를 사용하여 클라이언트에 보낼 응답을 생성합니다.
4) 응답 전송: 생성된 NextResponse 객체를 클라이언트에 반환하여 요청에 대한 응답을 완료합니다.
NextRequest: 클라이언트로부터 들어오는 HTTP 요청을 나타내며, 이 객체를 통해 요청의 속성(예: URL, 메서드, 헤더, 쿠키 등)을 조작하거나 확인할 수 있습니다. 즉, 요청에 대한 정보를 얻고 필요한 경우 요청을 수정할 수 있습니다.
NextResponse: 서버에서 클라이언트로 응답을 보내는 데 사용되는 객체입니다. 이 객체를 통해 응답을 생성하고, 응답의 상태 코드, 헤더, 본문 등을 설정할 수 있습니다. 즉, 요청에 대한 적절한 응답을 생성하여 클라이언트에 반환하는 역할을 합니다.
즉 NextRequest에서 쿠키 값을 읽어서 유무를 체크하고
NextResponse로 쿠키와 헤더를 설정하면 된다.
예시를 보면 NextResponse.next() 응답의 헤더로 cookie를 set한다고 나와있음
https://nextjs.org/docs/app/api-reference/functions/next-response
set(name, value)
Given a name, set a cookie with the given value on the response.
// Given incoming request /home
let response = NextResponse.next()
// Set a cookie to hide the banner
response.cookies.set('show-banner', 'false')
// Response will have a `Set-Cookie:show-banner=false;path=/home` header
return response
get(name)
Given a cookie name, return the value of the cookie. If the cookie is not found, undefined is returned. If multiple cookies are found, the first one is returned.
// Given incoming request /home
let response = NextResponse.next()
// { name: 'show-banner', value: 'false', Path: '/home' }
response.cookies.get('show-banner')
그리고 next()는 미들웨어를 끊거나 설정한 헤더, 쿠키 등을 반환할때 쓰인다.
스터디 더 하고 짤걸 공식 코드 가져다 붙이고 왜 안되나 하고있었네~
NextRequest.cookies.get과 NextResponse.cookies.get의 차이를 이해하지 못해서 그런 것 같다.
전자는 클라이언트에서 쿠키를 가져오고 후자는 response에 설정한 쿠키를 읽어오는 것이다.
**
[발단]
공통 fetch의 header에 accessToken을 심어야했다.
그리고 미들웨어에서 통신할때마다 토큰을 새로 발급받는다.(???)
미들웨어에서 response로 저장한 쿠키를 클라이언트사이드에서 접근할 수 없어 쿠키에 접근해 로컬 스토리지에 저장하는 api route를 만들고, 데이터 통신마다 getToken api를 거치는게 진짜 대박 별로였다.
특히 localstorage는 만료 시간을 설정할 수 없어서 value 저장시 만료 시간을 포함한 객체 형태로 저장할까 생각도 했는데 그것도 번거로운거 같아 생각만.. 했다.
하지만 로컬스토리지에 저장된 토큰은 만료가 되든 말든 남아있기때문에(싸늘) 통신을 할때 유효한지안한지 모르겠고~ 일단 던져보고 유효하지 않으면 재발급해보고 그거도 안되면 리프레시토큰 포함해서 재발급하고...
다시 로컬스토리지에 저장하는 api를 타고 ... 를 반복하는게 너무 불필요한 과정이라고 생각했다.(당연함)
불필요한 통신이 네트워크에 쌓여가다보면 속도 저하에도 영향을 줄거고 미들웨어를 더 잘 쓸 수 있을 것 같은데(공통 fetch 함수가 점점 길어지고있다.) 이게 최선인가...? 라는 부채감이 쌓였기 때문에 새벽에 잠도 못자고 무한 구글링과 gpt와 어쨋든 지구촌 모두의 힘을 모아모아
1) 미들웨어에서 쿠키의 토큰 체크
2) 액세스 토큰이 있을 경우 헤더에 담아 통신
3) 액세스 토큰이 없을 경우 갱신
4) 리프레쉬 토큰이 없을 경우 새로 발급
의 모든 통신 과정을 미들웨어에서 처리하였다.
그리고 리팩토링을 gpt에게 맡겼는데 반복되는 cookie set 등 공통 코드의 분리를 깔끔하게 빼줘서 맘에 든다. 나도 공통으로 사용되는 함수를 효율적으로 작성하기 위해 분리한다고 하긴하는데 젠장~
어쨌든 테스트를 위해 nextjs api route로
토큰 발행, decode util,
백엔드 api endpoint를 간이로 만들었는데 코드는 다음과 같다.
(원래 목표는 미들웨어 개선이었는데 테스트하려다보니 이것저것 추가함)
그래도 next에서 해서 진짜 빠르고 편하게 했다. 서버사이드 짱짱

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// header 적용 예외 path
const publicPaths = ['/auth/refresh'];
// 공통 fetch
const fetchWithErrorHandling = async (url: string, options: RequestInit) => {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error('Request failed');
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
return null;
}
};
// 리프레쉬 토큰으로 액세스 토큰 발행
const refreshAccessToken = (refreshToken: string) => {
return fetchWithErrorHandling(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
};
// 리프레쉬, 액세스 토큰 둘 다 없을 경우 신규 발행
const getToken = async () => {
const data = await fetchWithErrorHandling(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 'testID' }),
});
if (data) {
return { accessToken: data.accessToken, refreshToken: data.refreshToken };
}
return { accessToken: null, refreshToken: null };
};
// 커스텀 헤더(accessToken)
const setAuthorizationHeader = (request: NextRequest, token: string) => {
const requestHeaders = new Headers(request.headers);
requestHeaders.set('Authorization', `Bearer ${token}`);
return requestHeaders;
};
// 쿠키 저장
const setCookies = (response: NextResponse, tokens:any) => {
response.cookies.set('accessToken', tokens.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 20,
});
if (tokens.refreshToken) {
response.cookies.set('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60,
});
}
};
export const middleware = async (request: NextRequest) => {
const path = request.nextUrl.pathname;
// 예외처리
if (publicPaths.some(publicPath => path.includes(publicPath))) {
return NextResponse.next();
}
try {
const accessToken = request.cookies.get('accessToken')?.value;
const refreshToken = request.cookies.get('refreshToken')?.value;
// 엑세스 토큰 만료시 재발급
if (!accessToken && refreshToken) {
const res = await refreshAccessToken(refreshToken);
if (res.accessToken) {
const response = NextResponse.next({
request: {
headers: setAuthorizationHeader(request, res.accessToken),
},
});
setCookies(response, { accessToken: res.accessToken });
return response;
}
}
// 리프레시 토큰이 없다면 전체 토큰 재발급
if (!refreshToken) {
const { accessToken: at, refreshToken: rt } = await getToken();
const response = NextResponse.next({
request: {
headers: setAuthorizationHeader(request, at),
},
});
setCookies(response, { accessToken: at, refreshToken: rt });
return response;
}
return NextResponse.next({
request: {
headers: setAuthorizationHeader(request, accessToken as string),
},
});
} catch (error) {
console.error('Middleware error:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
};
export const config = {
matcher: ['/((?!_next/static|favicon.ico).*)'],
};
이건 리팩토링 전 코드
cookie set과 header set이 반복되서 40줄 정도 많다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const publicPaths = [
'/auth/refresh'
]
const refreshAccessToken = async (refreshToken: string) => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
})
if (!response.ok) throw new Error('Token refresh failed')
const data = await response.json()
return data.accessToken
} catch (error) {
console.error('Error refreshing token:', error)
return null
}
}
const getToken = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: 'testID' }),
})
if (!response.ok) {
throw new Error('유효한 아이디가 아님')
} else {
const data = await response.json();
return data;
}
} catch (error) {
console.error('Error refreshing token:', error)
return null
}
}
export const middleware = async (request: NextRequest) => {
const path = request.nextUrl.pathname
if (publicPaths.some(publicPath => path.includes(publicPath))) {
return NextResponse.next()
}
try {
let accessToken = request.cookies.get('accessToken')?.value;
const refreshToken = request.cookies.get('refreshToken')?.value;
if (!accessToken && refreshToken) {
const newAccessToken = await refreshAccessToken(refreshToken)
if (newAccessToken) {
// 요청 헤더 설정
const requestHeaders = new Headers(request.headers);
requestHeaders.set('Authorization', `Bearer ${newAccessToken}`);
// NextResponse 객체 생성
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.cookies.set({
name: 'accessToken',
value: newAccessToken,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 20
})
return response;
}
}
if (!refreshToken) {
const { accessToken: at, refreshToken: rt } = await getToken();
// 요청 헤더 설정
const requestHeaders = new Headers(request.headers);
requestHeaders.set('Authorization', `Bearer ${at}`);
// NextResponse 객체 생성
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// 쿠키 설정
response.cookies.set('accessToken', at, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 20
});
response.cookies.set('refreshToken', rt, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60
});
// 응답 반환
return response; // 쿠키가 설정된 응답 객체 반환
}
const requestHeaders = new Headers(request.headers);
requestHeaders.set('Authorization', `Bearer ${accessToken}`);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
console.error('Middleware error:', error)
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
export const config = {
matcher: [
'/((?!_next/static|favicon.ico).*)',
]
}
아래에서부턴 테스트용 api route
재발급할때 헤더에 refresh token이 잘 담겨가는지 테스트해야해서 만들었다.
jwt 라이브러리 설치하고 예제코드 가져다가 검증(verify), 생성(sign) 등등 해주고 json 형태로 반환한다.
서버사이드 응답이니까 NextResponse로 줘야함
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
export async function POST(req: NextRequest) {
try {
const { refreshToken } = await req.json();
if (!refreshToken) {
return NextResponse.json(
{ message: 'Refresh token is required' },
{ status: 400 }
);
}
// Refresh 토큰 검증
let decoded: any;
try {
decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string);
} catch (error) {
return NextResponse.json(
{ message: 'Invalid or expired refresh token' },
{ status: 401 }
);
}
// 새로운 액세스 토큰 생성
const accessToken = jwt.sign(
{ id: decoded.id },
process.env.JWT_SECRET as string,
{ expiresIn: process.env.JWT_ACCESS_EXPIRY || '15m' }
);
return NextResponse.json({
accessToken,
});
} catch (error) {
console.error('Error generating access token:', error);
return NextResponse.json(
{ message: 'Failed to generate access token' },
{ status: 500 }
);
}
}
통신이 양호한지 확인하기 위해 두개를 만들었다.
하나는 호출 성공이면 나오는 메시지이고 다른 하나는 아이디와 토큰을 반환한다.
토큰이 유효하지 않으면 냅다 401을 던지고 서버 에러는 500을 던지는 내용
토큰이 유효하면 200, 텍스트 데이터를 보낸다. data.data로 감싼건 지금 다시보니 왜 저렇게 했지 싶지만 넘어가주세용
// app/api/main/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/util/jwtDecode';
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({
code: 401,
message: 'Invalid authorization header format',
data: null
}, { status: 401 });
}
const token = authHeader.split(' ')[1];
try {
// 토큰 검증
const decoded:any = await verifyToken(token);
return NextResponse.json({
code: 200,
message: 'success',
data: {
data: `데이터 통신 성공!!`
}
});
} catch (error) {
if (error instanceof Error) {
if (error.message === 'Token has expired') {
return NextResponse.json({
code: 401,
message: 'Token has expired',
data: null
}, { status: 401 });
}
}
return NextResponse.json({
code: 401,
message: 'Invalid token',
data: null
}, { status: 401 });
}
} catch (error) {
console.error('API Error:', error);
return NextResponse.json({
code: 500,
message: 'Internal server error',
data: null
}, { status: 500 });
}
}
서버 api 엔드포인트에서 사용하는 액세스 토큰 유효 체크 함수
import jwt from 'jsonwebtoken';
export async function verifyToken(token: string) {
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.JWT_SECRET as string, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
});
}
sso id 값이 요청되니까 id 값이 있다는 전제하에 작성
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
export async function POST(req: NextRequest) {
try {
const { id } = await req.json();
if (!id) {
return NextResponse.json(
{ message: 'ID is required' },
{ status: 400 }
);
}
const accessToken = jwt.sign(
{ id },
process.env.JWT_SECRET as string,
{ expiresIn: process.env.JWT_ACCESS_EXPIRY || '15m' }
);
const refreshToken = jwt.sign(
{ id },
process.env.JWT_REFRESH_SECRET as string,
{ expiresIn: process.env.JWT_REFRESH_EXPIRY || '7d' }
);
return NextResponse.json({
accessToken,
refreshToken,
});
} catch (error) {
console.error('Token generation error:', error);
return NextResponse.json(
{ message: 'Token generation failed' },
{ status: 500 }
);
}
}
버튼을 클릭하면 id와 액세스토큰을 렌더링함
"use client"
import React, { useEffect, useState } from 'react';
import { dataGet } from '@/util/dateFetch';
export default function Home() {
const [data, setData] = useState<any>({});
const [isVisible, setIsVisible] = useState<any>(false);
useEffect(() => {
getData();
}, [])
const getData = async () => {
const response = await dataGet('/api/main')
if (response.code != 200) {
console.error(response.message)
} else {
setData({'main' : response.data})
}
}
const getTokenData = async () => {
const response = await dataGet('/api/viewToken')
if (response.code != 200) {
console.error(response.message)
} else {
setData((prev:any) => ({...prev, sub: response.data}));
setIsVisible(true)
}
}
return (
<>
{Object.keys(data).length > 0 ? (
<section>
<p>{data.main.data}</p>
<button onClick={getTokenData}>데이터 보기</button>
{isVisible && (
<ul>
<li><b>id : </b>{data.sub.id}</li>
<li><b>token : </b>{data.sub.token}</li>
</ul>
)}
</section>
) : (
<>데이터 로딩중</>
)}
</>
);
}