[Deep Dive] 유연한 JWT 인증 시스템 (3): Edge의 한계를 넘어, 리프레시 페이지 전략으로의 전환

인증 시스템 구축의 마지막 단계에서 예상치 못한 복병을 만났습니다. 로컬에서는 잘 돌아가던 미들웨어가 Vercel 배포 후 403 에러를 뱉어내기 시작한 것이죠. 수많은 스트레스 끝에 깨달은 Edge 서버의 제약과 이를 해결하기 위한 '리프레시 전용 페이지' 전략을 공유합니다.

1. 배경 및 문제 발생 (The 403 Nightmare)

이전 설계에서는 미들웨어에서 직접 토큰 재발행을 시도하고 쿠키를 저장하려 했습니다. 하지만 배포 후 다음과 같은 문제에 봉착했습니다.

  • Vercel Edge Runtime의 제한: 미들웨어는 Node.js 환경이 아닌 Edge 환경에서 동작합니다. 특정 IP나 외부 리소스에 대한 요청이 Edge 서버 레벨에서 차단(403 Forbidden)되는 이슈가 발생했습니다.

  • 버전 업데이트의 부담: 이를 해결하려면 Next.js 버전을 대폭 올려 미들웨어 런타임을 변경해야 했지만, 현재 프로젝트의 안정성을 위해 다른 길을 찾아야 했습니다.

  • SSR의 쿠키 제어 한계: 가장 결정적인 문제는 Next.js의 SSR(Server Side Rendering) 단계에서는 쿠키를 삭제하거나 저장할 수 없다는 점이었습니다. next/headers는 Server Action이나 Route Handler에서만 쓰기(Write)가 가능하기 때문입니다.


2. 전략 수정: 관심사의 분리와 '리프레시 페이지' 도입

"SSR에서 안 된다면, 클라이언트의 힘을 빌리자"는 결론에 도달했습니다. 더 가볍고 명확한 구조를 위해 관심사를 다음과 같이 쪼갰습니다.

🛠️ 미들웨어: 최소한의 가드(Guard) 역할

미들웨어에서 리프레시 토큰까지 체크하는 것은 블로그 서비스에 다소 과하다고 판단했습니다.

  • 역할: 액세스 토큰의 존재 유무단순 만료 여부만 판단합니다.

  • 동작: 토큰이 만료되었다면 즉시 /refresh 페이지로 리다이렉트시킵니다.

🛠️ 리프레시 페이지: 토큰 재발행의 전담 처리반

이 시스템의 핵심입니다. 클라이언트 컴포넌트로 구성된 /refresh 페이지는 다음과 같이 동작합니다.

  1. 사용자가 /refresh 페이지에 진입합니다.

  2. 클라이언트 사이드에서 Route Handler(API Route)로 토큰 재발행 요청을 보냅니다.

  3. Route Handler는 외부 백엔드와 통신하여 새 토큰을 받아오고, set-cookie를 통해 브라우저 쿠키에 직접 저장합니다.

  4. 저장이 완료되면 사용자가 원래 가려던 페이지로 다시 보냅니다.

export default function ReissuePage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get('callbackUrl');

  useEffect(() => {
    let isSubscribed = true;

    const syncToken = async () => {
      const res = await requestHttp.post(INTERNAL_URL_IN_CLIENT.REFRESH);
      if (res.ok) {
        router.replace(callbackUrl || PATH.HOME);
      } else {
        router.replace(PATH.LOGIN);
      }
    };

    if (isSubscribed) {
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
      syncToken();
    }

    return () => {
      isSubscribed = false;
    };
  }, [router, callbackUrl]);

3. 왜 이 방식이 더 나은가? (유지보수와 일관성)

단순히 에러를 피하기 위한 우회가 아니라, 아키텍처 측면에서 몇 가지 큰 이점을 얻었습니다.

  • 일관된 통신 경로: 이전에는 SSR에서 직접 외부 API를 부르기도 했지만, 이제는 모든 토큰 관련 처리가 Route Handler를 거치게 되어 데이터 흐름이 일관되게 정리되었습니다.

  • 브라우저-서버 동기화: 클라이언트에서 API Route를 호출하면 응답 헤더의 쿠키가 브라우저에 즉시 반영됩니다. SSR에서 겪었던 "쿠키 쓰기 불가" 문제를 완벽히 해결한 것입니다.

  • 성능과 보안의 타협점: 은행권처럼 극도의 보안이 필요하다면 모든 단계에서 검증하겠지만, 블로그 서비스 특성에 맞춰 속도는 가볍게, 만료 관리는 철저하게 가져가는 합리적인 선을 찾았습니다.


4. 마치며: 고통 끝에 배운 인프라의 이해

이번 과정은 정말 힘들었고 스트레스도 상당했습니다. 하지만 이 과정을 통해 두 가지 중요한 사실을 뼈저리게 배웠습니다.

  1. 미들웨어(Edge)는 만능이 아니며, 외부 통신 시 인프라 레벨의 차단이 발생할 수 있다.

  2. SSR 환경은 '읽기' 전용에 가깝고, 쿠키의 '쓰기'는 브라우저와 통신이 가능한 영역(API Route 등)에서 이루어져야 안전하다.

비록 돌아온 길 같지만, 결과적으로 더 명확하고 일관성 있는 인증 시스템을 갖추게 되었습니다. 저와 비슷한 에러로 밤을 지새우는 개발자분들에게 이 '리프레시 페이지 전략'이 하나의 이정표가 되길 바랍니다.