리액트에서 JWT 토큰 처리 및 API 요청

리액트에서 JWT 토큰 처리 및 API 요청

요약
JWT 토큰을 리액트에서 관리하고 요청응답 로직 만들어보기 feat.Axios
작성일
Feb 9, 2025
태그
React
JWT
security

React 프로젝트에서 JWT 토큰 처리하기

보안을 강화하기 위해 프로젝트에 적용해본 웹 애플리케이션에서 JWT(JSON Web Token)를 활용하고 있습니다.
이번 글에서는 제가 React 프로젝트에서 JWT 토큰을 어떻게 처리하고, 어떤 흐름으로 사용했는지 알아보겠습니다. 먼저 JWT에 대한 내용이 궁금하시다면 아래 내용을 참고 부탁드립니다.
 
 

JWT 인증 흐름

구현한 코드를 예시로 React 프로젝트에서 JWT 기반 인증 흐름을 정리해보겠습니다.

1. 로그인 (Login)

  • 사용자가 이메일, 비밀번호를 입력하면 /login API 요청을 보냄
  • 서버에서 Access TokenRefresh Token을 응답 헤더에 포함하여 전달
  • Authorization 헤더에서 Access Token을 꺼내어 상태(setToken)에 저장
const login = async (email: string, password: string) => { try { const response = await axiosInstance.post('/login', { email, password }) const newToken = response.headers['authorization'] if (!newToken) throw new Error('토큰이 누락되었습니다.') setToken(newToken) return response.data } catch (error) { console.error('로그인 에러:', error) throw error } }

2. API 요청 시 Access Token 자동 추가

  • Axios 요청 인터셉터를 통해 모든 API 요청에 Access Token을 자동 추가
  • /login, /refreshToken 요청에는 Authorization을 붙이지 않도록 예외 처리
axiosInstance.interceptors.request.use( (config) => { if (!config.url?.includes('/refreshToken') && !config.url?.includes('/login') && token) { config.headers.Authorization = token } return config }, (error) => Promise.reject(error), )

3. Access Token 만료 시 자동 갱신

  • API 요청 중 401(Unauthorized) 에러 발생 시 Refresh Token을 이용하여 새 Access Token 요청
  • 이미 다른 요청이 토큰을 갱신 중이라면 기다렸다가 새로운 토큰을 받은 후 재요청
  • 모든 대기 요청을 한 번에 실행하여 불필요한 요청 반복을 방지
axiosInstance.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config // Access Token이 만료되었을 때 if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/refreshToken')) { originalRequest._retry = true if (!isRefreshing) { isRefreshing = true try { const response = await axiosInstance.post('/refreshToken') const newToken = response.headers['authorization'] if (!newToken) throw new Error('리프레시 후 토큰이 누락되었습니다.') setToken(newToken) processQueue(newToken) // 새로운 토큰을 헤더에 추가 후 요청 재시도 originalRequest.headers.Authorization = newToken return axiosInstance(originalRequest) } catch (error) { refreshSubscribers = [] setToken(null) navigate('/signin') return Promise.reject(error) } finally { isRefreshing = false } } else { try { const newToken = await new Promise<string>((resolve, reject) => { refreshSubscribers.push((token: string) => { token ? resolve(token) : reject('토큰 갱신 실패') }) }) originalRequest.headers.Authorization = newToken return axiosInstance(originalRequest) } catch (error) { return Promise.reject(error) } } } return Promise.reject(error) }, )

4. 새로고침 시 로그인 유지

  • React는 페이지가 새로고침되면 상태가 초기화됨
  • 이를 방지하기 위해 컴포넌트가 마운트될 때 Refresh Token을 사용하여 새로운 Access Token을 요청
useEffect(() => { const refreshAccessToken = async () => { try { const response = await axiosInstance.post('/refreshToken') const newToken = response.headers['authorization'] if (newToken) { setToken(newToken) } else { throw new Error('토큰이 없음') } } catch (error) { console.error('토큰 갱신 실패:', error) setToken(null) navigate('/signin') } } if (!token) { refreshAccessToken() } }, [])

5. 로그아웃 (Logout)

  • 로그아웃 시, 서버에 Refresh Token을 제거하는 요청을 보낸 후 토큰 상태를 초기화
  • navigate('/signin')을 호출하여 로그인 페이지로 이동
const logout = () => { axiosInstance .post('/logout') .catch((err) => console.error('로그아웃 요청 실패:', err)) setToken(null) navigate('/signin') }

📝 정리

✅ 사용자가 로그인하면 Access Token과 Refresh Token을 발급
✅ API 요청 시 Authorization 헤더에 Access Token을 자동 추가
✅ Access Token이 만료되면 Refresh Token으로 새로운 Access Token을 요청
✅ 새로운 Access Token을 받은 후 기존 요청을 재시도
✅ 페이지를 새로고침하면 Refresh Token을 통해 자동 로그인 유지
Axios 인터셉터를 활용해 위 과정이 자동으로 수행됨

개선할 수 있는 부분

  1. 토큰 저장 방식 개선
    1. useState를 통해 메모리에 저장하는 방식은 보안적으로 안전하지만, 새로고침 시 날아가는 단점이 있습니다.
      👉 localStorage 또는 sessionStorage를 함께 사용하여 저장하는 방식을 고려해볼 수 있음
  1. Axios 인스턴스 내에서 interceptors.response.use에서 axiosInstance를 직접 사용하도록 변경
    1. 👉 현재 코드에서 axios(originalRequest)로 다시 요청을 보내는데, axiosInstance(originalRequest)로 바꾸면 기본 설정(baseURL, withCredentials)을 유지할 수 있음
  1. 초기 렌더링 시 토큰 갱신 로직 개선
    1. 👉 useEffect에서 token을 체크하는 로직이 있는데, 서버 요청이 실패한 경우 navigate('/signin')이 실행되면서 리렌더링을 유발할 가능성이 있음. 이를 해결하기 위해 useState의 로딩 상태를 추가하면 좋음
       

      참고문헌