JWT와 관리 전략 + NextJS 실전 사례

Clapsheep·2025년 5월 21일
8
post-thumbnail

대부분의 서비스에서 기본적으로 구현하는 로그인 기능은 사용자의 신원을 확인하는 인증(Authentication) 을 담당하며, 인증이 완료되면 해당 사용자의 권한에 따른 인가(Authorization) 로직이 이어진다.

인증(Authentication) 누구인가? - 사용자의 신원을 확인하는 절차
인가(Authorization) 무엇을 할 수 있는가? - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지를 판단

이러한 인증·인가 절차를 구현하기 위해, 현대 웹 개발에서 사실상 표준처럼 사용되는 방식이 바로 JWT(JSON Web Token) 이다. 오늘은 JWT의 개념과 사용 이유를 정리하고, 관리하는 전략에 대해서 알아보고자 한다.


🔑 JWT(JSON Web Token)

JWT는 사용자의 정보를 담은 JSON 객체를 Base64Url로 인코딩하고, 여기에 서명(Signature) 을 추가하여 생성된 토큰이다. 이 토큰은 인증·인가 정보를 클라이언트에 안전하게 전달하기 위해 사용된다.

JWT를 생성할 때는 secret key가 필요하며, 이 키가 노출되지 않았다는 전제 하에 토큰의 위·변조가 불가능하기 때문에 안전한 방식으로 간주된다.

⚠️ JWT는 위·변조를 막는 것일 뿐, 정보를 암호화하지는 않는다.
Base64Url 인코딩된 내용은 누구나 디코딩할 수 있기 때문에, 비밀번호나 주민등록번호와 같은 민감 정보를 절대 담아서는 안 된다.

⚠️ JWT는 토큰 탈취 자체를 방지하지는 못한다.
따라서 XSS, CSRF와 같은 공격에 주의해야 하며, 이를 막기 위해 HttpOnly 쿠키 사용, 짧은 유효기간 설정, HTTPS 전송 등이 필요하다.


📦 JWT 구조

JWT는 .으로 구분되어 있는 총 3개의 부분으로 구성된 문자열이다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSIsImlhdCI6MTUxNjIzOTAyMn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

각 부분을 디코딩하면 JSON 객체를 얻을 수 있는데, 각 부분이 담고있는 정보에 대해서 알아보자.

🧾 Header

헤더는 해당 토큰의 암호화 알고리즘과, 토큰 타입이 기재되어 있는 부분으로 Base64Url로 인코딩된다.

{
  "alg": "HS256",
  "typ": "JWT"
}

🧩 Payload

페이로드는 사용자의 정보나 토큰의 메타정보를 담고 있다. jwt를 만들 때 개발자가 해당 부분에 들어갈 데이터를 정의할 수 있으며, 이 또한 Base64Url로 인코딩되기 때문에 디코딩 시, 누구나 데이터를 확인할 수 있어 민감한 정보를 담지 않도록 해야한다.

{
  "sub": "1234567890",
  "name": "Joe",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

각 데이터를 클레임이라 부르며, 아래의 표는 대표적인 클레임 예시이다.

클레임의미
iss발급자 (issuer)
sub사용자 고유 ID (subject)
aud대상자 (audience)
exp만료 시간 (expiration, UNIX timestamp)
iat발급 시간 (issued at)
nbf이 시간 전에는 사용 불가 (not before)
role, email사용자 정의 커스텀 클레임

✍️ Signature

서명은 앞의 두 부분(Header.Payload)을 secret key로 서명한 결과이다. 서버에서는 이 서명을 검증해, 토큰이 위조되지 않았는지 확인한다.


🛠️ 토큰 관리 전략

그러면 이렇게 만들어진 JWT 토큰은 어디에 저장하고, 어떻게 관리해야 안전하게 사용할 수 있을까?
JWT는 보안이 보장된 저장과 전송이 전제되지 않으면 오히려 취약점을 유발할 수 있기 때문에, 토큰의 저장 방식과 만료 전략을 함께 설계해야 한다.


🔐 Access Token & Refresh Token 전략의 등장 배경

초기에는 하나의 JWT 토큰만으로 인증을 처리하는 경우가 많았다. 사용자가 로그인하면 하나의 토큰을 발급하고, 이 토큰을 매 요청마다 헤더에 포함시켜 인증하는 방식이다.

그러나 이 방식은 몇 가지 한계점이 존재했다.

  • 토큰이 유출되면 만료 전까지 누구나 사용할 수 있다.
  • 토큰을 서버가 무상태(stateless)하게 검증하기 때문에 중간에 폐기할 방법이 없다.
  • UX를 위해 토큰 수명을 길게 가져가면 보안 위험이 커지고, 반대로 짧게 하면 로그인 유지가 어렵다.

이러한 문제를 해결하기 위한 방안으로, 현재는 대부분 Access Token과 Refresh Token을 나누어 사용하는 구조를 채택하고 있다.


✅ Access Token & Refresh Token의 역할과 정보 구성

항목Access TokenRefresh Token
목적인증된 사용자인지 확인새로운 Access Token 발급
특징짧은 수명 (10~30분), 요청마다 포함긴 수명 (7~30일), 서버 저장 및 검증
담는 정보사용자 식별자, 권한, 만료 시간 등최소한의 사용자 정보 혹은 토큰 ID

Access Token은 사용자의 요청에 대한 권한을 검증하는 데 사용되며, Refresh Token은 Access Token이 만료되었을 때 새로운 토큰을 발급받기 위해 사용된다.

Refresh Token은 서버에 저장되거나 Redis에 상태로 보관되며, 탈취와 재사용을 막기 위해 RTR(Rotation) 방식이 권장된다.


📦 토큰 저장 전략: 어디에 저장할까?

토큰은 클라이언트 측에 저장되어야 하며, 대표적으로 세 가지 방식이 있다.

1️⃣ 메모리 (RAM, 전역 변수 등)

Access Token만 클라이언트 메모리(예: React 상태, 전역 변수, TanStack Query cache)에 저장하는 방식

JavaScript에서 접근 가능하지만, 페이지 새로고침 시 초기화되므로 XSS 노출 시간이 짧고 제한적

Refresh Token은 HttpOnly 쿠키에 저장하여 자동 갱신이 가능하도록 구성

✅ 장점:

새로고침 전까지 빠르게 재사용 가능

로컬스토리지보다 XSS 대응에 더 유리

❗ 단점:

페이지 새로고침 시 토큰 소멸 → 자동 로그인 구현에 Refresh 토큰 필수

2️⃣ HttpOnly 쿠키 (권장)

  • JavaScript에서 접근할 수 없도록 설정된 쿠키

  • 브라우저가 요청 시 자동으로 포함시켜 보내주기 때문에, 서버에서 쉽게 인증 처리 가능

  • 쿠키에 설정 가능한 보안 옵션:

    • HttpOnly: JS 접근 불가
    • Secure: HTTPS에서만 전송
    • SameSite=Strict: CSRF 공격 방지

3️⃣ 로컬스토리지

  • 사용하기는 쉽지만, JavaScript에서 접근 가능해 XSS 공격에 매우 취약하다.
  • 일반적으로 보안이 필요한 인증 토큰을 저장하는 용도로는 적합하지 않다.

🔐 요약 비교

저장 방식보안성새로고침 후 유지XSS 취약성주로 저장하는 토큰
메모리중간❌ 유지 안됨⚠️ 제한적Access (only)
로컬스토리지낮음유지됨❌ 매우 취약Access or Refresh
HttpOnly 쿠키높음유지됨✅ 안전Access / Refresh

🍪 HttpOnly 쿠키: 어떻게 동작하고 왜 안전한가?

✅ 서버에서 클라이언트에 쿠키를 심는 방법

res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000, // 15분
});
  • 응답 시 Set-Cookie 헤더로 설정됨
  • 브라우저는 이후 요청마다 자동으로 쿠키를 포함

