Axios 완벽 가이드: HTTP 클라이언트 라이브러리
이 글의 핵심
Axios는 Promise 기반의 HTTP 클라이언트로 인터셉터, 자동 JSON 변환, 요청 취소, 타임아웃 등 Fetch API보다 편리한 기능을 제공합니다. 브라우저와 Node.js에서 모두 사용 가능하며, TypeScript 지원이 우수하고 에러 처리가 직관적입니다.
Axios란?
Axios는 브라우저와 Node.js를 위한 Promise 기반 HTTP 클라이언트입니다. XMLHttpRequest(브라우저)와 http 모듈(Node.js)을 래핑하여 일관된 API를 제공합니다.
핵심 특징
-
Promise 기반
- async/await 지원
- 체이닝 가능
- 에러 처리 용이
-
자동 변환
- JSON 자동 변환
- Request/Response 변환
- Data transforming
-
인터셉터
- Request 가로채기
- Response 가로채기
- 전역 에러 처리
-
풍부한 기능
- 요청 취소
- 타임아웃
- CSRF 보호
- 진행률 추적
설치
Axios는 npm, yarn, pnpm 등 다양한 패키지 매니저로 설치할 수 있습니다. 프로젝트 환경에 맞는 방법을 선택하세요. npm을 사용하는 경우 아래 명령어로 최신 버전의 Axios를 설치하면 됩니다.
설치 후 package.json에 자동으로 의존성이 추가되며, TypeScript를 사용한다면 별도의 타입 정의 패키지 설치 없이 바로 사용할 수 있습니다. Axios는 자체적으로 TypeScript 타입을 포함하고 있어 개발 경험이 우수합니다.
# npm
npm install axios
# yarn
yarn add axios
# pnpm
pnpm add axios
# CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
CDN 방식은 프로토타이핑이나 간단한 테스트에 유용하지만, 프로덕션 환경에서는 번들러를 통한 설치를 권장합니다. 이를 통해 Tree-shaking으로 번들 크기를 최적화할 수 있습니다.
기본 사용법
GET 요청
import axios from 'axios';
// 기본 GET
const response = await axios.get('https://api.example.com/users');
console.log(response.data);
// Query parameters
const response = await axios.get('https://api.example.com/users', {
params: {
page: 1,
limit: 10,
sort: 'name'
}
});
// 구조 분해
const { data, status, headers } = await axios.get('/api/users');
POST 요청
// JSON 데이터
const response = await axios.post('https://api.example.com/users', {
name: '홍길동',
email: 'hong@example.com'
});
// FormData
const formData = new FormData();
formData.append('name', '홍길동');
formData.append('file', fileInput.files[0]);
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
PUT/PATCH/DELETE
// PUT (전체 업데이트)
await axios.put('/api/users/1', {
name: '김철수',
email: 'kim@example.com'
});
// PATCH (부분 업데이트)
await axios.patch('/api/users/1', {
name: '김철수'
});
// DELETE
await axios.delete('/api/users/1');
Axios 인스턴스
// 커스텀 인스턴스 생성
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 사용
const users = await api.get('/users');
const user = await api.get('/users/1');
인터셉터
Request 인터셉터
// JWT 토큰 자동 추가
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 로딩 상태 관리
api.interceptors.request.use(
(config) => {
store.dispatch({ type: 'LOADING_START' });
return config;
}
);
Response 인터셉터
// 자동 토큰 리프레시
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post('/api/refresh', {
refreshToken: localStorage.getItem('refreshToken')
});
localStorage.setItem('token', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// 리프레시 실패 → 로그아웃
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// 에러 정규화
api.interceptors.response.use(
(response) => response,
(error) => {
const message = error.response?.data?.message || error.message;
const status = error.response?.status;
// 전역 에러 토스트
toast.error(message);
return Promise.reject({ message, status });
}
);
에러 처리
try {
const response = await axios.get('/api/users/999');
} catch (error) {
if (axios.isAxiosError(error)) {
// Axios 에러
if (error.response) {
// 서버 응답 있음 (4xx, 5xx)
console.log('Data:', error.response.data);
console.log('Status:', error.response.status);
console.log('Headers:', error.response.headers);
} else if (error.request) {
// 요청 전송됨, 응답 없음
console.log('No response:', error.request);
} else {
// 요청 설정 중 에러
console.log('Error:', error.message);
}
} else {
// 일반 에러
console.error(error);
}
}
요청 취소
// AbortController 사용 (권장)
const controller = new AbortController();
axios.get('/api/users', {
signal: controller.signal
});
// 취소
controller.abort();
// 예: 컴포넌트 언마운트 시
useEffect(() => {
const controller = new AbortController();
axios.get('/api/data', { signal: controller.signal })
.then(res => setData(res.data));
return () => controller.abort();
}, []);
동시 요청
// Promise.all 사용
const [users, posts, comments] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments')
]);
// axios.all (deprecated)
axios.all([
axios.get('/api/users'),
axios.get('/api/posts')
])
.then(axios.spread((users, posts) => {
console.log(users.data, posts.data);
}));
파일 업로드
// 단일 파일
const formData = new FormData();
formData.append('file', file);
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`Upload: ${percent}%`);
}
});
// 여러 파일
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
await axios.post('/api/upload-multiple', formData);
TypeScript 지원
import axios, { AxiosResponse, AxiosError } from 'axios';
// 응답 타입 지정
interface User {
id: number;
name: string;
email: string;
}
const response: AxiosResponse<User> = await axios.get<User>('/api/users/1');
const user: User = response.data;
// 에러 타입 지정
try {
await axios.get('/api/users/1');
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ message: string }>;
console.log(axiosError.response?.data.message);
}
}
// 커스텀 인스턴스 타입
interface ApiError {
message: string;
code: string;
}
const api = axios.create<any, AxiosResponse<any>, ApiError>({
baseURL: 'https://api.example.com'
});
재시도 로직
// axios-retry 플러그인
import axiosRetry from 'axios-retry';
axiosRetry(axios, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429;
}
});
// 수동 구현
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await axios.get(url, options);
} catch (error) {
if (i === maxRetries - 1) throw error;
// 지수 백오프
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
React에서 사용
// Custom Hook
import { useState, useEffect } from 'react';
import axios from 'axios';
function useAxios(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
axios.get(url, { signal: controller.signal })
.then(res => setData(res.data))
.catch(err => {
if (!axios.isCancel(err)) {
setError(err);
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 사용
function UserList() {
const { data, loading, error } = useAxios('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
베스트 프랙티스
1. 중앙화된 API 클라이언트
실무에서는 여러 컴포넌트에서 API를 호출하기 때문에 중앙화된 API 클라이언트를 만드는 것이 중요합니다. 이를 통해 인증 토큰, 에러 처리, 로딩 상태 등을 한 곳에서 관리할 수 있습니다.
아래 예제는 실제 프로젝트에서 자주 사용하는 패턴입니다. baseURL을 환경 변수로 관리하고, timeout을 설정하며, 모든 요청에 JWT 토큰을 자동으로 추가합니다. 또한 401 에러 발생 시 자동으로 로그인 페이지로 리다이렉트하는 기능도 포함되어 있습니다.
// api/client.js
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 인터셉터
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.clear();
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
// api/users.js
import api from './client';
export const getUsers = () => api.get('/users');
export const getUser = (id) => api.get(`/users/${id}`);
export const createUser = (data) => api.post('/users', data);
export const updateUser = (id, data) => api.put(`/users/${id}`, data);
export const deleteUser = (id) => api.delete(`/users/${id}`);
이렇게 API 함수를 별도 파일로 분리하면 컴포넌트가 깔끔해지고 재사용성이 높아집니다.
2. 환경별 설정
개발 환경과 프로덕션 환경에서 다른 API URL과 timeout을 사용해야 하는 경우가 많습니다. 환경 변수를 활용하면 이를 쉽게 관리할 수 있습니다.
개발 환경에서는 로컬 서버(localhost:5000)를 사용하고 timeout을 길게 설정해 디버깅을 편하게 합니다. 프로덕션 환경에서는 실제 API 서버를 사용하고 timeout을 짧게 설정해 사용자 경험을 개선합니다.
// config/axios.js
const config = {
development: {
baseURL: 'http://localhost:5000/api',
timeout: 30000,
},
production: {
baseURL: 'https://api.example.com',
timeout: 10000,
}
};
const env = process.env.NODE_ENV || 'development';
export default axios.create(config[env]);
실전 사례: 인증이 필요한 대시보드 구축
실무에서 가장 자주 만나는 시나리오는 JWT 인증이 필요한 대시보드 애플리케이션입니다. 로그인 후 토큰을 받아 저장하고, 이후 모든 요청에 토큰을 자동으로 포함해야 합니다. 또한 토큰이 만료되면 자동으로 리프레시하는 로직도 필요합니다.
토큰 리프레시 자동화
JWT 토큰은 보안을 위해 짧은 만료 시간(15분~1시간)을 가집니다. 토큰이 만료될 때마다 사용자가 다시 로그인하는 것은 좋지 않은 경험이므로, refresh token을 사용해 자동으로 새 토큰을 발급받아야 합니다.
아래 코드는 401 에러 발생 시 자동으로 refresh token을 사용해 새 access token을 발급받고, 실패한 요청을 재시도하는 로직입니다. _retry 플래그를 사용해 무한 루프를 방지합니다.
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000
});
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const { data } = await axios.post('/api/auth/refresh', {
refreshToken
});
localStorage.setItem('token', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
전역 로딩 상태 관리
API 요청이 진행 중일 때 로딩 스피너를 표시하는 것은 좋은 사용자 경험입니다. 인터셉터를 사용하면 모든 요청에 대해 자동으로 로딩 상태를 관리할 수 있습니다.
let activeRequests = 0;
api.interceptors.request.use((config) => {
activeRequests++;
store.dispatch({ type: 'LOADING_START' });
return config;
});
api.interceptors.response.use(
(response) => {
activeRequests--;
if (activeRequests === 0) {
store.dispatch({ type: 'LOADING_END' });
}
return response;
},
(error) => {
activeRequests--;
if (activeRequests === 0) {
store.dispatch({ type: 'LOADING_END' });
}
return Promise.reject(error);
}
);
이 패턴을 사용하면 여러 요청이 동시에 진행되어도 모든 요청이 완료될 때까지 로딩 스피너가 표시됩니다.
주의사항과 팁
1. 메모리 누수 방지
React에서 컴포넌트가 언마운트된 후에도 API 요청이 완료되면 setState를 호출하려고 해서 메모리 누수 경고가 발생할 수 있습니다. AbortController를 사용해 컴포넌트 언마운트 시 요청을 취소하세요.
2. CORS 문제 해결
개발 환경에서 CORS 에러가 자주 발생합니다. 백엔드에서 CORS 헤더를 설정하거나, 개발 서버의 프록시 기능을 사용하세요. Create React App의 경우 package.json에 “proxy” 필드를 추가하면 됩니다.
3. 보안 고려사항
토큰을 localStorage에 저장하면 XSS 공격에 취약합니다. 민감한 애플리케이션의 경우 httpOnly 쿠키를 사용하는 것이 더 안전합니다. 또한 HTTPS를 반드시 사용하고, 민감한 데이터는 암호화해서 전송하세요.
Axios는 간편하고 강력한 HTTP 클라이언트입니다. 인터셉터와 에러 처리로 안정적인 API 통신을 구축할 수 있습니다. 실무에서는 중앙화된 API 클라이언트를 만들고, 토큰 리프레시 로직을 추가하며, 로딩 상태를 전역으로 관리하는 것이 일반적인 패턴입니다.