회사에 와서 새로 배우게 된 것들 중에 '이걸 진작 알았으면 을매나 좋았게요~~'하는 것들이 아주 많다. 그 중에 하나가 interceptor 개념!

서버에 axios 요청을 날리기 직전에 요청을 가로채서, 또는 응답이 돌아온 직후에 응답을 가로채서 특정 작업을 처리하고자 할 때 interceptor를 사용할 수 있다. 모든 (또는 대부분의) axios 요청 보내기 직전에 공통적으로, 모든 (또는 대부분의) axios 응답 받기 직후에 공통적으로 처리할 작업들을 중앙집중화된(centralized) 방식으로 간단히 구현할 수 있다. 그 처리하고자 하는 공통적인 특정 작업이 도대체 무엇이냐면,, 아래와 같이 다양한 것이 될 수 있다.
interceptor를 몰랐던 시절, axios 요청을 보낼 때마다 일일히 토큰을 header에 넣어서 보내곤 했었다. local storage에 있는 JWT 토큰을 꺼내고, header에 넣고... 몇 줄 짜리 똑같은 코드를 매번 반복하여 쓰는 것이 아주 번거롭다고 생각했는데, 이 번거로움을 줄일 수 있는 간단한 방법이 있었다니!
axios.interceptors.request.use(config => {
const authToken = localStorage.getItem('authToken');
if (authToken) {
// 요청을 보낼 때마다 요청을 가로채서 header에 Authorization key에 토큰을 담아준다.
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
});
이렇게 interceptor에다가 header에 Access token을 넣는 로직을 단 한 번 정의해놓으면, API 요청을 할 때마다 일일히 access token을 담지 않아도 된다. 요청 직후 interceptor가 알아서 요청을 가로채서 토큰을 담아주기 때문이다.
access token이 만료되면, refresh token을 이용하여 access token을 재발급받아야 한다. 토큰 재발급은 토큰을 담아야 하는 요청들이라면 공통적으로 필요한 로직이기 때문에 interceptor를 이용하여 중앙집중화된 방식으로 관리하면 역시나 편리할 것이다.
axios.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 401) {
const newToken = await refreshToken();
localStorage.setItem('authToken', newToken);
// Retry the original request
return axios(error.config);
}
return Promise.reject(error);
}
);
예전에 interceptor를 모르던 시절 팀 프로젝트를 했을 때는 백엔드단에서 클라이언트로부터 요청이 들어올 때마다 access token의 만료 여부를 확인하고 ☞ 만료된 토큰일 경우 쿠키에 담아 보낸 refresh token을 가지고 access token을 바로 재발급해주는 방식을 채택했었다.
그런데 interceptor를 사용하여 토큰 만료 시 재발급 요청을 클라이언트 단에서 단 한 번 구현해놓으면, 서버에서 access token이 만료될 경우 일단 401 에러부터 던져주고 토큰 재발급 로직을 클라이언트에게 맡기는 것도 가능하다. 클라이언트에서 401 에러를 받았을 때 refresh token을 이용하여 access token 재발급을 받는 로직을 interceptor에 단 한 번 구현해 놓으면 모든 요청에 일일히 재발급 로직을 구현할 필요가 없기 때문이다. 이렇게 하면 토큰 갱신을 클라이언트 측에서 담당하게 되므로, 서버 측에서는 access token 만료 조건부가 아니라 클라이언트가 토큰 재발급 요청하는 케이스 조건부로 토큰을 재발급해주게 될 것이다. 근데 굳이 이 방법이 더 좋은 방법인건지는 모르겠다. 재발급 여부를 판단하는 로직을 클라이언트에 두느냐 서버에 두느냐의 차이만 있는 것 아닐까?
서버는 토큰 만료 시 401만 던져주라~~ 토큰 갱신은 클라이언트 측에서 필요할 때 요청하마~~ 방식의 상대적 장점은 무엇일까? (아직 모르겠음)
상태 코드 별 error handling은 모든 응답에 대해 공통적으로 처리해야할 일이기도 하다. 응답 interceptor를 이용하여, 응답을 가로채서 상태 코드 별 error handling을 중앙집중화된 방식으로 단 한 번 구현해놓으면, 응답을 받을 때마다 일일히 상태 코드 별 에러 처리를 신경 쓰지 않아도 된다.
axios.interceptors.response.use(
response => response,
error => {
const status = error.response ? error.response.status : null;
if (status === 401) {
// Handle unauthorized access
console.log("Unauthorized access");
} else if (status === 404) {
// Handle not found errors
console.log("Post not found");
} else {
// Handle other errors
console.error("An error occurred:", error);
}
return Promise.reject(error);
}
);
요즘 회사에서 쓰고 있는 코드이다! 유저가 FormData type의 파일을 전송하려할 경우 header에 Content-Type: 'multipart/form-data'을 추가해줘야 한다. 이 때 FormData일 경우 해당 속성을 header에 추가해주는 로직을 interceptor에 조건부로 넣으면, 번거로운 일 신경 덜 쓰고 유저가 저장하려는 FormData나 열심히 body에 담으면 되겠다.
axios.interceptors.request.use(config => {
if (config.data instanceof FormData) {
config.headers['Content-Type'] = 'multipart/form-data';
}
return config;
});
이 밖에도 interceptor로 처리할 수 있는 작업은 아주 다양하다. 그런데 interceptor에서 정의한 로직을 빼야하는 케이스에서는 어떻게 해야할까?
interceptor에서 특정 로직을 정의해놓기는 했는데, 특정 케이스에서 해당 로직을 사용하고 싶지 않을 경우에는 어떻게 하나? 이러한 소수의 케이스 때문에 interceptor에 로직을 담는 걸 포기해버리는 건 손해다.
이러한 경우에는 eject 메서드를 사용하면 된다.
// interceptor의 참조값을 myInterceptor에 담는다.
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
// eject 메서드를 사용하여 interceptor에서 선언한 로직을 사용하지 않는다.
axios.interceptors.request.eject(myInterceptor);
예를 들어, 토큰이 필요하지 않은 요청을 보낼 경우에 한하여 token을 주입하는 interceptor 내부 로직을 사용하지 않도록 할 수 있겠다.
이렇게 interceptor를 사용하면 반복되는 로직을 중앙집중화된 방식으로 단 한 번만 구현하면 되므로, 중복 코드를 피하고 일관된 방식으로 API 호출을 관리할 수 있다. 유지보수성이 단연 향상될 수 있다. 이로써 개발자 경험이 무지 향상되더라! 아주아주 편리하다!!
참고 자료:
https://medium.com/@barisberkemalkoc/axios-interceptor-intelligent-db46653b7303
axios docs
https://axios-http.com/kr/docs/interceptors