본문 바로가기
🔥Next 뽀개기

axios interceptors 설정하기(feat.cookie)

by 짱돌보리 2024. 8. 6.
728x90

🍪axios interceptors 설정하기(feat.cookie)

1. axios 인터셉터 설정

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

2. 요청 인터셉터 설정

axiosInstance.interceptors.request.use(
  async (config: InternalAxiosRequestConfig) => {
    const excludedUrls = ['/auth/tokens'];
    if (excludedUrls.some((url) => config.url?.includes(url))) {
      return config;
    }

    const accessToken = getCookie('accessToken');
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }

    return config;
  },
  (error) => Promise.reject(error),
);
  • 요청 인터셉터는 모든 요청이 서버에 보내지기 전에 호출된다.
  • excludedUrls 배열에 포함된 URL에 대해서는 토큰을 추가하지 않는다 여기서 내 api는 /auth/tokens URL이 제외되는걸로...
  • getCookie 함수를 사용하여 쿠키에서 accessToken을 가져온다. 만약 토큰이 존재하면 요청 헤더에 Authorization 필드를 추가한다.

❓왜 excludedUrls 하는가


auth/tokens와 같은 URL은 보통 새로운 액세스 토큰을 요청하거나 리프레시 토큰을 사용하는 요청이다. 이러한 요청에서는 이미 리프레시 토큰이 Authorization 헤더에 포함되어 있어야 하고, 액세스 토큰을 추가하면 오류가 발생할 수 있다.

만약 요청 인터셉터가 모든 요청에 대해 액세스 토큰을 추가한다면, /auth/tokens에 대한 요청이 진행될 때 이미 만료된 액세스 토큰을 사용하려고 시도할 수 있다. 이 경우 서버는 401 오류를 반환하고, 응답 인터셉터에서 다시 토큰을 갱신하려고 시도하면서 무한 루프가 발생할 수 있다.

3. 리프레시 토큰 함수

const refreshToken = async () => {
  const refreshToken = getCookie('refreshToken');

  if (!refreshToken) {
    throw new Error('리프레시 토큰이 없습니다.');
  }

  try {
    const response = await axiosInstance.post(
      '/auth/tokens',
      {},
      {
        headers: {
          Authorization: `Bearer ${refreshToken}`,
        },
      },
    );

    const { accessToken, refreshToken: newRefreshToken } = response.data;

    // 새로운 토큰 저장
    setCookie('accessToken', accessToken);
    setCookie('refreshToken', newRefreshToken);

    axiosInstance.defaults.headers.common['Authorization'] =
      `Bearer ${accessToken}`;

    return accessToken;
  } catch (error) {
    deleteCookie('accessToken');
    deleteCookie('refreshToken');
    if (typeof window !== 'undefined') {
      alert('다시 로그인해주세요.');
    }
    throw error;
  }
};
  • 액세스 토큰이 만료되었을 때 호출되어 새로운 액세스 토큰을 요청한다.
  • 쿠키에서 refreshToken을 가져오고, 없으면 오류를 발생시킨다.
  • /auth/tokens 엔드포인트에 POST 요청을 보내어 새로운 토큰을 요청한다.
  • refreshToken을 Authorization 헤더에 추가해 보냄
  • 서버로부터 새로운 accessToken과 refreshToken을 받아서 쿠키에 저장한다.
  • 인스턴스의 기본 헤더에 새로운 액세스 토큰을 설정한다.

4. 응답 인터셉터 설정

axiosInstance.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig;

    if (error.response?.status === 401) {
      try {
        const accessToken = await refreshToken();

        // 새로운 액세스 토큰으로 헤더 업데이트 및 원래 요청 재시도
        originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;

        return axiosInstance(originalRequest);
      } catch (error) {
        return Promise.reject(error);
      }
    }

    return Promise.reject(error);
  },
);
  • 응답 인터셉터는 서버의 응답을 가로채 처리한다.
  • 응답이 성공적이면 그대로 반환한다.
  • 만약 응답의 상태 코드가 401 (Unauthorized)일 경우, refreshToken 함수를 호출해 새로운 액세스 토큰을 요청한다.
  • 새로운 액세스 토큰을 받고, 원래 요청의 헤더를 업데이트한 후, 원래의 요청을 다시 시도한다.

풀코드

import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { deleteCookie, getCookie, setCookie } from 'cookies-next';

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

axiosInstance.interceptors.request.use(
  async (config: InternalAxiosRequestConfig) => {
    const excludedUrls = ['/auth/tokens'];
    if (excludedUrls.some((url) => config.url?.includes(url))) {
      return config;
    }

    const accessToken = getCookie('accessToken');
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }

    return config;
  },
  (error) => Promise.reject(error),
);

const refreshToken = async () => {
  const refreshToken = getCookie('refreshToken');

  if (!refreshToken) {
    throw new Error('리프레시 토큰이 없습니다.');
  }

  try {
    const response = await axiosInstance.post(
      '/auth/tokens',
      {},
      {
        headers: {
          Authorization: `Bearer ${refreshToken}`,
        },
      },
    );

    const { accessToken, refreshToken: newRefreshToken } = response.data;

    // 새로운 토큰 저장
    setCookie('accessToken', accessToken);
    setCookie('refreshToken', newRefreshToken);

    axiosInstance.defaults.headers.common['Authorization'] =
      `Bearer ${accessToken}`;

    return accessToken;
  } catch (error) {
    deleteCookie('accessToken');
    deleteCookie('refreshToken');
    if (typeof window !== 'undefined') {
      alert('다시 로그인해주세요.');
    }
    throw error;
  }
};

axiosInstance.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig;

    if (error.response?.status === 401) {
      try {
        const accessToken = await refreshToken();

        // 새로운 액세스 토큰으로 헤더 업데이트 및 원래 요청 재시도
        originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;

        return axiosInstance(originalRequest);
      } catch (error) {
        return Promise.reject(error);
      }
    }

    return Promise.reject(error);
  },
);

export default axiosInstance;

https://axios-http.com/kr/docs/interceptors