[Next.js] 미들웨어(middleware) 사용기

Ganziman·2024년 10월 15일
1

오늘은 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이 없으면

  • 현재 경로가 /account/signin 또는 /account/signup이 아니라면
    -> DOMAIN_URL/account/signin으로 즉시 리다이렉트
  • 아니라면 (즉, 로그인 회원가입 페이지 접근 시)
    -> NextREsponse.next()로 그대로 통과

accessToken이 있다면

  • 다음 미들웨어나 API/페이지 핸들러로 요청을 전달

그 다음 코드로 넘어가보자

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를 업데이트하기 위한 것이며, 클라이언트에 직접 전송되지 않는다.

이렇게 해서 서버와 클라이언트 간의 상태 일관성을 유지할 수 있게 작업했다

끝으로..

  • 권한 처리 중앙화
    기존에는 각 Route별로 로그인·권한 검증 로직을 중복 작성했는데, 미들웨어 하나로 일관되게 관리하면서 코드 중복이 크게 줄고 유지보수성이 높아졌다.
  • 일관된 보안 정책 적용
    로그인 여부 확인, 사용자 역할 검사 등 공통 보안 로직을 미들웨어에서 처리하니, 서비스 전반에 동일한 기준으로 접근 제어를 적용할 수 있었다.
  • 유연한 예외 리다이렉션
    특정 경로만 예외 처리하거나, 조건에 따라 로그인 페이지·권한 없음 페이지로 동적으로 리다이렉트하는 로직을 한곳에서 정의할 수 있어 흐름 파악이 쉬워졌다.
  • 런타임 제약과 학습 곡선
    Edge 환경에서는 일부 Node.js API 사용이 제한되어 초기 적응이 필요했고, `RequestCookies`/`ResponseCookies` API를 숙지하는 데 시간이 걸렸다. 하지만 이 과정을 통해 Next.js 미들웨어의 동작 원리를 깊이 이해하게 되었다.

... 또 사용하면서 내 경험으로 느꼈던 장점이 생긴다면 적으러 오겠다

profile
GanziMan 입니다.

0개의 댓글