Axios Interceptor를 이용해서 로그인 상태 유지하기

kidstone·2024년 9월 22일
0

외개인 프로젝트

목록 보기
5/10
post-thumbnail

들어가며

로그인 상태를 유지하기 위해 API 매 요청마다 항상 토큰을 헤더에 실어보내면서 사용자 인증을 진행해야 한다. 그에 따라 기존에 작성했던 로직은 다음과 같다.
내가 채택한 로그인 인증 방식

import { API } from './api';
import useAuthStore from '@store/authStore';
import { toast } from 'react-toastify';

const makeAuthorizedRequest = async (url, method = 'get', config) => {
	try {
		let response;
		switch (method) {
			case 'get':
				response = await API.get(url);
				break;
			case 'post':
				response = await API.post(url, config);
				break;
			case 'put':
				response = await API.put(url, config);
				break;
			case 'delete':
				response = await API.delete(url, { data: config });
				break;
			case 'patch':
				response = await API.patch(url);
				break;
			default:
				throw new Error('Invalid HTTP method');
		}
		return response;
	} catch (error) {
		if (error.response && error.response.status === 403) {
			try {
				const refreshResponse = await API.get('/api/v1/member/refresh');
				setAccessToken(refreshResponse.data.access_token);
				const accessToken = useAuthStore.getState().accessToken;
				API.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
				return await makeAuthorizedRequest(url, method, config);
			} catch (error) {
				toast.error('로그인이 필요한 서비스입니다!');
				setTimeout(() => {
					window.location.href = 'http://127.0.0.1:3000/login';
				}, 3000);
			}
		} else {
			if (error.response) {
				const errorMessage = error.response.data?.errorMessages?.errorMessage;

				if (errorMessage) {
					toast.error(errorMessage);
				} else {
					toast.error('알 수 없는 오류가 발생했습니다.');
				}
			} else {
				toast.error('네트워크 오류가 발생했습니다.');
			}
			return error.response.data.errorMessages.errorMessage;
		}
		// throw error;
	}
};

const setAccessToken = (token) => {
	useAuthStore.getState().setAccessToken(token);
};

export { makeAuthorizedRequest };

makeAuthorizedRequest 파일의 기능은 다음과 같다.

  • 파라미터로 url, method 방식, config(페이로드)를 받는다.
  • axios 객체(코드상에서는 API)를 이용하여 API 요청을 한다.
  • 요청에 대해 403 에러가 발생하면 refresh token을 이용해 access token을 갱신하는 API를 호출한다.
  • 받아온 access token을 이용해서 Authorization 헤더에 실은뒤 API를 재요청한다.
  • access token을 갱신하는 API 호출시 에러가 나면 로그인 페이지로 이동시킨다.

이 함수를 만들 당시에는 axios interceptor라는 것을 들어보기만 했지, 제대로 알지 못했다. 하지만 최근에 회사 실무에서 interceptor를 접하면서 이 프로젝트에도 interceptor로 코드를 수정하기로 결정했다.

Axios Interceptor

Promise 기반의 HTTP 요청을 처리하는 클라이언트 단에서 사용하는 라이브러리인 axios의 기능 중 하나인 interceptor는 API 요청이나 응답을 가로채고, 추가 작업을 수행할 수 있도록 도와주는 기능이다. 요청을 보내기 전이나 응답을 받은 뒤에, 필요한 로직을 추가하여 코드 중복을 줄이고, 전역적으로 설정을 관리하므로 유지보수 측면에서 매우 용이하다.

Interceptor 종류

  • 요청 인터셉터: 서버로 요청을 보내기 전에 요청을 가로채서 추가적인 로직을 수행하거나 에러 처리를 할 수 있다.
  • 응답 인터셉터: 서버로부터 응답을 받은 뒤에 응답을 가로채서 필요한 로직을 수행하거나 에러 처리를 할 수 있다.

아래 코드는 기존의 코드를 interceptor를 이용하여 수정한 코드이다.

import axios from 'axios';
import { toast } from 'react-toastify';
import useAuthStore from '@store/authStore';

let isToastVisible = false; // 전역 플래그 변수

// 토스트 메시지 표시 함수
const showToast = (message) => {
  if (!isToastVisible) {
    isToastVisible = true;
    toast.error(message);
    setTimeout(() => {
      isToastVisible = false;
    }, 3000);
  }
};

export const API = axios.create({
  baseURL: 'http://127.0.0.1:8080',
  timeout: 30000,
  withCredentials: true
});

