accessToken과 refreshToken 발행 및 관리 재발행 등 참 어려웠다

#AccessToken#CursorAI#JWT#NextJS#PHP백엔드

CURSOR AI를 활용해서 로그인 기능을 만들기 시작했을 때만 해도 로그인만 되면 끝 아니야?라는 생각을 했다.

실제로 이메일과 비밀번호로 로그인하고, JWT Access Token을 발급하는 것까지는 큰 어려움이 없었다.


문제는 그 다음이었다.


Access Token이 만료되면?

로그인을 해두고 조금 지나면 자연스럽게 Access Token이 만료된다. 그때마다 다시 로그인 화면으로 보내는 건 사용자 경험이 너무 안 좋다.

그래서 등장하는 게 Refresh Token인데, 여기서부터 머리가 복잡해지기 시작했다.

  • Refresh Token은 왜 HttpOnly 쿠키로 굽는지

  • 왜 Access Token은 DB에 저장하지 않는지

  • 로그아웃은 도대체 어디까지가 로그아웃인지

하나하나 명확히 설명하지 못한 상태에서 구현을 시작하니 로직이 계속 꼬였다.


가장 헷갈렸던 부분: Access와 Refresh의 역할

처음에는 두 토큰의 차이를 이렇게 단순하게 생각했다.

  • Access Token: 로그인 증명

  • Refresh Token: 연장용 토큰

그런데 이게 완전히 잘못된 이해였다.

정리해보니 실제 역할은 이랬다.

  • Access Token

    • 수명이 짧다

    • 서버는 서명과 만료 시간만 보고 검증한다

    • 빠르고 가볍지만, 서버에서 강제로 끊기는 어렵다

  • Refresh Token

    • 수명이 길다

    • DB에 저장해서 상태를 관리한다

    • 이게 사실상 로그인 세션이다

이걸 이해하고 나서야 왜 실무에서 Access와 Refresh를 굳이 나누는지 납득이 됐다.


Refresh Token을 DB로 관리해야 했던 이유

Refresh Token을 단순히 쿠키로만 관리하면 이런 문제가 생긴다.

  • 로그아웃해도 서버는 모른다

  • 특정 기기만 로그아웃시키기 어렵다

  • 토큰 탈취 여부를 판단할 수 없다

그래서 Refresh Token은 DB에 해시 값으로 저장하고, 요청이 들어올 때마다 이 세션이 아직 유효한지를 서버가 직접 판단하도록 만들었다.

이 순간부터 아, 이건 그냥 토큰이 아니라 세션 관리구나라는 생각이 들었다.


제일 어려웠던 부분: Refresh 재발행과 재사용 탐지

가장 많이 헤맸던 부분은 Refresh Token 재발행 로직이었다. Refresh Token을 사용하면 기존 토큰은 바로 폐기하고 새 토큰을 발급하는 방식(회전, rotation)을 적용했다. 그리고 이미 한 번 사용해서 폐기된 Refresh Token이 다시 서버로 들어오면? 이건 정상적인 사용이 아니라 토큰이 복제되었을 가능성이 있는 상황으로 판단해야 한다. 그래서 이런 경우에는 해당 디바이스, 또는 계정 전체를 강제 로그아웃시키는 정책을 적용했다.


이 로직을 이해하고 나서야 왜 인증 로직이 길고 복잡할 수밖에 없는지 체감이 됐다.


서버와 프론트의 역할을 나누니 정리가 됐다

중간에 가장 크게 방향을 바꾼 부분은 역할 분리였다.

  • PHP 서버

    • 토큰 발급

    • Refresh 검증과 재발행

    • 세션(DB) 관리

  • Next.js 프론트

    • 로그인 상태 UI

    • 접속한 디바이스 목록 표시

    • 로그아웃 요청

프론트에서 토큰을 직접 다루려고 할수록 복잡해졌고, 모든 판단을 서버에서 하도록 넘기니 구조가 훨씬 깔끔해졌다.


정리하면서 느낀 점

CURSOR AI 덕분에 코드를 못 짜서 막힌 건 아니었다. 대부분은 개념이 정리되지 않은 상태에서 구현을 시작했기 때문이었다.

이번 작업을 통해 느낀 건 이거다.

로그인 기능은 단순한 UI 작업이 아니라
상태와 신뢰를 설계하는 문제라는 것.

다음에 인증 기능을 만들 땐 무작정 코드부터 치기보다는 구조부터 먼저 그려보게 될 것 같다.

29

댓글