리액트

[axios] 인터셉터(intercepter)

나는시화 2025. 1. 7. 22:06

사용한 프레임워크

axios, react-cookie

1. 왜 사용했는지?

만료시간이 다 된 토큰을 사용했을 때, 자동으로 represh token으로 access token을 재발급 받기 위해서 사용함. 

토큰은 cookie에 담아서 사용하기 때문에 react-cookie를 사용함. 

인터셉터(Interceptor)란? 요청(Request) 또는 응답(Response)이 애플리케이션에서 처리되기 전에 가로채어 특정 작업을 수행할 수 있도록 해주는 중간 처리 메커니즘

 

2. 사용 방법

2-1 먼저 cookie를 전역에서 사용하기 위해 최상위 컴포넌트에 <CookiesProvider>를 감싸준다. 

// App.tsx
useAxiosInterceptor();
return (
    <>
        <CookiesProvider>
            <Routes>
                <Route element={<MainContent/>}>
                    <Route path={MAIN_PATH()} element={<Main/>}/>
                    <Route path={SIGNUP_PATH()} element={<SignUp/>}/>
                    <Route path={LOGIN_PATH()} element={<Login/>}/>
                    <Route path={TEST_PATH()} element={<Test/>}/>
                    <Route path={CATEGORY_PATH()} element={<Category/>}/>
                </Route>
            </Routes>
        </CookiesProvider>
    </>
);

 

2-2 useAxiosIntercepter.tsx 작성

import React from 'react';
import axios from 'axios';
import { useCookies } from 'react-cookie';
import {LOGIN_PATH} from "../constant";

const API_DOMAIN = process.env.REACT_APP_SERVER_DOMAIN + "/api";

// 커스텀 훅으로 인터셉터 관리
export const useAxiosInterceptor = () => {
    const [cookies, setCookie, removeCookie] = useCookies(['accessToken', 'refreshToken']);
    React.useEffect(() => {
        // 요청 인터셉터
        // const requestInterceptor = axios.interceptors.request.use(
        //     (config) => {
        //         const accessToken = cookies.accessToken;
        //         console.log(cookies);
        //         debugger;
        //         if (accessToken) {
        //             config.headers.Authorization = `Bearer ${accessToken}`;
        //         }
        //         return config;
        //     },
        //     (error) => Promise.reject(error)
        // );

        // 응답 인터셉터
        const responseInterceptor = axios.interceptors.response.use(
            (response) => response,
            async (error) => {
                const originalRequest = error.config;
                if (error.response?.status === 401 && !originalRequest._retry && error.response?.data.code === 'EJT') {
                    originalRequest._retry = true;

                    try {
                        if (!cookies.refreshToken) {
                            console.log("refreshToken이 없습니다.");
                            return;
                        }
                        const refreshToken = cookies.refreshToken;
                        const response = await axios.post(`${API_DOMAIN}/auth/refresh`, { refreshToken });

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

                        const now = new Date().getTime();
                        const accessTokenExpires = new Date(now + accessTokenExpiredMs);
                        const refreshTokenExpires = new Date(now + refreshTokenExpiredMs);
                        // 새 토큰들 쿠키에 설정
                        setCookie('accessToken', accessToken, {
                            path: '/',
                            expires: accessTokenExpires
                        });

                        if (newRefreshToken) {
                            setCookie('refreshToken', newRefreshToken, {
                                path: '/',
                                expires: refreshTokenExpires
                                // 리프레시 토큰 만료 시간 설정
                            });
                        }

                        // 원래 요청 재시도
                        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
                        return axios(originalRequest);
                    } catch (refreshError) {
                        // 토큰 갱신 실패 시 로그아웃 처리
                        removeCookie('accessToken');
                        removeCookie('refreshToken');

                        // 로그인 페이지로 리다이렉트 등 추가 처리
                        window.location.href = LOGIN_PATH();

                        return Promise.reject(refreshError);
                    }
                }

                return Promise.reject(error);
            }
        );

        // 클린업 함수: 컴포넌트 언마운트 시 인터셉터 제거
        return () => {
            // axios.interceptors.request.eject(requestInterceptor);
            axios.interceptors.response.eject(responseInterceptor);
        };
    }, [cookies.accessToken, cookies.refreshToken, setCookie, removeCookie]);
};

 

API 서버에서 만료된 토큰일 경우 401 응답 값과 EJT 에러 코드를 반환을 했으며, 다시 요청하지 않았을 경우에만 ${API_DOMAIN}/auth/refresh에 refreshToken을 보내서 accessToken을 요청

if (error.response?.status === 401 && !originalRequest._retry && error.response?.data.code === 'EJT') {

 

// 재귀요청 방지
originalRequest._retry = true
// refreshToken이 없으면 요청X
if (!cookies.refreshToken) {
    console.log("refreshToken이 없습니다.");
    return;
}
// accessToken이 필요한 api를 호출하니깐 원래 요청을 다시 요청할 때 header에 accessToken을 담아서 요청
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest); // 원래 요청 재시도

 


현재는 response 인터셉터만 필요하기 때문에 해당 내용만 정리. 

request는 필요하면 나중에 추가 정리.