📌 개선 이유

백엔드와 통신시 인증이 필요한 경우 아래와 같이 헤더에 토큰을 넣어서 메서드를 요청해야 한다.

매 요청 작업마다 작성하면서도 '이걸 이렇게 다 쓰는 게 맞나?' 'spring의 인터셉터 같은 게 있지않을까?' 하고 생각했었다.하지만 무지했었기 때문에 일단 일일이 다 작성했던 상태였다.그리고 이제 리팩토링을 해볼까 하는 시점에서 편리한 방법이 있는지 검색해보았고,  Axios Interceptor 를 알게 되었다.

 

 

📌 Axios Interceptor 란?

Axios Interceptor는 Axios에서 HTTP 요청과 응답을 가로채고, 이를 처리하거나 변형할 수 있는 기능을 제공한다. 이 기능은 요청을 보내기 전이나 응답을 받은 후에 특정 작업을 수행해야 할 때 매우 유용하고, 주로 다음과 같은 상황일 때 사용된다.

  1. 공통 헤더 추가: 모든 요청에 공통적으로 사용되는 헤더(예: 인증 토큰)를 추가할 때.
  2. 로딩 스피너 표시: 요청이 진행 중일 때 로딩 스피너를 표시하고, 응답을 받은 후 스피너를 숨길 때.
  3. 에러 처리: 응답에서 에러가 발생했을 때 공통적인 에러 처리 로직을 적용할 때.
  4. 응답 데이터 변형: 서버에서 받은 데이터를 클라이언트에서 사용할 수 있는 형태로 변형할 때.

즉, 나의 경우

인증이 필요한 모든 axios 요청에서 직접 헤더를 넣는 귀찮은 반복 작업을 axios interceptor를 이용하여 해결할 수 있다 !!

 

 

📌 AxiosInstance 생성

1. 의존성 및 초기 설정

import axios from 'axios';
import { useCookies } from 'react-cookie';
import { useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';

 

2. Axios 인스턴스 설정

const AxiosInstance = () => {
    const [cookies, setCookies, removeCookies] = useCookies(['accessToken']);
    const navigate = useNavigate();
    const [isRefreshing, setIsRefreshing] = useState(false);  // 현재 토큰 갱신 요청이 진행 중인지 여부
    const [refreshSubscribers, setRefreshSubscribers] = useState([]);  // 토큰 갱신이 완료된 후 다시 시도해야 할 요청들을 저장

 

3. Axios 인스턴스 생성

    useEffect(() => {
        console.log('AxiosInstance - 쿠키에서 읽은 accessToken:', cookies.accessToken);
    }, [cookies]);

    const axiosInstance = axios.create({
        headers: { "Content-Type": "application/json" },  // 기본 헤더 설정
        baseURL: 'http://localhost:8082/',  // 기본 URL 설정
        withCredentials: true  // 크로스 도메인 요청에 쿠키를 포함
    });

 

4. 토큰 갱신 및 요청 구독 함수

    const subscribeTokenRefresh = (cb) => {
        setRefreshSubscribers((prevSubscribers) => [...prevSubscribers, cb]);
    };

    const onRefreshed = (newAccessToken) => {
        refreshSubscribers.forEach((cb) => cb(newAccessToken));
        setRefreshSubscribers([]);
    };

 

5. 액세스 토큰 갱신 함수

    const refreshAccessToken = async () => {
        try {
            const response = await axios.post('http://localhost:8082/api/v1/auth/n/refreshToken', {
                accessToken: cookies.accessToken
            }, {
                withCredentials: true,
            });

            const newAccessToken = response.data.accessToken;
            setCookies('accessToken', newAccessToken, { path: '/' });
            return newAccessToken;
        } catch (error) {
            removeCookies('accessToken', { path: '/' });
            navigate('/login');
            throw error;
        }
    };

 

6. 요청 인터셉터

    axiosInstance.interceptors.request.use(
        (config) => {
            if (!config.url.includes('/api/v1/auth/n/login') && cookies.accessToken) {
                config.headers.Authorization = `Bearer ${cookies.accessToken}`;
            }
            return config;
        },
        (error) => Promise.reject(error)
    );

 

7. 응답 인터셉터

    axiosInstance.interceptors.response.use(
        (response) => response,
        async (error) => {
            const statusCode = error.response?.status;
            const originalRequest = error.config;

            if (statusCode === 401 && !originalRequest._retry) {
                if (isRefreshing) {
                    return new Promise((resolve) => {
                        subscribeTokenRefresh((newAccessToken) => {
                            originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
                            resolve(axiosInstance(originalRequest));
                        });
                    });
                }

                originalRequest._retry = true;
                setIsRefreshing(true);

                try {
                    const newAccessToken = await refreshAccessToken();
                    onRefreshed(newAccessToken);
                    originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
                    return axiosInstance(originalRequest);
                } catch (refreshError) {
                    return Promise.reject(refreshError);
                } finally {
                    setIsRefreshing(false);
                }
            }

            return Promise.reject(error); 
        }
    );

 

8. Axios 인스턴스 반환

    return axiosInstance;  
};

export default AxiosInstance;

 

 

Axios 인터셉터를 사용하여 자동으로 액세스 토큰을 갱신하고, 갱신된 토큰으로 실패한 요청을 재시도할 수 있도록 한다.

이를 통해 사용자는 로그인을 다시 하지 않아도 자동으로 토큰이 갱신되고 요청이 처리되는 경험을 할 수 있게 된다.

 


 

📌 AxiosInstance 적용

컴포넌트에서 Axios 인스턴스 사용하면 된다.

그리고 일일이 헤더에 토큰을 넣어줬던 부분을 삭제했다!

import { useAxios } from '../AxiosContext'; // Axios 인스턴스 가져오기
const axios = useAxios(); // Axios 인스턴스 사용

 

예시)

 

 


 

📌 결론

이번 작업에서는 Axios 인스턴스를 생성하고, 이를 통해 HTTP 요청 및 응답을 관리하는 방식을 구현했다. 특히, 액세스 토큰을 갱신하고 이를 재사용하는 로직을 포함하여, 인증 시스템을 보다 효율적으로 관리할 수 있게 되었다.

 

만약 이 방법을 진작에 알았다면 처음부터 일일이 헤더를 작성하지 않았을 텐데...😓 하고 잠시 자책했지만, 그런 비효율적인 경험이 오히려 도움이 되었다. 번거로움과 필요성을 직접 체감했기 때문에 스스로 찾아보고, 적용해보면서 얻은 것이 중요하다고 생각한다.

그리고 나는 이러한 유지보수성과 자동화가 개선되는 부분에서 재미와 쾌감을 느끼게 되는 것 같다..! 

'Project 댕린이집' 카테고리의 다른 글

[Refactoring] Redis를 사용해 Refresh Token 관리해보자  (0) 2024.07.25
[소셜로그인] 카카오 로그인  (0) 2024.07.03
[알림장] 작성 / 조회  (0) 2024.07.01
[게시판] 댓글  (0) 2024.06.29
[게시판] 좋아요  (0) 2024.06.28
xoo | 수진