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

Ganziman·2024년 10월 15일

오늘은 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
실패를 두려워하지 않고 끊임없이 시도하는 프론트엔드 개발자입니다.

0개의 댓글