# Axios 설치 및 사용 가이드라인
## 1. 설치
### 1.1 기본 설치
“`bash
# npm
npm install axios
# yarn
yarn add axios
# pnpm
pnpm add axios
# 버전 확인
npm list axios
“`
### 1.2 TypeScript 타입 설치 (TypeScript 사용시)
“`bash
# TypeScript 지원 (Axios는 기본 포함)
# 추가 설치 불필요
“`
### 1.3 선택적 패키지
“`bash
# API 모킹 (개발 환경)
npm install msw
# 재시도 로직
npm install axios-retry
# 캐싱
npm install axios-cache-adapter
“`
—
## 2. 프로젝트 구조
### 2.1 추천 디렉토리 구조
“`
src/
├── api/
│ ├── client.ts # Axios 인스턴스 설정
│ ├── interceptors.ts # 요청/응답 인터셉터
│ └── endpoints/
│ ├── user.ts # 사용자 API
│ ├── post.ts # 게시물 API
│ ├── comment.ts # 댓글 API
│ └── index.ts # 통합 내보내기
├── hooks/
│ └── useApiError.ts # 에러 처리 커스텀 훅
├── services/
│ └── auth.service.ts # 인증 서비스
└── types/
└── api.ts # API 타입 정의
“`
—
## 3. 기본 설정 (Axios 인스턴스)
### 3.1 Axios 클라이언트 생성
“`typescript
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from ‘axios’;
// 환경에 따른 베이스 URL
const API_BASE_URL = process.env.REACT_APP_API_URL || ‘http://localhost:3000/api’;
// Axios 인스턴스 생성
const client: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 10000, // 10초
headers: {
‘Content-Type’: ‘application/json’
}
});
export default client;
“`
### 3.2 고급 설정
“`typescript
// api/client.ts
import axios from ‘axios’;
const client = axios.create({
// 기본 설정
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
// 헤더 설정
headers: {
‘Content-Type’: ‘application/json’,
‘Accept’: ‘application/json’
},
// 요청 설정
withCredentials: true, // 쿠키 포함
validateStatus: (status) => status < 500, // 상태 코드 검증
// 재시도 설정
maxRetries: 3,
// 응답 변환
transformResponse: [
(data) => {
// 응답 데이터 변환
if (typeof data === ‘string’) {
try {
return JSON.parse(data);
} catch (e) {
return data;
}
}
return data;
}
]
});
export default client;
“`
—
## 4. 인터셉터 (Interceptors)
### 4.1 요청 인터셉터 (Request Interceptor)
“`typescript
// api/interceptors.ts
import client from ‘./client’;
import axios from ‘axios’;
// 요청 인터셉터
client.interceptors.request.use(
(config) => {
// ✅ 요청 전 실행
// 1. 토큰 추가
const token = localStorage.getItem(‘accessToken’);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 2. 요청 타임스탬프
config.headers[‘X-Request-ID’] = generateRequestId();
// 3. 로깅
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`);
console.log(‘Headers:’, config.headers);
return config;
},
(error) => {
// ❌ 요청 설정 오류
console.error(‘Request Config Error:’, error);
return Promise.reject(error);
}
);
// 응답 인터셉터
client.interceptors.response.use(
(response) => {
// ✅ 응답 성공 (200-299)
console.log(`[Response] ${response.status} ${response.config.url}`);
// 응답 데이터 추출
return response.data;
},
async (error) => {
// ❌ 응답 실패 (300+)
const { response, config } = error;
// 1. 토큰 만료 처리 (401)
if (response?.status === 401) {
console.warn(‘Token expired, attempting refresh…’);
// 토큰 갱신 시도
const refreshed = await refreshToken();
if (refreshed) {
// 원래 요청 재실행
return client(config);
}
// 갱신 실패 시 로그인 페이지로
window.location.href = ‘/login’;
}
// 2. 403 Forbidden
if (response?.status === 403) {
console.error(‘Access forbidden’);
}
// 3. 404 Not Found
if (response?.status === 404) {
console.error(‘Resource not found’);
}
// 4. 500+ Server Error
if (response?.status >= 500) {
console.error(‘Server error’);
}
console.error(`[Error] ${response?.status} ${config?.url}`);
console.error(‘Error:’, response?.data);
return Promise.reject({
status: response?.status,
message: response?.data?.message || ‘Unknown error’,
data: response?.data
});
}
);
// 헬퍼 함수
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async function refreshToken(): Promise
try {
const refreshToken = localStorage.getItem(‘refreshToken’);
const response = await axios.post(
`${process.env.REACT_APP_API_URL}/auth/refresh`,
{ refreshToken }
);
const { accessToken, newRefreshToken } = response.data;
localStorage.setItem(‘accessToken’, accessToken);
localStorage.setItem(‘refreshToken’, newRefreshToken);
return true;
} catch (error) {
console.error(‘Token refresh failed:’, error);
return false;
}
}
“`
—
## 5. API 엔드포인트 정의
### 5.1 사용자 API
“`typescript
// api/endpoints/user.ts
import client from ‘../client’;
interface User {
id: number;
name: string;
email: string;
role: string;
}
interface CreateUserData {
name: string;
email: string;
password: string;
}
interface UpdateUserData {
name?: string;
email?: string;
}
// 사용자 목록 조회
export const getUsers = async (params?: {
page?: number;
limit?: number;
role?: string;
}): Promise
return client.get(‘/users’, { params });
};
// 사용자 상세 조회
export const getUser = async (userId: number): Promise
return client.get(`/users/${userId}`);
};
// 사용자 생성
export const createUser = async (data: CreateUserData): Promise
return client.post(‘/users’, data);
};
// 사용자 업데이트
export const updateUser = async (
userId: number,
data: UpdateUserData
): Promise
return client.put(`/users/${userId}`, data);
};
// 사용자 부분 업데이트
export const patchUser = async (
userId: number,
data: Partial
): Promise
return client.patch(`/users/${userId}`, data);
};
// 사용자 삭제
export const deleteUser = async (userId: number): Promise
return client.delete(`/users/${userId}`);
};
// 사용자 검색
export const searchUsers = async (query: string): Promise
return client.get(‘/users/search’, { params: { q: query } });
};
“`
### 5.2 게시물 API
“`typescript
// api/endpoints/post.ts
import client from ‘../client’;
interface Post {
id: number;
title: string;
content: string;
userId: number;
createdAt: string;
updatedAt: string;
}
interface CreatePostData {
title: string;
content: string;
}
// 게시물 목록
export const getPosts = async (params?: {
page?: number;
limit?: number;
userId?: number;
}) => {
return client.get
};
// 게시물 상세
export const getPost = async (postId: number) => {
return client.get
};
// 게시물 생성
export const createPost = async (data: CreatePostData) => {
return client.post
};
// 게시물 수정
export const updatePost = async (postId: number, data: Partial
return client.put
};
// 게시물 삭제
export const deletePost = async (postId: number) => {
return client.delete(`/posts/${postId}`);
};
// 게시물 좋아요
export const likePost = async (postId: number) => {
return client.post(`/posts/${postId}/like`);
};
// 게시물 조회수 증가
export const increasePostView = async (postId: number) => {
return client.post(`/posts/${postId}/view`);
};
“`
### 5.3 인증 API
“`typescript
// api/endpoints/auth.ts
import client from ‘../client’;
interface LoginData {
email: string;
password: string;
}
interface AuthResponse {
accessToken: string;
refreshToken: string;
user: {
id: number;
email: string;
name: string;
};
}
interface RegisterData {
email: string;
password: string;
name: string;
}
// 로그인
export const login = async (data: LoginData): Promise
return client.post(‘/auth/login’, data);
};
// 회원가입
export const register = async (data: RegisterData): Promise
return client.post(‘/auth/register’, data);
};
// 로그아웃
export const logout = async (): Promise
return client.post(‘/auth/logout’);
};
// 토큰 갱신
export const refreshAccessToken = async (
refreshToken: string
): Promise<{ accessToken: string }> => {
return client.post(‘/auth/refresh’, { refreshToken });
};
// 이메일 인증
export const verifyEmail = async (email: string): Promise
return client.post(‘/auth/verify-email’, { email });
};
“`
### 5.4 통합 내보내기
“`typescript
// api/endpoints/index.ts
export * as userAPI from ‘./user’;
export * as postAPI from ‘./post’;
export * as authAPI from ‘./auth’;
“`
—
## 6. 컴포넌트에서 사용
### 6.1 useQuery와 함께 사용
“`typescript
// components/UserList.tsx
import { useQuery } from ‘@tanstack/react-query’;
import { userAPI } from ‘../api/endpoints’;
function UserList() {
const { data: users, isLoading, error, refetch } = useQuery({
queryKey: [‘users’],
queryFn: () => userAPI.getUsers({ page: 1, limit: 10 }),
staleTime: 1000 * 60 * 5 // 5분
});
if (isLoading) return
;
if (error) return
;
return (
-
{users?.map(user => (
- {user.name}
))}
);
}
export default UserList;
“`
### 6.2 useMutation과 함께 사용
“`typescript
// components/CreateUserForm.tsx
import { useMutation, useQueryClient } from ‘@tanstack/react-query’;
import { useState } from ‘react’;
import { userAPI } from ‘../api/endpoints’;
function CreateUserForm() {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({ name: ”, email: ”, password: ” });
const mutation = useMutation({
mutationFn: (data) => userAPI.createUser(data),
onSuccess: () => {
// 성공 시
queryClient.invalidateQueries({ queryKey: [‘users’] });
setFormData({ name: ”, email: ”, password: ” });
alert(‘사용자가 생성되었습니다’);
},
onError: (error: any) => {
// 실패 시
alert(`오류: ${error.message}`);
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate(formData);
};
return (
);
}
export default CreateUserForm;
“`
—
## 7. 에러 처리
### 7.1 커스텀 에러 클래스
“`typescript
// types/api.ts
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public data?: any
) {
super(message);
this.name = ‘ApiError’;
}
}
export interface ErrorResponse {
status: number;
message: string;
errors?: Record
}
“`
### 7.2 에러 처리 훅
“`typescript
// hooks/useApiError.ts
import { useCallback } from ‘react’;
export function useApiError() {
const handleError = useCallback((error: any) => {
// API 에러
if (error.status) {
switch (error.status) {
case 400:
return ‘잘못된 요청입니다’;
case 401:
return ‘로그인이 필요합니다’;
case 403:
return ‘접근 권한이 없습니다’;
case 404:
return ‘리소스를 찾을 수 없습니다’;
case 409:
return ‘중복된 데이터입니다’;
case 422:
return ‘유효하지 않은 데이터입니다’;
case 500:
return ‘서버 오류입니다’;
default:
return error.message || ‘요청 처리 중 오류 발생’;
}
}
// 네트워크 오류
if (error.message === ‘Network Error’) {
return ‘네트워크 연결을 확인하세요’;
}
// 기타 오류
return ‘알 수 없는 오류가 발생했습니다’;
}, []);
return { handleError };
}
“`
—
## 8. 파일 업로드/다운로드
### 8.1 파일 업로드
“`typescript
// api/endpoints/file.ts
import client from ‘../client’;
// 단일 파일 업로드
export const uploadFile = async (file: File, userId: number) => {
const formData = new FormData();
formData.append(‘file’, file);
formData.append(‘userId’, userId.toString());
return client.post(‘/files/upload’, formData, {
headers: {
‘Content-Type’: ‘multipart/form-data’
}
});
};
// 다중 파일 업로드
export const uploadMultipleFiles = async (files: File[], userId: number) => {
const formData = new FormData();
files.forEach(file => {
formData.append(‘files’, file);
});
formData.append(‘userId’, userId.toString());
return client.post(‘/files/upload-multiple’, formData, {
headers: {
‘Content-Type’: ‘multipart/form-data’
},
onUploadProgress: (progressEvent) => {
// 업로드 진행률
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
console.log(`Upload: ${percentCompleted}%`);
}
});
};
“`
### 8.2 파일 다운로드
“`typescript
// api/endpoints/file.ts
// 파일 다운로드
export const downloadFile = async (fileId: number, fileName: string) => {
return client.get(`/files/${fileId}/download`, {
responseType: ‘blob’
}).then(blob => {
// 파일 다운로드
const url = window.URL.createObjectURL(blob);
const link = document.createElement(‘a’);
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
});
};
// 진행률 포함 다운로드
export const downloadFileWithProgress = async (
fileId: number,
fileName: string,
onProgress?: (percent: number) => void
) => {
return client.get(`/files/${fileId}/download`, {
responseType: ‘blob’,
onDownloadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
onProgress?.(percentCompleted);
}
}).then(blob => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement(‘a’);
link.href = url;
link.download = fileName;
link.click();
window.URL.revokeObjectURL(url);
});
};
“`
—
## 9. 재시도 로직
### 9.1 기본 재시도
“`typescript
// api/client.ts
import axios from ‘axios’;
import axiosRetry from ‘axios-retry’;
const client = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000
});
// 재시도 설정
axiosRetry(client, {
retries: 3, // 최대 3번 재시도
retryDelay: axiosRetry.exponentialDelay, // 지수 백오프
retryCondition: (error) => {
// 재시도 조건
return (
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429 // Too Many Requests
);
}
});
export default client;
“`
### 9.2 커스텀 재시도 로직
“`typescript
// api/retry.ts
import axios, { AxiosRequestConfig } from ‘axios’;
export async function requestWithRetry(
config: AxiosRequestConfig,
maxRetries: number = 3,
delayMs: number = 1000
): Promise
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await axios(config);
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// 지수 백오프
const delay = delayMs * Math.pow(2, attempt);
console.log(`Retry attempt ${attempt + 1}/${maxRetries}, waiting ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
“`
—
## 10. 캐싱
### 10.1 자체 캐싱 구현
“`typescript
// api/cache.ts
class RequestCache {
private cache = new Map
private ttl = 5 * 60 * 1000; // 5분
set(key: string, data: any) {
this.cache.set(key, { data, timestamp: Date.now() });
}
get(key: string) {
const item = this.cache.get(key);
if (!item) return null;
// TTL 확인
if (Date.now() – item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear() {
this.cache.clear();
}
delete(key: string) {
this.cache.delete(key);
}
}
export const requestCache = new RequestCache();
“`
—
## 11. 인증 서비스
### 11.1 인증 서비스
“`typescript
// services/auth.service.ts
import { authAPI } from ‘../api/endpoints’;
class AuthService {
private accessToken: string | null = null;
private refreshToken: string | null = null;
constructor() {
this.loadTokens();
}
async login(email: string, password: string) {
try {
const response = await authAPI.login({ email, password });
this.setTokens(response.accessToken, response.refreshToken);
return response;
} catch (error) {
console.error(‘Login failed:’, error);
throw error;
}
}
async register(email: string, password: string, name: string) {
try {
const response = await authAPI.register({ email, password, name });
this.setTokens(response.accessToken, response.refreshToken);
return response;
} catch (error) {
console.error(‘Register failed:’, error);
throw error;
}
}
async logout() {
try {
await authAPI.logout();
this.clearTokens();
} catch (error) {
console.error(‘Logout failed:’, error);
this.clearTokens();
}
}
async refreshAccessToken(): Promise
if (!this.refreshToken) {
throw new Error(‘No refresh token available’);
}
try {
const response = await authAPI.refreshAccessToken(this.refreshToken);
this.accessToken = response.accessToken;
this.saveTokens();
return response.accessToken;
} catch (error) {
this.clearTokens();
throw error;
}
}
private setTokens(accessToken: string, refreshToken: string) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.saveTokens();
}
private saveTokens() {
if (this.accessToken) {
localStorage.setItem(‘accessToken’, this.accessToken);
}
if (this.refreshToken) {
localStorage.setItem(‘refreshToken’, this.refreshToken);
}
}
private loadTokens() {
this.accessToken = localStorage.getItem(‘accessToken’);
this.refreshToken = localStorage.getItem(‘refreshToken’);
}
private clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem(‘accessToken’);
localStorage.removeItem(‘refreshToken’);
}
getAccessToken(): string | null {
return this.accessToken;
}
isAuthenticated(): boolean {
return !!this.accessToken;
}
}
export const authService = new AuthService();
“`
—
## 12. 실전 예제: 완전한 API 통합
“`typescript
// hooks/useUserAPI.ts
import { useQuery, useMutation, useQueryClient } from ‘@tanstack/react-query’;
import { userAPI } from ‘../api/endpoints’;
import { useApiError } from ‘./useApiError’;
export function useUsers(page: number = 1, limit: number = 10) {
return useQuery({
queryKey: [‘users’, page, limit],
queryFn: () => userAPI.getUsers({ page, limit }),
staleTime: 1000 * 60 * 5
});
}
export function useUser(userId: number | null) {
return useQuery({
queryKey: [‘users’, userId],
queryFn: () => userAPI.getUser(userId!),
enabled: !!userId
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
const { handleError } = useApiError();
return useMutation({
mutationFn: userAPI.createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [‘users’] });
},
onError: (error) => {
console.error(‘Create user error:’, handleError(error));
}
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, data }: { userId: number; data: any }) =>
userAPI.updateUser(userId, data),
onSuccess: (_, { userId }) => {
queryClient.invalidateQueries({ queryKey: [‘users’] });
queryClient.invalidateQueries({ queryKey: [‘users’, userId] });
}
});
}
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userAPI.deleteUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [‘users’] });
}
});
}
“`
—
## 13. 모니터링 및 로깅
### 13.1 요청/응답 로깅
“`typescript
// api/logger.ts
import client from ‘./client’;
export function setupLogging() {
client.interceptors.request.use((config) => {
console.log(‘📤 Request:’, {
method: config.method?.toUpperCase(),
url: config.url,
headers: config.headers,
data: config.data
});
return config;
});
client.interceptors.response.use(
(response) => {
console.log(‘📥 Response:’, {
status: response.status,
url: response.config.url,
data: response.data
});
return response;
},
(error) => {
console.error(‘❌ Error:’, {
status: error.response?.status,
url: error.config?.url,
message: error.message,
data: error.response?.data
});
return Promise.reject(error);
}
);
}
“`
—
## 14. 체크리스트
### 설치 및 설정
– [ ] npm install axios
– [ ] API 클라이언트 인스턴스 생성
– [ ] 기본 헤더 설정
– [ ] 베이스 URL 설정
### 인터셉터
– [ ] 요청 인터셉터 (토큰 추가, 로깅)
– [ ] 응답 인터셉터 (에러 처리, 토큰 갱신)
– [ ] 401 Unauthorized 처리
– [ ] CORS 설정
### API 엔드포인트
– [ ] 사용자 API
– [ ] 게시물 API
– [ ] 인증 API
– [ ] 파일 업로드/다운로드 API
### React Query 통합
– [ ] useQuery 설정
– [ ] useMutation 설정
– [ ] 캐시 무효화
– [ ] 에러 처리
### 추가 기능
– [ ] 재시도 로직
– [ ] 파일 업로드 진행률
– [ ] 로깅 및 모니터링
– [ ] 인증 서비스
—
## 15. 자주하는 실수
| 실수 | 원인 | 해결방법 |
|——|——|———|
| CORS 오류 | 서버 설정 부족 | 서버에서 CORS 헤더 추가 |
| 토큰 만료 | 갱신 로직 없음 | 인터셉터에서 토큰 갱신 |
| 중복 요청 | useEffect 의존성 | 의존성 배열 명시 |
| 캐시 문제 | 부실한 데이터 | invalidateQueries 사용 |
| 메모리 누수 | 구독 해제 안 함 | cleanup 함수 작성 |
—
## 16. 성능 최적화
“`typescript
// 1. 요청 취소
const controller = new AbortController();
client.get(‘/data’, { signal: controller.signal });
controller.abort(); // 요청 취소
// 2. 동시 요청 제한
import pLimit from ‘p-limit’;
const limit = pLimit(5); // 최대 5개 동시 요청
// 3. 요청 배치 처리
const requests = [getUser(1), getUser(2), getUser(3)];
Promise.all(requests);
“`
—
## 17. 환경 변수 설정
“`bash
# .env.local
REACT_APP_API_URL=http://localhost:3000/api
REACT_APP_API_TIMEOUT=10000
# .env.production
REACT_APP_API_URL=https://api.example.com
REACT_APP_API_TIMEOUT=15000
“`
—
**최종 권장 사항:**
✅ React Query + Axios 조합 사용
✅ 인터셉터로 인증 처리
✅ 엔드포인트별로 함수 분리
✅ TypeScript로 타입 안전성 확보
✅ 에러 처리 명확히 하기