My Boundary As Much As I Experienced

Axios Instance Interceptor로 토큰을 넣어주는 방법. 그리고 interceptor를 쓰지 않았을 때와 썼을 때의 비교. 본문

FrontEnd/Frontend etc.

Axios Instance Interceptor로 토큰을 넣어주는 방법. 그리고 interceptor를 쓰지 않았을 때와 썼을 때의 비교.

Bumang 2024. 6. 7. 19:03

이제 그냥 관습적으로 쓰게 된 Axios Instance의 Interceptor..

주로 토큰 만료되면 재요청 해주는 기능을 위해 쓴다.

 

Axios Interceptor에 의존하느라

전통 방식으로 '토큰 만료 시 재발급 받아 재요청'을 어떻게 구현하는지 고민해보지 않았다.

그런데 이번에 듣던 강의에서 위 로직을 전통방식으로 어떻게 구현하는지 경험해봤는데,

이참에 둘이 비교해보기로 하였다. 

 

 

일단 AxiosInstance Interceptor란?

Axios 인스턴스는 axios.create()로 생성한, 기본 설정과 인터셉터를 공유하는 Axios 객체이다.

이를 통해 애플리케이션 전역에서 일관된 요청 설정을 관리할 수 있다. (모듈화)

특히 특정 경로에 대해 토큰을 자동으로 추가하는 등이 대표적인 쓰임새이다.

이러한 특징으로 인해 api함수마다 중복해서 쓰는 코드의 양을 줄이고 유지 보수를 용이하게 한다.

const API = axios.create({
  baseURL: 'https://example.com/api',
  timeout: 1000,
});

 

말 그대로 axios.create()를 실행해서 만들어낸 인스턴스를 뜻한다.

 

 

 

AxiosInstance Interceptor를 쓰지 않았을 때 토큰 만료 시 관리법

Axios Interceptor를 쓰지 않으면 아래처럼 try-catch문을 이용해서 구현하는게 일반적이다.

 

catch 부분에서 response status가 토큰 만료일 시,

 

1. 토큰을 가져온다.

2. 토큰 재발급 요청한다.

3. 새로 발급한 토큰으로 원래 api를 재요청한다.

 

식으로 구현한다.

 

이렇게 토큰을 관리할 시, 모든 페이지 및 컴포넌트 마다 비동기 요청 시 try-catch문을 만들고

그 안에 토큰 관리 코드를 다 일일이 써줘야 한다.

