기존의 Axios요청
기존의 axios요청은 다음과 같이 진행되었다.
await axios.post('URL' ,{});
중복된 URL입력이 필요했으며, 토큰 검증과정은 각 로직에 중복되어 들어갔다.
이에 Axios인스턴드를 생성하여 baseURL을 지정한 뒤 interceptor를 등록하여 중복된 토큰 유효성 검사 로직을 간소화 할 수 있으며 토큰 유효성 검사도중 프로미스들을 큐에 넣어 토큰 유효성 검사가 끝난 뒤(리프레시 토큰을 통해 새로운 토큰을 재발급 받은 뒤) 큐에 있는 프로미스들을 실행시킬 수 있다.
Axios Instance생성하기
생성한 Axios Instance를 통해 요청 시 baseURL이 지정되어 URL을 중복 입력할 필요가 없다.
import axios from 'axios';
const API_URL = process.env.EXPO_PUBLIC_BACKEND_API;
// Axios 인스턴스 생성
export const api = axios.create({
baseURL: API_URL, // 모든 API 요청에 이 baseURL이 자동으로 붙음
});
토큰 갱신 중에 실패했던 요청들을 모아둔 큐를 처리하는 함수
토큰이 새로 갱신된 경우 새 토큰으로 재시도할 수 있도록 resolve해주며 토큰 갱신에 실패한 경우 모두 실패 처리한다.
import { useEffect } from 'react';
import { useAuth } from '@/api/auth';
import { useStorageState } from '@/hooks/useStorageState';
import { api } from './axiosInstance';
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
console.log('📦 processQueue:', {
error,
tokenPreview: token?.slice?.(0, 20) || null,
queueLength: failedQueue.length,
});
failedQueue.forEach(prom => {
if (token) prom.resolve(token);
else prom.reject(error);
});
failedQueue = [];
};
갱신 도중 대기중인 모든 요청을 처리한 뒤 큐를 초기화 하는 로직이다.
useAxiosInterceptor 커스텀 훅
커스텀 훅에는 interceptor가 2개 있다. 인터셉터는 요청을 가로채어 조작을 하거나 특정 응답이 발생한 경우 해당로직을 적용할 수 있다.
가령 클라이언트가 토큰이 유효하지 않다는 응답을 받은 경우 클라이언트는 리프레시 토큰을 서버로 보내어 새로운 리프레시 토큰과 토큰을 발급받아야 한다. 이 경우엔 응답 인터셉터가 작동하는 것이다.
export const useAxiosInterceptor = () => {
const [[isLoading, session], setSession] = useStorageState('session');
const { logoutUser } = useAuth();
useEffect(() => {
console.log('--- 인터셉터 useEffect 실행 ---');
// ✅ 세션이 완전히 로딩되기 전에는 인터셉터 등록하지 않음
if (isLoading || !session) {
console.log('* 세션 로딩 중이거나 없음 → 인터셉터 등록 생략');
return;
}
console.log('✅ Axios 인터셉터 등록됨');
const requestInterceptor = api.interceptors.request.use(config => {
console.log('* 인터셉터에서 세션 토큰 확인:', session.token);
if (session?.token) {
/// 요청 헤더에 토큰을 추가함.
config.headers.Authorization = `Bearer ${session.token}`;
console.log('* 요청에 토큰 추가됨:', session.token.slice(0, 20) + '...');
}
return config;
});
const responseInterceptor = api.interceptors.response.use(
res => res,
async err => {
/// 요청 오류가 401 Unauthorized이고, 토큰 갱신이 필요한 경우 처리함
const originalRequest = err.config;
if (err.response?.status === 401 && !originalRequest._retry) {
console.warn('🔐 401 Unauthorized → 토큰 갱신 시도 중...');
originalRequest._retry = true;
if (isRefreshing) {
console.log('🕒 이미 갱신 중 → 요청 큐에 등록');
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token: unknown) => {
console.log('🟢 새 토큰으로 요청 재시도');
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch(error => Promise.reject(error));
}
isRefreshing = true;
try {
const identifier = session.user?.identifier;
const refreshToken = session?.RT;
console.log('🧪 identifier:', identifier, typeof identifier);
console.log('🧪 refreshToken:', refreshToken, typeof refreshToken);
if (
typeof identifier !== 'string' ||
!identifier.trim() ||
typeof refreshToken !== 'string' ||
!refreshToken.trim()
) {
console.error('❌ 세션 정보 부족 → 토큰 갱신 불가');
console.log('⚠️ session:', JSON.stringify(session, null, 2));
throw new Error('세션 정보 누락');
}
console.log(`🔄 토큰 갱신 요청: /api/tokens/refresh`);
const res = await api.post(`/api/tokens/refresh`, { RT: refreshToken });
const newToken = res.data.token;
const newRT = res.data.RT;
console.log('✅ 토큰 갱신 성공');
console.log('🆕 AccessToken:', newToken.slice(0, 20) + '...');
console.log('🆕 RefreshToken:', newRT.slice(0, 20) + '...');
await setSession({
token: newToken,
RT: newRT,
user: session.user,
});
originalRequest.headers.Authorization = `Bearer ${newToken}`;
processQueue(null, newToken);
return api(originalRequest);
} catch (refreshError: any) {
const code = refreshError?.response?.data?.code;
console.error('❌ 토큰 갱신 실패:', refreshError?.response?.data);
if (code === 'T002') {
console.warn('🔒 RefreshToken 만료 → 로그아웃 실행');
logoutUser(session.user?.identifier);
}
processQueue(refreshError, null);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(err);
}
);
return () => {
console.log('🧹 Axios 인터셉터 해제됨');
api.interceptors.request.eject(requestInterceptor);
api.interceptors.response.eject(responseInterceptor);
};
}, [isLoading, session]); // ✅ isLoading도 의존성에 추가
};
요청 인터셉터 구현 부분에 대한 설명
모든 Axios 요청에 세션의 AccessToken을 자동으로 포함시키는 역할을 수행한다.
const requestInterceptor = api.interceptors.request.use(config => {
console.log('* 인터셉터에서 세션 토큰 확인:', session.token);
if (session?.token) {
/// 요청 헤더에 토큰을 추가함.
config.headers.Authorization = `Bearer ${session.token}`;
console.log('* 요청에 토큰 추가됨:', session.token.slice(0, 20) + '...');
}
return config;
});
api는 Axios Instance이며 api.interceptor.request.use를 통해 config를 받아 이를 이용하여 헤더를 수정한다.
응답 인터셉터 구현 부분에 대한 설명
const responseInterceptor = api.interceptors.response.use(
res => res,
async err => {
/// 요청 오류가 401 Unauthorized이고, 토큰 갱신이 필요한 경우 처리함
const originalRequest = err.config;
if (err.response?.status === 401 && !originalRequest._retry) {
console.warn('🔐 401 Unauthorized → 토큰 갱신 시도 중...');
originalRequest._retry = true;
if (isRefreshing) {
console.log('🕒 이미 갱신 중 → 요청 큐에 등록');
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token: unknown) => {
console.log('🟢 새 토큰으로 요청 재시도');
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch(error => Promise.reject(error));
}
isRefreshing = true;
try {
const identifier = session.user?.identifier;
const refreshToken = session?.RT;
console.log('🧪 identifier:', identifier, typeof identifier);
console.log('🧪 refreshToken:', refreshToken, typeof refreshToken);
if (
typeof identifier !== 'string' ||
!identifier.trim() ||
typeof refreshToken !== 'string' ||
!refreshToken.trim()
) {
console.error('❌ 세션 정보 부족 → 토큰 갱신 불가');
console.log('⚠️ session:', JSON.stringify(session, null, 2));
throw new Error('세션 정보 누락');
}
console.log(`🔄 토큰 갱신 요청: /api/tokens/refresh`);
const res = await api.post(`/api/tokens/refresh`, { RT: refreshToken });
const newToken = res.data.token;
const newRT = res.data.RT;
console.log('✅ 토큰 갱신 성공');
console.log('🆕 AccessToken:', newToken.slice(0, 20) + '...');
console.log('🆕 RefreshToken:', newRT.slice(0, 20) + '...');
await setSession({
token: newToken,
RT: newRT,
user: session.user,
});
originalRequest.headers.Authorization = `Bearer ${newToken}`;
processQueue(null, newToken);
return api(originalRequest);
} catch (refreshError: any) {
const code = refreshError?.response?.data?.code;
console.error('❌ 토큰 갱신 실패:', refreshError?.response?.data);
if (code === 'T002') {
console.warn('🔒 RefreshToken 만료 → 로그아웃 실행');
logoutUser(session.user?.identifier);
}
processQueue(refreshError, null);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(err);
}
);
1. 먼저 정상적은 응답의 경우 응답 인터셉터는 response를 그냥 넘기지만 에러가 발생한 경우 에러의 유형을 검사한다.
2. 에러의 유형이 리프레시 토큰을 통해 토큰 재발급 요청을 해야하는 경우라면 현재 세션에서 아이디와 리프레시 토큰을 가져와서 리프레시 토큰 검증 요청을 수행한다.
3. 새로운 토큰을 정상적으로 받은 경우에 대해서 세션을 업데이트 해준다.
4. 만약 리프레시 토큰이 유효하지 않은 경우 로그아웃을 수행한다.


'React' 카테고리의 다른 글
| Expo 앱 초기화, RN프로젝트 시작 (0) | 2025.03.06 |
|---|---|
| React Native 환경 변수 설정 방법 (0) | 2025.03.04 |
| React Native / 서버요청 커스텀 훅 만들기 (0) | 2025.02.27 |
| React Native / Typescript 에서의 Props 타입지정 (1) | 2025.02.27 |
| React Native / Prettier설정 (0) | 2025.02.23 |