AxiosInterceptors 설정

Yeji·2023년 7월 26일

Before start...

Vue3로 프로젝트를 진행하는 과정에서 AxiosInterceptors 설정에 대해 정리하고자 한다.
모든 설정은 공식문서를 기반으로 한다.

1. AxiosInterceptors

1-1. 정의

then과 catch로 처리되기 전에 axios 요청이나 응답을 가로채는 것이다.

서버에 요청을 보내기 전에 요청을 가로채서 헤더에 토큰을 넣어줄 수도 있고,
에러 응답이 왔을 때 에러를 사용자에게 띄워줄 수 있다.

1-2. 활용

이번 프로젝트에서 다음과 같이 활용할 것이다.

  1. 서버에 보내는 요청의 헤더에 자동으로 accessToken을 넣어 보낸다.
  2. unAuthorized 응답이 왔을 때 refreshToken으로 accessToken을 재발급 받고, 재발급 받은 accessToken으로 다시 기존 요청을 보낸다.

이 외 서비스 특성을 반영해 여러 세부 조건을 넣어주면 된다. 굵직한 동작은 이게 다다.

1-3. 공식문서 예시

공식문서 바로가기

// Add a request interceptor - 요청 인터셉터
axios.interceptors.request.use(function (config) {
    // Do something before request is sent - 요청 날리기 전 실행
    return config;
  }, function (error) {
    // Do something with request error	- 요청 에러시 실행
    return Promise.reject(error);
  });

// Add a response interceptor - 응답 인터셉터
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger	
   // - 200번대 응답은 여기서 처리
    // Do something with response data - 응답받은 데이터 처리
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
   // - 200번대 외의 응답은 여기서 처리
    // Do something with response error - 응답 에러 처리
    return Promise.reject(error);
  });

2. 적용

.env 파일을 설정하기 전이라 baseURL이 그대로 노출되어 있지만 어차피 로컬이라 상관 없이 그냥 진행한다.

2-1. axios instance 생성

공식문서 바로가기

인스턴스를 생성하고 인스턴스에 커스텀 설정을 할 수 있다. 나 같은 경우는 요청을 날리는 BASE URL를 적었다.

import axios from 'axios';

const axiosInstance = axios.create({
    baseURL: 'http://127.0.0.1:8000/api/',
    headers: {
        'Content-Type': 'application/json',
    },
});

export default axiosInstance;

2-2. 요청 헤더에 accessToken 싣기

이제 요청을 할 때마다 accessToken이 있다면 헤더에 실어서 보낼 것이다.

  • accessToken이 없는 경우에는 로그인을 하지 않은 유저니, 토큰이 필요한 요청에는 에러가 뜨겠고 토큰이 필요 없는 요청에는 응답이 올 것이다.
axiosInstance.interceptors.request.use(
    function (config) {
        const { accessToken } = useAuthStore();

        if (accessToken) {
            config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
    },
    function (error) {
        return Promise.reject(error);
    }
);

2-3. unAuthorized (401) 처리

만약 에러 코드가 unAuthorized 401이라면, 두 가지 상황이 있겠다.

  1. accessToken이 필요한 요청에 accessToken을 보내지 않음
  2. accessToken이 만료된 상태

내가 실행하고 싶은 동작은 다음과 같다.

  1. refreshToken이 존재한다면

    1-1. refreshToken을 보내 새로운 accessToken을 발급 받고 이전 요청을 다시 보낸다.

    1-2.존재하는 refreshToken 역시 만료되었다면 재로그인을 요청하며 관리자 첫 페이지로 리다이렉트 한다.

  2. refreshToken이 존재하지 않는다면

    2-1. 키오스크 첫 페이지로 리다이렉트 한다.


참고로 2번은 현재 진행하는 프로젝트 서비스의 특징 때문에 그렇다. 따라서 굵직한 기능을 먼저 작성하고, (토큰 재발급 & 이전 요청 다시 보냄) 본인이 따로 처리해주고 싶은 걸 알아서 작성하면 된다.

axiosInstance.interceptors.response.use(
    function (response) {
        console.log(response);
        return response;
    },
    function (error) {
        console.log('ERROR >>>', error.response.data);
        const originalRequest = error.config;
        const { refreshToken, updateAccessToken } = useAuthStore();

      // 401 에러 발생시
        if (error.response.status === 401) {
          // refreshToken이 있다면
            if (refreshToken) {
              // refreshToken으로 accessToken 발급 요청
                return axios
                    .post('http://127.0.0.1:8000/api/accounts/token/refresh/', {
                        username: '로그인된 사용자 이름',
                        refreshToken,
                    })
              		// accessToken 발급 성공
                    .then((res) => {
                  		// 새롭게 발급 받은 accessToken 저장 및 이전 요청 다시 보냄
                        updateAccessToken(res.data?.accessToken); 
                        originalRequest.headers.Authorization = `Bearer ${res.data.accessToken}`;
                        return axiosInstance(originalRequest);
                    })
					// refreshToken 만료, accessToken 발급 실패 
                    .catch((error) => {
						// 재로그인 요청 및 어드민 첫페이지로 리다이렉트
                        alert('다시 로그인해주세요.');
						router.push({ name: 'admin-index' });
                        return Promise.reject(error);
                    });
			// refreshToken이 없다면
            } else {
				// 키오스크 첫페이지로 리다이렉트
                router.push({ name: 'kiosk-index' });
            }
        }

        return Promise.reject(error);
    }
);

2-9. 전체 코드

import axios from 'axios';
import router from '@/router/index';
import { useAuthStore } from '@/stores/auth.store';

const axiosInstance = axios.create({
    baseURL: 'http://127.0.0.1:8000/api/',
    headers: {
        'Content-Type': 'application/json',
    },
});

axiosInstance.interceptors.request.use(
    function (config) {
        const { accessToken } = useAuthStore();

        if (accessToken) {
            config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
    },
    function (error) {
        return Promise.reject(error);
    }
);

axiosInstance.interceptors.response.use(
    function (response) {
        console.log(response);
        return response;
    },
    function (error) {
        console.log('ERROR >>>', error.response.data);
        const originalRequest = error.config;
        const { refreshToken, updateAccessToken } = useAuthStore();
		// Unauthorized : request a new accessToken
        if (error.response.status === 401) {
            if (refreshToken.length) {
                return axios
                    .post('http://127.0.0.1:8000/api/accounts/token/refresh/', {
                        username: '로그인된 사용자 이름',
                        refreshToken,
                    })
                    .then((res) => {
                        console.log(res);
                        updateAccessToken(res.data?.accessToken);
                        originalRequest.headers.Authorization = `Bearer ${res.data.accessToken}`;
                        return axiosInstance(originalRequest);
                    })
                    .catch((error) => {
                        alert('다시 로그인해주세요.');
                        router.push({ name: 'admin-index' });
                        return Promise.reject(error);
                    });
            } else {
                router.push({ name: 'kiosk-index' });
            }
        }

        return Promise.reject(error);
    }
);

export default axiosInstance;
profile
채워나가는 과정

0개의 댓글