React 프로젝트에서 JWT 토큰 처리하기
보안을 강화하기 위해 프로젝트에 적용해본 웹 애플리케이션에서
JWT(JSON Web Token)
를 활용하고 있습니다. 이번 글에서는 제가
React
프로젝트에서 JWT 토큰을 어떻게 처리하고, 어떤 흐름으로 사용했는지 알아보겠습니다. 먼저 JWT에 대한 내용이 궁금하시다면 아래 내용을 참고 부탁드립니다.JWT 인증 흐름
구현한 코드를 예시로 React 프로젝트에서 JWT 기반 인증 흐름을 정리해보겠습니다.
1. 로그인 (Login)
- 사용자가 이메일, 비밀번호를 입력하면
/login
API 요청을 보냄
- 서버에서
Access Token
과Refresh 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 인터셉터를 활용해 위 과정이 자동으로 수행됨
개선할 수 있는 부분
- 토큰 저장 방식 개선
useState
를 통해 메모리에 저장하는 방식은 보안적으로 안전하지만, 새로고침 시 날아가는 단점이 있습니다.👉
localStorage
또는 sessionStorage
를 함께 사용하여 저장하는 방식을 고려해볼 수 있음- Axios 인스턴스 내에서
interceptors.response.use
에서axiosInstance
를 직접 사용하도록 변경
👉 현재 코드에서
axios(originalRequest)
로 다시 요청을 보내는데, axiosInstance(originalRequest)
로 바꾸면 기본 설정(baseURL
, withCredentials
)을 유지할 수 있음