-
[ Project ] Axios Interceptor 활용하기Project 2023. 9. 6. 18:59
- Axios Interceptor 활용하기
Axios Interceptor를 활용하여 모든 요청과 응답 전에 원하는 로직을 적용할 수 있다.
이를 활용하여
1. 모든 요청 헤더에 토큰이 필요.
2. accessToken토큰이 만료되었을 경우 토큰 재발급하고, 갱신된 토큰을 헤더에 설정하고 재요청.
위 두 가지 문제를 해결할 수 있다.
- Request Interceptor, 헤더에 Access Token 설정하기
const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_BASE_URL, }); // 요청 인터셉터 : 헤더에 토큰 추가 instance.interceptors.request.use( (config) => { const token = {token} config.headers.Authorization = token; return config; }, (error) => { return Promise.reject(error); } );
모든 요청 헤더에 Authorization : token 설정된다.
- Response Interceptor, 토큰 만료 이슈 처리
토큰 만료 헤더에 설정한 토큰이 만료된 토큰이라면, 상태코드 401에러(백엔드에서 설정한 상태코드)가 발생한다.
응답 Interceptor를 통해서 에러 상태코드에 따른 핸들링을 할 수 있다.
즉, 토큰 만료에 해당하는 상태코드가 감지되면 토큰 재발급 후에 실패한 요청에 대해서 재요청을 보낼 수 있다.
정리하자면
- 401 응답 감지: 응답 인터셉터에서 401 응답을 감지
- 토큰 재발급 요청: 새로운 토큰을 재발급받기 위한 요청
- 원래 요청 재시도: 재발급받은 토큰을 사용하여 원래 요청을 재시도. 이때, originalRequest 객체를 수정하여 새로운 토큰을 헤더에 설정
// 응답 인터셉터: 401 에러 처리 instance.interceptors.response.use( (response: AxiosResponse) => { return response; }, async (error: any) => { const {config, response: { status }} = error; // 토큰 만료 상태코드 401 핸들링 if (status === 401) { const originalRequest = config; // refreshToken값 가져오기 const refreshToken = {refreshToken} // 토큰 재발급 요청 const newToken = await instance.post('api/member/v1/accessToken/expired',{}, {headers: {refreshToken,},} ).then((res) => {return res.data.data}) .catch((err) => {return Promise.reject(err)}); originalRequest.headers.Authorization = newToken // 재요청 return instance(originalRequest); } return Promise.reject(error); } );
- 토큰 재발급 무한 루프
무한 루프 토큰 만료된 요청 4개가 각각 토큰 재발급을 받으면서 토큰 만료 <-> 재발급 무한루프가 발생했다.
대략적으로 상황을 요약하자면
- A,B,C,D 요청 모두 토큰이 만료되어 401에러가 발생
- A요청에 대한 response interceptor -> 토큰 재발급 -> 토큰 갱신
- B요청에 대한 response interceptor -> 토큰 재발급 -> 토큰 갱신
- C, D 요청 반복
- A 재요청 -> 토큰 만료 401에러
- B 재요청 -> 토큰 만료 401에러
- C, D 반복
- A요청 응답 interceptor 토큰 재발급 -> 토큰 갱신
- B요청 응답 interceptor 토큰 재발급 -> 토큰 갱신
- - -무한 반복
대략 이런 무한 루프인 것 같다.
무한루프를 해결하기 위해 두 개의 변수를 추가하여 토큰 재발급을 한 번으로 제한하는 로직을 추가했다.
1. 토큰 재발급 중인지 확인하는 플래그 <boolean> = isRefreshing
2. 토큰 재발급 요청의 Promise 객체 <Promise> = refreshedTokenPromise
let isRefreshing = false; // 토큰 재발급 중인지 확인하는 플래그 let refreshedTokenPromise: Promise<any> | null = null; // 토큰 재발급 요청의 Promise 객체
- Response Interceptor 401 에러 핸들링 과정
1. A,B,C,D 요청 모두 토큰이 만료되어 401에러가 발생
2. A요청에서 최초 401에러를 감지, isRefreshing을 false에서 true로 업데이트.
// 401 에러 발생하고 토큰 재발급 중이 아닐 때 if (status === 401 && !isRefreshing) { isRefreshing = true; // 재발급 중으로 플래그 변경 const originalRequest = config; // refreshToken 가져오기 const refreshToken = {refreshToken} // 토큰 재발급 요청 refreshedTokenPromise = axios.post('/api/member/v1/accessToken/expired',{}, {headers: {refreshToken,},}) .then((res) => {isRefreshing = false; // 재발급 완료로 플래그 변경 refreshedTokenPromise = null; return res.data.data; // 새로운 토큰 반환 }) .catch((err) => { isRefreshing = false; if (err.response.status === 403) { // refreshToken 만료 // 로그아웃 로직 } return Promise.reject(err); }); // 새로 발급받은 토큰으로 요청 재실행 const newToken = await refreshedTokenPromise; config.headers.Authorization = newToken.accessToken; return instance(originalRequest);
3. 토큰 재발급 및 refreshedTokenPromise에 토큰 재발급 요청 Promise객체 할당
4. B,C,D요청에 대한 response interceptor 401에러 핸들링에서는 refreshedTokenPromise이 resolve 상태가 될 때까지 대기
refreshedTokenPromise 상태가 peding에서 resolve가 되면, 재발급된 토큰으로 재요청
if (status === 401 && isRefreshing) { // 재발급이 완료될 때까지 기다린다. const newToken = await refreshedTokenPromise; if (newToken) { config.headers.Authorization = newToken; } // 새로운 토큰으로 요청 재실행 return instance(config); }
- 전체 코드
let isRefreshing = false; // 토큰 재발급 중인지 확인하는 플래그 let refreshedTokenPromise: Promise<any> | null = null; // 토큰 재발급 요청의 Promise 객체 // * * 코드 생략 // 응답 인터셉터: 401 에러 처리 (토큰 만료 등) instance.interceptors.response.use( (response: AxiosResponse) => { // 정상 응답은 그대로 반환 return response; }, async (error: any) => { const { config, response: { status }, } = error; // 401 에러 발생하고 토큰 재발급 중이 아닐 때 if (status === 401 && !isRefreshing) { isRefreshing = true; // 재발급 중으로 플래그 변경 const originalRequest = config; // refreshToken 가져오기 const refreshToken = {refreshToken} // 토큰 재발급 요청 refreshedTokenPromise = axios.post('/api/member/v1/accessToken/expired',{}, {headers: {refreshToken,},}) .then((res) => {isRefreshing = false; // 재발급 완료로 플래그 변경 refreshedTokenPromise = null; return res.data.data; // 새로운 토큰 반환 }) .catch((err) => { isRefreshing = false; if (err.response.status === 403) { // 재발급 요청에 권한이 없는 경우 // 로그아웃 로직 추가 필요 } return Promise.reject(err); }); // 새로 발급받은 토큰으로 요청 재실행 const newToken = await refreshedTokenPromise; config.headers.Authorization = newToken.accessToken; return instance(originalRequest); } // 401 에러 발생하고 토큰 재발급 중일 때 if (status === 401 && isRefreshing) { // 재발급이 완료될 때까지 기다린다. const newToken = await refreshedTokenPromise; if (newToken) { config.headers.Authorization = newToken; } // 새로운 토큰으로 요청 재실행 return instance(config); } // 기타 에러는 그대로 반환 return Promise.reject(error); } ); export default instance;
무한 루프 해결 👍 토큰 재발급 한 번만 요청된다.
'Project' 카테고리의 다른 글
[ Project ] FormData ( File과 Json data를 함께 ) (0) 2023.10.19 [ Project ] 컴포넌트 설계에 대한 고민 (Feat. 합성 컴포넌트) (1) 2023.09.10 [ Project ] RTK-Query에 Axios 입히기 (0) 2023.09.04 [ Project ] React 재사용 버튼 컴포넌트 만들기 (0) 2023.09.01 [ Project ] Next.js 13 다크모드 적용기 (0) 2023.08.30