// 요청 인터셉터
API.interceptors.request.use(
  (config) => {
    const accessToken = useAuthStore.getState().accessToken;
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);


// 응답 인터셉터
API.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error.config;

    // 재전송 방지 플래그
    if (error.response && error.response.status === 403) {
      if (!originalRequest._retry) {
        originalRequest._retry = true; // 재전송 플래그 설정
        try {
          const refreshResponse = await axios.get('http://127.0.0.1:8080/api/v1/member/refresh');
          if (refreshResponse.status === 200) {
            setAccessToken(refreshResponse.data.access_token);
            API.defaults.headers.common['Authorization'] = `Bearer ${refreshResponse.data.access_token}`;
            return API(originalRequest); // 원래 요청 재전송
          }
        } catch (refreshError) {
          showToast('로그인이 필요한 서비스입니다!');
          setTimeout(() => {
            window.location.href = 'http://127.0.0.1:3000/login';
          }, 3000);
        }
      }
    }

    // 일반 에러 처리
    if (error.response) {
      const errorMessage = error.response.data?.errorMessages?.errorMessage;
      if (errorMessage) {
        showToast(errorMessage);
      } else {
        showToast('알 수 없는 오류가 발생했습니다.');
      }
    } else if (error.request) {
      // 요청은 했지만 응답이 없는 경우
      showToast('서버에서 응답이 없습니다. 네트워크를 확인하세요.');
    } else {
      // 기타 에러
      showToast(`오류 발생: ${error.message}`);
    }

    return Promise.reject(error);
  }
);

// Access Token 설정 함수
const setAccessToken = (token) => {
  useAuthStore.getState().setAccessToken(token);
};

요청 인터셉터

  1. API.interceptors.request.use(...)

이 부분은 Axios의 요청 인터셉터를 설정하는 코드이다. API는 Axios 인스턴스를 의미하며, interceptors.request는 요청이 서버로 전송되기 전에 가로채는 기능을 제공한다.
use 메서드는 두 개의 콜백 함수를 인자로 받는다. 첫 번째는 요청을 처리하는 함수, 두 번째는 요청 에러를 처리하는 함수다.

  1. 첫 번째 콜백 함수 (config) => { ... }

이 함수는 요청이 서버로 전송되기 전에 호출된다. config 객체는 요청에 대한 모든 설정을 포함하고 있다.
이 함수 안에서 전역 상태로 저장되어있는 accessToken을 가져온다.
만약 토큰이 있다면, 요청 헤더에 Authorization 필드를 추가한다. Bearer는 토큰 인증 방식에서 일반적으로 사용하는 접두사로, accessToken과 함께 사용된다.
이렇게 하면, 서버는 요청을 받을 때 이 토큰을 확인하여 사용자를 인증할 수 있다. 마지막으로 수정된 config를 반환하며 axios가 서버로 요청을 보낼 때 수정된 config가 적용되어 요청을 진행한다.

  1. 두 번째 콜백 함수 (error) => { ... }

이 함수는 요청 중 발생한 에러를 처리한다. 요청이 실패하면 이 함수가 호출된다.
return Promise.reject(error); 코드는 에러를 다시 발생시켜, 호출한 곳에서 이 에러를 처리할 수 있도록 합니다.

응답 인터셉터

  1. API.interceptors.response.use(...)

이 부분은 Axios의 응답 인터셉터를 설정하는 코드다. 서버로부터 응답을 받은 후 이를 가로채는 기능을 제공한다.
use 메서드는 두 개의 콜백 함수를 인자로 받는다. 첫 번째는 정상적인 응답을 처리하는 함수, 두 번째는 에러를 처리하는 함수다.

  1. 첫 번째 콜백 함수 (response) => { ... }

이 함수는 서버로부터의 응답이 성공적일 때 호출되며 응답을 그대로 반환한다.
이 부분은 추가적인 처리가 필요 없을 때 간단하게 응답을 반환하는 역할을 한다.

  1. 두 번째 콜백 함수 (error) => { ... }

이 함수는 요청 중 에러가 발생했을 때 호출된다. 에러 객체를 인자로 받아서 처리한다.
const originalRequest = error.config; 에러가 발생한 원래 요청의 설정을 가져온다.이 정보를 사용하여 요청을 재전송할 수 있다.

  1. if (error.response && error.response.status === 403) { ... }
    서버 응답 상태 코드가 403(접근 금지)인 경우, 토큰을 갱신하기 위한 API 요청을 진행한다. 성공적인 응답을 받으면, 새로운 토큰을 기본 헤더에 추가한 뒤 새로운 토큰을 저장하고 원래 요청을 재전송한다.
const refreshResponse = await axios.get('http://127.0.0.1:8080/api/v1/member/refresh');

특히, 토큰을 갱신하는 API 요청 부분에서 기존에 사용하던 axios 인스턴스(내 코드에서는 API 인스턴스)를 사용해서 요청하게 되면 요청 인터셉터에서 갱신되지 않은 토큰을 헤더에 실어 요청을 보내기 때문에 계속 403에러가 반환될 수 있다. (이 부분에서 에러 무한루프에 걸리면서 애를 좀 먹었다...ㅎ) 따라서 나같은 경우는 인터셉터가 적용된 인스턴스가 아니라, 새로 axios 인스턴스를 이용하여 요청을 진행했다.

무한 403 에러

로그인 후 프로필 조회하는 영상

영상을 통해 로그인이 되어 있지 않으면 로그인 페이지로 이동시키고, 로그인 완료 후에 프로필 조회 요청이 제대로 되고 있는 것을 확인할 수 있다.

profile
안녕하세요. 웹 프론트엔드 개발자 앞잡이 '꼬마돌' 입니다.

0개의 댓글