[Javascript] Axios 인터셉터

인터셉터로 요청/응답 제어하기

📤 요청 인터셉터

// 모든 요청에 공통 로직 적용
axios.interceptors.request.use(
(config) => {
// 요청 시작 로그
console.log(`🚀 API 요청: ${config.method?.toUpperCase()} ${config.url}`);

// 인증 토큰 자동 추가
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}

// 요청 시간 기록 (성능 측정용)
config.metadata = { startTime: new Date() };

return config;
},
(error) => {
console.error('❌ 요청 설정 오류:', error);
return Promise.reject(error);
}
);

📥 응답 인터셉터

// 모든 응답에 공통 로직 적용
axios.interceptors.response.use(
(response) => {
// 응답 시간 계산
const endTime = new Date();
const duration = endTime - response.config.metadata.startTime;
console.log(`✅ API 응답: ${response.config.url} (${duration}ms)`);

return response;
},
async (error) => {
const originalRequest = error.config;

// 401 에러: 토큰 갱신 처리
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await [axios.post](http://axios.post)('/auth/refresh', {
refreshToken
});

const newToken = [response.data](http://response.data).accessToken;
localStorage.setItem('accessToken', newToken);

// 원래 요청에 새 토큰 적용 후 재시도
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);

} catch (refreshError) {
// 토큰 갱신 실패 시 로그인 페이지로 리다이렉트
localStorage.clear();
window.location.href = '/login';
}
}

// 에러 로깅
console.error('❌ API 에러:', {
url: error.config?.url,
status: error.response?.status,
message: error.message
});

return Promise.reject(error);
}
);

실전 예제: API 클래스 구현

🏢 사용자 관리 API 클래스

class UserService {
constructor() {
this.client = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
timeout: 10000
});

this.setupInterceptors();
}