✅ 클라이언트에서 접근 불가

console.log(document.cookie); // access_token은 보이지 않음
  • HttpOnly 속성으로 인해 자바스크립트에서 쿠키에 접근할 수 없음
  • XSS로 인한 탈취를 방지할 수 있음

🧩 토큰 운영 전략 요약

항목권장 방식
Access TokenHttpOnly 쿠키에 저장, 짧은 수명
Refresh TokenHttpOnly + Secure 쿠키에 저장, Rotation 적용
토큰 갱신 방식Refresh Token으로 Access Token 재발급 (RTR)
로그아웃 처리서버에서 쿠키 삭제, Refresh Token 무효화

🛡 보안 위협: XSS와 CSRF란?

🔸 XSS (Cross-Site Scripting)

공격자가 사용자의 브라우저에 악성 스크립트를 삽입하여 민감한 정보를 탈취하는 공격

예방:

  • 사용자 입력 Escape/Sanitize
  • 민감 정보는 HttpOnly 쿠키에 저장
  • 신뢰할 수 없는 HTML은 렌더링하지 않기

🔸 CSRF (Cross-Site Request Forgery)

사용자가 로그인된 상태에서 공격자가 의도한 요청을 강제로 보내는 공격

예방:

  • SameSite=Strict 쿠키 설정
  • 요청에 CSRF 토큰 포함 (폼 기반 요청일 때)

⚙️ 실전 사례: 왜 나는 두 토큰 모두 쿠키에 저장했는가

내가 사용하는 스택은 Next.js (v15, App Router 기반) + NestJS 백엔드이다. 이 환경에서는 두 토큰 모두 HttpOnly 쿠키에 저장할 수밖에 없었다. 그 이유는 다음과 같다.

✅ 1. Next.js 서버 컴포넌트는 클라이언트 저장소(localStorage 등)에 접근할 수 없다

서버 컴포넌트에서 토큰을 사용하려면,
브라우저가 자동으로 서버에 토큰을 실어 보내야 한다.
→ 즉, 쿠키를 사용할 수밖에 없었다.


✅ 2. middleware.ts에서 토큰 검증을 해야 했기 때문

Next.js에서는 middleware.ts를 통해 라우트를 보호하는데, 이 코드는 서버에서 실행되기 때문에 localStorage나 메모리에 접근할 수 없다.

const token = request.cookies.get('access_token')?.value;

→ 쿠키에 토큰이 없으면, 미들웨어에서 인증 로직 자체가 동작하지 않는다.


✅ 3. SSR과 서버 인증의 자연스러운 연계

SSR(서버사이드 렌더링)에서는 요청 시점에 인증 정보가 필요하다. 쿠키는 브라우저가 서버에 자동으로 전송해주기 때문에,
서버에서 인증 상태를 쉽게 확인할 수 있다는 장점이 있다.


🔚 결론

실전에서는 보안과 아키텍처 제약을 고려할 때, Access Token조차도 HttpOnly 쿠키에 저장하는 방식이 더 안전하고 안정적이었다.
이 선택은 단순한 기술 선택이 아닌, Next.js + NestJS 환경에서 실현 가능한 최선의 보안 전략이었다.


profile
왜 사용하는지 적어보려고 블로그를 합니다.

0개의 댓글