Web

Refresh 토큰을 활용한 끊김 없는 로그인 유지하기

sungjae0309 2026. 4. 30. 02:06

웹 서비스를 개발할 때 가장 중요한 것 중 하나는 보안사용자 경험이다. 이번 포스팅에서는 로그인이 필요한 페이지를 보호하고, 토큰이 만료되어도 사용자가 모르게 자동으로 로그인을 연장하는 기능을 구현한 과정을 기록하고자 한다. 

 

1. API 권한 확인

  • 자물쇠가 있는 API: 가장 먼저 백엔드 Swagger 문서를 확인했다. Swagger 문서를 보면 오른쪽에 회색 자물쇠 아이콘이 있는 것들이 있다.이는 요청 헤더에 유효한 토큰이 반드시 포함되어야 함을 의미한다. 즉, 로그인한 사용자만 사용 가능 

2. 로그인 상태 관리와 UI 대응

  • 로그인에 성공한 화면이다. 로그인을 하면 서버에서 받은 accessToken을 localStorage에 저장한다. 동시에 Navbar의 UI도 토큰 유무에 따라 '로그인/회원가입' 버튼에서 '마이페이지/로그아웃' 버튼으로 바뀌도록 조건부 렌더링을 적용하였다. 

3. 울타리(ProtectedRoute) 테스트

  • 접근 제어가 잘 되는지 확인하기 위해 개발자 도구의 Application 탭에서 수동으로 accessToken을 삭제해봤다

  • 토큰이 없는 상태로 마이페이지에 접근하자, ProtectedRoute가 이를 감지하여 "로그인이 필요한 서비스입니다"라는 알림을 띄우고 로그인 페이지로 리다이렉트 시키는 것을 확인이 된다
  • Access Token은 보안을 위해 수명이 짧다. 하지만 만료될 때마다 사용자가 다시 로그인해야 한다면 매우 불편하기 때문에 Refresh Token을 활용해야 한다. 

4. 테스트를 위한 서버 세팅

  • 토큰 만료 10초로 수정: 토큰 재발급 로직이 잘 작동하는지 빠르게 확인하기 위해, 서버의 .env 파일에서 JWT_EXPIRES_IN 값을 기존 3600초에서 10초로 대폭 줄여서 진행하였다

5. 문제 발생 

  • 그러나 로직을 다 짰는데도 불구하고 10초 뒤에 요청을 보내면 자꾸 "세션이 만료되었습니다"라는 팝업이 뜨며 로그아웃이 되었다
  • 이 과정에서 두 가지 큰 문제를 발견:
    1. 인스턴스 불일치: API 호출 시 인터셉터가 설정된 커스텀 api 인스턴스가 아닌, 일반 axios 라이브러리를 직접 사용하고 있었다. Gemini말로는 도구를 가져오는 통로가 잘못되었던 것이라고 한다. 
    2. 데이터 필드명 오류: 서버는 리프레시 토큰을 보낼 때 refresh라는 이름을 원했지만, 내 코드에서는 refreshToken이라는 이름으로 보내고 있었다. Swagger 문서를 다시 확인해서 수정하였다. 

6. 최종 결과: 끊김 없는 서비스 완성

  • 네트워크 탭을 보면 아주 성공적으로 문제가 해결되었다. 사용자는 이제 어떤 팝업창도 안 뜨는 상태로 마이페이지 정보를 그대로 확인할 수 있게 된다
    1. me (401): 10초가 지나 토큰이 만료되어 첫 요청 실패
    2. refresh (201): 인터셉터가 가로채서 몰래 새 토큰을 받아옴
    3. me (200): 받아온 새 토큰으로 원래 요청을 재시도하여 성공