setupInterceptors() {
// 요청 인터셉터: 인증 토큰 자동 추가
this.client.interceptors.request.use(config => {
const token = this.getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// 응답 인터셉터: 에러 처리
this.client.interceptors.response.use(
response => response,
error => this.handleError(error)
);
}

getAuthToken() {
return localStorage.getItem('authToken');
}

handleError(error) {
const message = error.response?.data?.message || error.message;
throw new Error(`API 요청 실패: ${message}`);
}

// 사용자 목록 조회
async getUsers(options = {}) {
const { page = 1, limit = 10, search = '' } = options;

const response = await this.client.get('/users', {
params: { page, limit, search }
});

return {
users: [response.data.data](http://response.data.data),
totalCount: [response.data.total](http://response.data.total),
currentPage: page
};
}

// 사용자 상세 조회
async getUserById(userId) {
const response = await this.client.get(`/users/${userId}`);
return [response.data](http://response.data);
}

// 사용자 생성
async createUser(userData) {
const response = await [this.client.post](http://this.client.post)('/users', userData);
return [response.data](http://response.data);
}

// 사용자 정보 수정
async updateUser(userId, userData) {
const response = await this.client.put(`/users/${userId}`, userData);
return [response.data](http://response.data);
}

// 사용자 삭제
async deleteUser(userId) {
await this.client.delete(`/users/${userId}`);
return { success: true, userId };
}

// 사용자 검색
async searchUsers(query) {
const response = await this.client.get('/users/search', {
params: { q: query }
});
return [response.data](http://response.data);
}
}

// 싱글톤 패턴으로 사용
export const userService = new UserService();

// 사용 예시
async function handleUserOperations() {
try {
// 사용자 목록 조회
const userList = await userService.getUsers({
page: 1,
limit: 20,
search: 'john'
});
console.log('사용자 목록:', userList);

// 새 사용자 생성
const newUser = await userService.createUser({
name: '김개발',
email: '[kim.dev@example.com](mailto:kim.dev@example.com)',
role: 'developer'
});
console.log('생성된 사용자:', newUser);

// 사용자 정보 수정
const updatedUser = await userService.updateUser([newUser.id](http://newUser.id), {
name: '김시니어',
role: 'senior-developer'
});
console.log('수정된 사용자:', updatedUser);

} catch (error) {
console.error('사용자 작업 중 오류:', error.message);
}
}

파일 업로드 및 다운로드

📁 파일 업로드

// 단일 파일 업로드
async function uploadFile(file, onProgress) {
const formData = new FormData();
formData.append('file', file);
formData.append('category', 'documents');
formData.append('description', [file.name](http://file.name));

try {
const response = await [axios.post](http://axios.post)('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / [progressEvent.total](http://progressEvent.total)
);
console.log(`업로드 진행률: ${percentCompleted}%`);
onProgress?.(percentCompleted);
}
});

return [response.data](http://response.data);
} catch (error) {
console.error('파일 업로드 실패:', error);
throw error;
}
}

// 다중 파일 업로드
async function uploadMultipleFiles(files) {
const formData = new FormData();

files.forEach((file, index) => {
formData.append(`files`, file);
});

const response = await [axios.post](http://axios.post)('/api/upload/multiple', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});

return [response.data](http://response.data);
}

// 사용 예시
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = [event.target](http://event.target).files[0];
if (file) {
try {
const result = await uploadFile(file, (progress) => {
console.log(`업로드 진행률: ${progress}%`);
});
console.log('업로드 완료:', result);
} catch (error) {
console.error('업로드 에러:', error);
}
}
});

📥 파일 다운로드

// 파일 다운로드
async function downloadFile(fileId, filename) {
try {
const response = await axios.get(`/api/files/${fileId}/download`, {
responseType: 'blob'
});

// Blob을 사용하여 파일 다운로드 트리거
const url = window.URL.createObjectURL(new Blob([[response.data](http://response.data)]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
[link.click](http://link.click)();
document.body.removeChild(link);

// 메모리 정리
window.URL.revokeObjectURL(url);

} catch (error) {
console.error('파일 다운로드 실패:', error);
}
}

에러 처리 베스트 프랙티스

🛡️ 포괄적인 에러 처리

// 에러 타입별 처리 함수
function handleApiError(error) {
if (error.response) {
// 서버가 응답했지만 상태 코드가 2xx 범위를 벗어남
const { status, data } = error.response;

switch (status) {
case 400:
console.error('잘못된 요청:', data.message);
break;
case 401:
console.error('인증 실패 - 로그인이 필요합니다');
// 로그인 페이지로 리다이렉트
window.location.href = '/login';
break;
case 403:
console.error('접근 권한이 없습니다');
break;
case 404:
console.error('요청한 리소스를 찾을 수 없습니다');
break;
case 429:
console.error('요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요');
break;
case 500:
console.error('서버 내부 오류가 발생했습니다');
break;
default:
console.error(`서버 오류 (${status}):`, data.message);
}

return { type: 'response', status, message: data.message };

} else if (error.request) {
// 요청은 전송되었지만 응답을 받지 못함 (네트워크 오류)
console.error('네트워크 오류: 서버에 연결할 수 없습니다');
return { type: 'network', message: '네트워크 연결을 확인해주세요' };

} else {
// 요청 설정 중에 오류 발생
console.error('요청 설정 오류:', error.message);
return { type: 'config', message: error.message };
}
}

// 재시도 로직이 포함된 API 호출
async function apiCallWithRetry(requestFn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
const errorInfo = handleApiError(error);

if (attempt === maxRetries || errorInfo.type === 'response') {
throw error; // 최대 재시도 횟수 도달 또는 서버 응답 오류
}

console.log(`재시도 ${attempt}/${maxRetries} - ${delay}ms 후 다시 시도...`);
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
}

// 사용 예시
async function fetchUserData(userId) {
return apiCallWithRetry(
() => axios.get(`/api/users/${userId}`),
3, // 최대 3번 재시도
1000 // 1초 간격
);
}

성능 최적화 팁

⚡ 요청 최적화 기법

// 1. 요청 취소 (AbortController 사용)
class ApiService {
constructor() {
this.pendingRequests = new Map();
}

async fetchData(endpoint, options = {}) {
// 이전 요청이 있다면 취소
if (this.pendingRequests.has(endpoint)) {
this.pendingRequests.get(endpoint).abort();
}

const controller = new AbortController();
this.pendingRequests.set(endpoint, controller);

try {
const response = await axios.get(endpoint, {
...options,
signal: controller.signal
});

this.pendingRequests.delete(endpoint);
return [response.data](http://response.data);

} catch (error) {
this.pendingRequests.delete(endpoint);

if (axios.isCancel(error)) {
console.log('요청이 취소되었습니다:', endpoint);
} else {
throw error;
}
}
}

// 모든 대기중인 요청 취소
cancelAllRequests() {
this.pendingRequests.forEach(controller => controller.abort());
this.pendingRequests.clear();
}
}

// 2. 요청 디바운싱 (검색 등에 유용)
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

// 검색 API 호출 최적화
const debouncedSearch = debounce(async (query) => {
if (query.trim()) {
const results = await axios.get('/api/search', {
params: { q: query }
});
console.log('검색 결과:', [results.data](http://results.data));
}
}, 300);

// 3. 동시 요청 제한
class RateLimitedApi {
constructor(maxConcurrent = 5) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}

async request(config) {
return new Promise((resolve, reject) => {
this.queue.push({ config, resolve, reject });
this.processQueue();
});
}

async processQueue() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}

this.running++;
const { config, resolve, reject } = this.queue.shift();

try {
const response = await axios(config);
resolve(response);
} catch (error) {
reject(error);
} finally {
this.running--;
this.processQueue(); // 다음 요청 처리
}
}
}

const rateLimitedApi = new RateLimitedApi(3); // 최대 3개 동시 요청

🎯 마무리

Axios는 JavaScript 생태계에서 가장 강력하고 유연한 HTTP 클라이언트 라이브러리입니다. 이 가이드에서 다룬 내용들을 실제 프로젝트에 적용하면 더욱 안정적이고 효율적인 API 통신을 구현할 수 있습니다.

핵심 포인트 정리

인스턴스 활용: 공통 설정을 가진 인스턴스로 코드 중복 제거
인터셉터 사용: 인증, 로깅, 에러처리 등 공통 로직 처리
에러 처리: 다양한 에러 상황에 대한 체계적인 대응
성능 최적화: 요청 취소, 디바운싱, 동시 요청 제한
타입 안정성: TypeScript와 함께 사용하여 더욱 안전한 코드 작성

Axios를 마스터하여 더 나은 웹 애플리케이션을 만들어보세요! 🚀


Share