[Deep Dive] 유연한 JWT 인증 시스템 (2): 재시도를 넘어선 자동 리다이렉트와 예외 전략

이전 글에서 Access/Refresh 토큰의 재발급 로직을 다뤘다면, 이번에는 토큰 재발급조차 실패했을 때 혹은 비인가 사용자가 접근했을 때의 최종적인 처리 전략을 다뤄보겠습니다.

Next.js의 서버/클라이언트 환경을 모두 아우르는 requestHttp 유틸리티와 미들웨어 전략입니다.

1. 최종 방어선: AuthError와 자동 리다이렉트

Refresh 토큰마저 만료되어 더 이상 인증을 유지할 수 없을 때, 시스템은 사용자를 로그인 페이지로 안전하게 안내해야 합니다. 이를 위해 커스텀 에러인 AuthError를 정의하고 고차 함수로 처리 로직을 공통화했습니다.

백엔드와 프론트엔드의 유기적 협력의 핵심은 401 상태 코드에 있습니다.

  1. 백엔드: AuthGuard가 유효하지 않은 요청에 401을 던집니다.

  2. 프론트엔드 유틸: request 함수가 이를 감지해 AuthError를 발생시킵니다.

  3. 리다이렉트 래퍼: withAuthRedirect가 에러를 잡아 환경에 맞는 리다이렉트 로직을 실행합니다.

🛠️ 고차 함수(Higher-Order Function)를 통한 관심사 분리

단순히 fetch를 호출하는 기능(baseHttp)과 인증 실패 시 리다이렉트하는 기능(withAuthRedirect)을 분리하여 유지보수성을 높였습니다.

  • window.location.href의 이유: 클라이언트 사이드에서 useRouter 훅을 쓸 수 없는 컴포넌트 외부(유틸 파일)이기 때문입니다. 또한, 페이지를 완전히 새로고침하여 메모리에 남은 이전 사용자 상태를 깨끗하게 비워주는 보안상의 이점도 있습니다.

  • 서버 사이드 리다이렉트: 서버 환경에서는 Next.js 표준인 redirect 함수를 사용하여 즉각적인 페이지 전환을 수행합니다.

async function withAuthRedirect<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn();
  } catch (err) {
    if (err instanceof AuthError) {
      if (typeof window === 'undefined') {
        // 서버 사이드: Next.js redirect 활용
        redirect(PATH.LOGIN);
      } else {
        // 클라이언트 사이드: 로그아웃 처리 후 이동
        await fetch(INTERNAL_URL_IN_CLIENT.LOGOUT, { method: 'POST' });
        window.location.href = PATH.LOGIN;
      }
    }
    throw err;
  }
}

2. Isomorphic Fetch 유틸리티: 서버와 클라이언트를 하나로

Next.js 환경에서 통신 라이브러리를 만들 때 가장 까다로운 점은 코드의 실행 시점이 두 곳(서버와 클라이언트)이라는 것입니다. requestHttp는 이 Isomorphic(동형) 특성을 완벽히 대응하도록 설계되었습니다.

브라우저에서 API를 호출할 때는 브라우저가 자동으로 쿠키를 헤더에 실어 보내지만, 서버 사이드(Server Components나 SSR 단계)에서 외부 백엔드 API를 호출할 때는 이야기가 달라집니다. 이때의 주체는 브라우저가 아닌 '서버'이기 때문에, 우리가 명시적으로 쿠키를 챙겨서 보내주지 않으면 백엔드는 유저가 누구인지 알 방법이 없습니다.

왜 동적 임포트(import('next/headers'))를 사용했는가?

이 유틸리티 함수는 클라이언트와 서버 양쪽에서 호출됩니다. 하지만 next/headers서버 전용 모듈이기 때문에, 단순히 파일 상단에서 static하게 임포트하면 클라이언트 번들에 포함되어 에러를 발생시킵니다.

  1. 환경 분기: typeof window === 'undefined'를 통해 현재 실행 환경이 서버인지 확인합니다.

  2. 동적 로드: 서버 환경일 때만 필요한 모듈을 런타임에 가져와 브라우저 환경에서의 충돌을 원천 차단했습니다.

  3. 컨텍스트 포워딩: 현재 Next.js 서버가 클라이언트로부터 받은 cookie 헤더를 그대로 복사하여, 백엔드 API로 보내는 요청 헤더에 주입합니다.

// 서버 사이드 쿠키 전달 로직의 디테일if (typeof window === 'undefined') {try {
    // 1. 서버 환경에서만 실행되는 동적 임포트
    const { headers: nextHeaders } = await import('next/headers');
    
    // 2. 현재 요청(Request)의 쿠키 컨텍스트를 추출
    const cookieHeader = nextHeaders().get('cookie');
    
    // 3. 백엔드 API로 보낼 헤더에 수동으로 주입 (서버가 브라우저 대리인 역할)
    if (cookieHeader) {
      baseHeaders.cookie = cookieHeader;
    }
  } catch (err) {
    // next/headers를 사용할 수 없는 엣지 케이스 대응
    console.error('Failed to forward cookies in server context');
  }
}

3. 페이지 접근 제어: 미들웨어 전략

API 요청뿐만 아니라 페이지 진입 시점에서도 보안은 중요합니다. 저는 Next.js의 Middleware를 활용해 페이지 진입 전, 사용자의 권한을 검사를 하여 시스템의 문지기 역할을 하도록 페이지 성격을 구분했습니다.

  • 인가 전용 페이지: 로그인이 필요한 경로(예: /posts, /admin)에 토큰 없이 접근하면 로그인 페이지로 리다이렉트합니다.

  • 비인가 전용 페이지: 이미 로그인한 사용자가 로그인/회원가입 페이지에 접근하면 메인 페이지로 돌려보냅니다.

  • 공용 페이지: 홈페이지처럼 누구나 접근 가능하지만, 토큰 여부에 따라 UI(예: 글쓰기 버튼 노출 등)를 다르게 보여줍니다.

🛠️ Next.js Middleware를 통한 접근 제어

서버에 요청이 가기 전, Edge 단계에서 사용자의 인증 상태에 따라 길을 나누어 줍니다.

// middleware.ts (예시)import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('accessToken');
  const isAuthPage = request.nextUrl.pathname.startsWith('/login');
  const isPrivatePage = request.nextUrl.pathname.startsWith('/write');

  // 로그인 상태에서 로그인 페이지 접근 시 메인으로if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/', request.url));
  }

  // 비로그인 상태에서 작성 페이지 접근 시 로그인으로if (isPrivatePage && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

특히 matcher 설정을 통해 API 라우트나 정적 파일(_next/static, png 등)을 제외함으로써 미들웨어가 불필요하게 실행되는 오버헤드를 방지했습니다.


4. 마치며: 견고한 시스템이 주는 안정감

이번 인증 시스템 구축을 통해 얻은 가장 큰 수확은 "사용자는 결코 에러 메시지를 보지 않는다"는 점입니다.

  • 토큰이 만료되면 자동으로 재발급받아 요청을 완료하고,

  • 재발급조차 실패하면 자연스럽게 로그인 페이지로 안내하며,

  • 서버 사이드에서도 쿠키를 잃어버리지 않고 데이터를 가져옵니다.

이러한 추상화와 설계는 초기 구축 비용은 들지만, 이후 기능 개발 시 인증에 대한 고민 없이 비즈니스 로직에만 집중할 수 있게 해주는 든든한 기반이 됩니다.