Axios는 node.js와 브라우저를 위한 Promise 기반의 HTTP 비동기 통신 라이브러리로, JSON 데이터 자동 변환, 요청 및 응답 인터셉터 기능 등 다양한 장점을 가지고 있다.
Axios를 사용할 때마다 헤더에 토큰을 넣어주거나, baseURL을 작성하는 등 반복되는 코드를 작성하지 않기 위해서 커스터마이징한 인스턴스를 모듈화하여 사용했다. 새로운 인스턴스를 생성한 후, interceptor에서 토큰 관련 부분을 알아서 처리하도록 만들어보았다.
커스터마이징 과정은 다음과 같다.
axios.create([config])
사용자 지정 config로 Axios
instance를 만들 수 있다.
config 안에는 baseURL
, timeout
, withCredentials
등 다양한 옵션을 지정할 수 있다.
이번 프로젝트에서는 아래와 같이 baseURL만 지정해줬다.
const customAxios = axios.create({
baseURL: SERVER_BASE_URL,
});
interceptor
를 사용하면 then
또는 catch
로 처리되기 전에 요청과 응답을 가로챌 수 있다.
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.use(
function (config) {
// 요청이 전달되기 전에 작업 수행
return config;
},
function (error) {
// 요청 오류가 있는 작업 수행
return Promise.reject(error);
});
게시글 조회 페이지와 같이 비로그인 유저도 접근할 수 있는 페이지들이 있지만, 북마크 여부 등 토큰 유무에 따라 받아오는 데이터 값이 다르기 때문에 토큰을 확인하는 절차가 필수였다. 요청을 인터셉트해서 access token의 정보를 넣어주도록 하였다.
- access token 값의 유무 확인
- 토큰이 있으면 헤더의 Authorization에
Bearer ${token}
형태로 토큰 값을,
토큰이 없으면 빈 문자열을 넣어준다.
customAxios.interceptors.request.use(
(config) => {
const nextConfig = config;
const accessToken = getAccessToken();
nextConfig.headers.Authorization = accessToken
? `Bearer ${accessToken}`
: '';
return nextConfig;
},
(error) => {
console.log(error);
return Promise.reject(error);
},
);
axios.interceptors.response.use(
function (response) {
// 2xx 범위에 있는 상태 코드는 이 함수를 트리거 한다.
// 응답 데이터가 있는 작업 수행
return response;
},
function (error) {
// 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 한다.
// 응답 오류가 있는 작업 수행
return Promise.reject(error);
});
access token의 유효 시간은 30분, refresh token의 유효 시간은 24시간으로 설정되어 있다.
응답을 가로채서 토큰 만료 에러가 뜨면 서버에 토큰 재발행 요청을 보내도록 했다.
- 에러코드가 'EXPIRED_ACCESS_TOKEN'인지 확인
- 기존의 access token과 refresh token의 값을 서버에 전달하여 토큰 재발행 요청
- 재발행된 토큰 값을 저장소에 저장
- 재발행된 토큰 값을 헤더에 넣어 재요청
customAxios.interceptors.response.use(
(response) => response,
async (error) => {
// response에서 error가 발생했을 경우 catch로 넘어가기 전에 처리하는 부분
const { errorCode } = error?.response?.data;
const originalRequest = error?.config;
if (
errorCode === 'EXPIRED_ACCESS_TOKEN' &&
!originalRequest.retry
) {
const refreshToken = await getRefreshToken();
const accessToken = await getAccessToken();
const res = await customAxios.post(
'/member/reissue',
{ accessToken, refreshToken }
);
const { newAccessToken, newRefreshToken } = res.data;
setAccessToken(newAccessToken);
setRefreshToken(newRefreshToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return customAxios(originalRequest);
}
}
return Promise.reject(error);
},
);
// src/apis/core/instance.js
import axios from 'axios';
import { SERVER_BASE_URL } from '../../constants/serverBaseUrl';
import {
setAccessToken,
getAccessToken,
removeAccessToken,
} from '../../utils/controlAccessToken';
import {
setRefreshToken,
getRefreshToken,
removeRefreshToken,
} from '../../utils/controlRefreshToken';
const customAxios = axios.create({
baseURL: SERVER_BASE_URL,
});
customAxios.interceptors.request.use(
(config) => {
const nextConfig = config;
const accessToken = getAccessToken();
nextConfig.headers.Authorization = accessToken
? `Bearer ${accessToken}`
: '';
return nextConfig;
},
(error) => {
console.log(error);
return Promise.reject(error);
},
);
customAxios.interceptors.response.use(
(response) => response,
async (error) => {
// response에서 error가 발생했을 경우 catch로 넘어가기 전에 처리하는 부분
const { errorCode } = error?.response?.data;
const originalRequest = error?.config;
if (
errorCode === 'EXPIRED_ACCESS_TOKEN' &&
!originalRequest.retry
) {
const refreshToken = await getRefreshToken();
const accessToken = await getAccessToken();
const res = await customAxios.post(
'/member/reissue',
{ accessToken, refreshToken }
);
const { newAccessToken, newRefreshToken } = res.data;
setAccessToken(newAccessToken);
setRefreshToken(newRefreshToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return customAxios(originalRequest);
}
}
return Promise.reject(error);
},
);
export default customAxios;
참고 자료
Axios 공식문서