오늘은 Next.js에서 사용하는 미들웨어에 대해 이야기해 보려고 한다.
이유는 우리팀의 아주 잘생긴 동료 개발자분이 있는데
"범수님 미들웨어가 뭐에요?"
이렇게 말씀하셨다.
이때 나는 정확하게 알지 못했기 때문에 설명을 제대로 드릴 수 없었다. 하지만 어떤 느낌인지 왜 쓰는진 알고 있었는데,,,
왜 말을 못 할까? 내 자신이 답답하고 멍청이였따.
그래서 한번 복습할 겸 velog에 작성한 미들웨어에 대해 정리해 보려고 한다.
미들웨어란?
미들웨어는 서로 다른 애플리케이션이 통신할 때 중간에서 역할을 해주는 소프트웨어다.
한 줄로 간단히 요약하면 이렇게 설명할 수 있다.
그러면 어떤 느낌인지 살짝 알게 되었으니 Next.js에서 미들웨어는 어떤 역할을 할까?
클라이언트에서 요청이 서버에 도달하기 전에 수행되는 소프트웨어라고 볼 수 있다.
내가 미들웨어를 사용하는 주요 목적은
1. 인증(authentication)
2. 리다이렉트(redirect)
이다.
한번 작성한 코드를 보면서 다시 한번 곱씹어 보자
export default function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const accessToken = req.cookies.get("accessToken");
const refreshToken = req.cookies.get("refreshToken");
if (!accessToken?.value) {
if (pathname !== "/account/signin" && pathname !== "/account/signup") {
return NextResponse.redirect(DOMAIN_URL + "/account/signin");
}
return NextResponse.next();
}
}
요청 URL의 pathname을 추출하고 쿠키에서 accessToken과 refreshToken을 받아온다.
이후, 액세스 토큰이 없으면서 서비스 페이지를 이용 중이면 바로 로그인 페이지로 리다이렉트한다
accessToken이 없으면
accessToken이 있다면
그 다음 코드로 넘어가보자
try {
await jwtVerify(accessToken.value, ACCESS_TOKEN_SECRET);
if (pathname === "/account/signin" || pathname === "/account/signup") {
return NextResponse.redirect(DOMAIN_URL + "/main");
}
}
jose란?
JSON Web Token (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE) 등의 작업을 간편하게 처리할 수 있도록 도와준다
즉, jwt 토큰을 생성하거나 검증할 수 있게 하는 라이브러리
JWT 검증을 위해 jose 라이브러리의 jwtVerify 함수를 사용해서 accessToken을 검증한 후,
/account/signin 또는 /account/signup 경로일 때는 서비스 메인 페이지로, 그 외 검증 실패 시에는 로그인 페이지로 리다이렉트했다.
하지만 try가 있으면 catch가 있는 법…
catch (error) {
if (error instanceof JWTExpired && refreshToken?.value != null) {
try {
const verified = await jwtVerify(refreshToken.value, REFRESH_TOKEN_SECRET);
const newAccessToken = await new SignJWT(verified.payload)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt()
.setExpirationTime(ACCESS_TOKEN_EXPIRY)
.sign(ACCESS_TOKEN_SECRET);
const newRefreshToken = await new SignJWT(verified.payload)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt()
.setExpirationTime(REFRESH_TOKEN_EXPIRY)
.sign(REFRESH_TOKEN_SECRET);
const res = NextResponse.next({ request: req });
res.cookies.set("accessToken", newAccessToken);
res.cookies.set("refreshToken", newRefreshToken);
return res;
} catch (error) {
return NextResponse.redirect(DOMAIN_URL + "/account/signin");
}
} else {
return NextResponse.redirect(DOMAIN_URL + "/account/signin");
}
}
여기서는 accessToken 만료 시 refreshToken이 있는 경우 한 번 더 검증을 해서 새로운 토큰을 발급한다.
다 아는 이야기겠지만 만료된 JWT 토큰일 때 새 토큰을 재발급 받는 게 아니라 Refresh 토큰이 유효할 경우에만 새로 발급한다
res.cookies.set는 최종적으로 클라이언트 브라우저에 쿠키를 설정한다.
이 때, req.cookies.set이랑 헷갈릴 수 있어서 적어보자면 res.cookies.set은 응답 헤더에 Set-Cookie를 추가해 최종적으로 브라우저에 쿠키를 저장하도록 하는 메서드이다.
반면 req.cookies.set은 미들웨어 내부의 req.cookies를 업데이트하기 위한 것이며, 클라이언트에 직접 전송되지 않는다.
이렇게 해서 서버와 클라이언트 간의 상태 일관성을 유지할 수 있게 작업했다
... 또 사용하면서 내 경험으로 느꼈던 장점이 생긴다면 적으러 오겠다