매우 중복되는 코드가 많아지고 관리가 힘들어지는게 당연하다. (세팅 하나만 바꾸면 모든 페이지마다 다 바꿔줘야하니까)

    try {
      ...
    } catch (error: unknown) {
      let errorResponse = (error as AxiosError).response;
      if (errorResponse?.status === 400) {
        Alert.alert(
          "알림",
          (errorResponse as AxiosResponse<{message: string}>).data.message,
        );
        dispatch(orderSlice.actions.rejectOrder(item.orderId));
      }
      // aceess 토큰 만료 에러면, 아래와 같은 일들을 해야한다.
      if (errorResponse?.status === 419) {
        // 토큰 재발급하는 코드
        const refreshToken = await EncryptedStorage.getItem("refreshToken");
        // 토큰 재발급
        const response = await axios.post(
          `${Config.API_URL}/refreshToken`,
          {}, // body는 비어두기
          {
            headers: {
              authorization: `Bearer ${refreshToken}`,
            },
          },
        );
        const accessToken = response.data.data.accessToken;
        await EncryptedStorage.setItem("accessToken", accessToken);
        // 다시 요청
        await axios.post(
          `${Config.API_URL}/accept`,
          {orderId: item.orderId},
          {headers: {authorization: `Bearer ${accessToken}`}},
        );
      }

 

 

 

Axios Interceptor를 썼을 때 토큰 관리법

Axios Instance의 interceptor를 사용하면

API 요청하기 직전 / 응답받은 직후 실행시킬 코드를 지정할 수 있다.

사실 Interceptor는 쓰기 나름이라 모든 개발팀이 조금씩 다 다르게 사용하고 있을 것이다.

여러가지 패턴이 있을 수 있는 것이다. 그 중 몇 가지 사용 예시를 적어보겠다.

 

1. 요청하기 직전에 토큰을 넣어주는 방법 (request)

request 전에 토큰을 무조건 넣어주는 방법이다. 없다면 토큰을 재발행해온다.

(그러나 토큰이 있지만 만료됐는지 아닌지는 여기서 관리하지 않는다)

API.interceptors.request.use(async (config) => {
    if (!!config.headers.Authorization) {
        // headers.Authorization이 있으면 계속 진행
        return config;
    }
    // 없으면 token 받아옴
    const token = await auth().currentUser?.getIdToken();
    if (token) {
        // 토큰 넣어줌
        config.headers['Authorization'] = 'Bearer ' + token;
    }
    return config;
});

 

 

2. 토큰이 만료되었을 때 refreshToken으로 다시 accessToken을 발급받아 재요청한 뒤 다시 응답을 받는 방법 (response)

토큰 만료 에러가 나올 시 토큰 재발급 api호출을 하여 access토큰을 받는다.

그리고 다시 원래 하려던 요청을 하여 제대로 된 값을 받는 방법이다.

 

사실 전역에 쓰는 것이 일반적인 방법인데, 이렇게 useEffect에다가 넣어주는 사람도 있나보다.

App.tsx처럼 앱 전체를 커버하는 컴포넌트 단에서 활용하였다.

// AppInner.tsx

  useEffect(() => {
    axios.interceptors.response.use(
      response => {
        return response;
      },
      async error => {
        const {
          config, // config가 원래 요청
          response: {status},
        } = error;
        if (status === 419) {
          if (error.response.data.code === "expired") {
            const originalRequest = config;
            // 토큰 재발급하는 코드
            const refreshToken = await EncryptedStorage.getItem("refreshToken");
            // 토큰 재발급

            const {data} = await axios.post(
              `${Config.API_URL}/refreshToken`,
              {}, // body는 비어두기
              {
                headers: {
                  authorization: `Bearer ${refreshToken}`,
                },
              },
            );

            dispatch(userSlice.actions.setAccessToken(data.data.accessToken));
            originalRequest.headers.authorization = `Bearer ${data.data.accessToken}`;
            return axios(originalRequest);
          }
        }
        // status 419가 아니라면, 다시 한 번 reject!
        // 그러면 catch에 걸린다.
        return Promise.reject(error);
      },
    );
  }, [dispatch]);

 

 

3. 요청보내기 전에 토큰 만료됐는지 확인하고 (request), 응답 받은 토큰으로 교체하기 (response)

다른 블로그에서 발견한건데, 이 개발팀은 accessToken을 다시 발급받는 함수를 따로 관리하지 않고

모든 요청마다 서버단에서 검사해주는거 같다.

만약 프론트에서 refreshToken까지 같이 보냈다면

서버는 accessToken을 다시 갱신해주는 것이다.

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ACCESS_EXP_MESSAGE, CheckJWTExp } from 'utils/CheckJwtExp';
import {
  getLocalStorage,
  removeLocalStorage,
  setLocalStorage,
} from './localStorage';

axios.defaults.withCredentials = true;

/** 1. 요청 전 - access토큰있는데 만료되면 refresh토큰도 헤더담아서 요청보내기 */
axios.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const accessToken = getLocalStorage('access_token');
    const refreshToken = getLocalStorage('refresh_token');
    if (accessToken) {
      /** 2. access토큰 있으면 만료됐는지 체크 */
      if (CheckJWTExp() === ACCESS_EXP_MESSAGE) {
        /** 3. 만료되면 만료된 access, refresh 같이 헤더 담아서 요청 */
        config.headers!.Authorization = `${accessToken}`;
        config.headers!.Refresh = `${refreshToken}`;
      } else {
        config.headers!.Authorization = `${accessToken}`;
      }
    }
    return config;
  },
  (error) => Promise.reject(error)
);

/** 4. 응답 전 - 새 access토큰받으면 갈아끼기 */
axios.interceptors.response.use(
  async (response: AxiosResponse) => {
    if (response.headers.authorization) {
      const newAccessToken = response?.headers?.authorization;
      removeLocalStorage('access_token'); // 만료된 access토큰 삭제
      setLocalStorage('access_token', newAccessToken); // 새걸로 교체
      response.config.headers = {
        authorization: `${newAccessToken}`,
      };
    }
    return response;
  },
  (error) => {
    //응답 200 아닌 경우 - 디버깅
    return Promise.reject(error);
  }
);