Axios란?axios는 웹 개발에서 가장 널리 사용되는 HTTP 클라이언트 라이브러리 중 하나이다.
주로 브라우저와 서버 간 통신(REST API 호출)을 위해 사용된다.
fetch와 비슷한 역할을 하지만, 더 편리한 기능과 인터셉터 등 고급 기능을 제공한다.
이번 프로젝트에서 axios를 사용하여 REST API 호출을 하게 되어, axios에 대해서 정리해보기로 했다.
Axios vs. Fetch| 항목 | Axios | Fetch |
|---|---|---|
| 응답 자동 JSON 파싱 | ✅ | ❌ (수동 파싱 필요) |
| 요청/응답 인터셉터 | ✅ | ❌ |
| 에러 처리 방식 | 상태 코드에 따라 .catch() | 400~500 에러도.then() |
| 요청 취소 | ✅ (CancelToken) | ✅ (AbortController) |
| 브라우저 지원 | IE(Internet Explorer) 포함 | IE 미지원 |
🔍
fetch는
- HTTP 응답 코드가 실패(예: 404, 500)여도 성공 응답으로 간주하고
.then()으로 넘어감- 브라우저가 아예 요청을 보내지 못했거나, 응답을 전혀 받지 못했을 때(네트워크 레벨에서 문제가 생겼을 때만)
.catch()로 넘어감
🔍
axios는
- 400~599 응답일 경우 자동으로
.catch()로 감
Axios 커스텀 인스턴스는 중복되는 설정을 한 번에 묶어 재사용할 수 있도록 도와주는 기능이다.
Axios 인스턴스는 기본 설정(baseURL, 헤더, 타임아웃 등)을 저장해 놓은 Axios 객체이다.
이 인스턴스를 이용하면 모든 요청에서 공통 설정을 재사용할 수 있어 코드가 간결하고 일관성 있게 유지된다.
| 일반 방식 | 커스텀 인스턴스 |
|---|---|
| 매번 설정해야 함 | 한 번 설정 후 재사용 |
| 유지보수 어려움 | 변경이 용이함 |
| 코드 중복 ⬆️ | 코드 재사용 ⬆️ |
// 일반 방식
axios.get("/user", { headers: { Authorization: `Bearer ${token}` } });
axios.post("/post", postData, { headers: { Authorization: `Bearer ${token}` } }); ➡️ 너무 반복됨😩 → 커스텀 인스턴스로 해결😄인스턴스 생성
axios.create()를 통해 커스텀 Axios 인스턴스를 만들고, 기본 설정을 지정한다.
// api.ts
import axios from "axios";
const api = axios.create({
baseURL: "https://example.com/api",
timeout: 5000,
headers: {
"Content-Type": "application/json",
},
});
export default api;
🔍 baseURL: "https://example.com/api"
- 모든 요청 URL의 기본 경로를 설정함
- 예를 들어
api.get("/user")를 호출하면 → 실제 요청 URL은https://example.com/api/user가 됨- 중복된 도메인 작성 방지, 유지보수 편리
🔍 timeout: 5000
- 요청 제한 시간(ms 단위)을 설정함
- 여기서는 5초(5000ms) 동안 응답이 없으면 요청이 자동으로 실패함
- 네트워크 지연이나 서버 문제에 대한 안전장치 역할을 함
🔍 headers: { "Content-Type": "application/json" }
- 모든 요청에 기본으로 포함될 HTTP 헤더를 설정함
- 여기선
Content-Type을application/json으로 설정 → 서버에 JSON 형식의 데이터를 보낼 것임을 명시함- 주로
POST,PUT,PATCH요청 시에 사용됨
인스턴스 사용
import api from "./api";
// GET
const res = await api.get("/user");
// POST
const res = await api.post("/post", { title: "Hello" });
🔍
const res = await api.get("/user");
- 의미
GET방식으로/user경로에 비동기 HTTP 요청을 보냄baseURL이https://example.com/api라면 → 실제 요청 URL은https://example.com/api/userawait키워드를 사용하여 응답이 올 때까지 기다림- 반환
res는 Axios의 응답 객체(Response)를 담고 있음- 구조
{ data: ..., // 응답 데이터 (실제 우리가 원하는 것) status: 200, // HTTP 상태 코드 headers: ..., // 응답 헤더 config: ..., // 요청 시 설정 정보 request: ... // 요청 자체에 대한 정보 }- 응답 데이터(
data)만 받고 싶은 경우const { data } = await api.get("/user");
🔍
const res = await api.post("/post", { title: "Hello" });
- 의미
POST방식으로/post경로에 비동기 HTTP 요청을 보냄- 두 번째 인자인
{ title: "Hello" }는 요청 바디(body)로 전달됨- 주로 서버에 새로운 데이터를 생성할 때 사용하는 방식임
- 실제 요청 내용 (요약)
➡️POST https://example.com/api/post Content-Type: application/json { "title": "Hello" }api인스턴스에 기본적으로Content-Type: application/json이 설정돼 있어서 JSON으로 자동 전송됨
인터셉터(Interceptor)는 요청 또는 응답이 실제로 네트워크를 오가기 전에 중간에 가로채서 조작할 수 있는 후킹(Hooking) 메커니즘이다.
요청 보내기 전에 → 헤더 추가, 로딩 스피너 ON
응답 받자마자 → 에러 메시지 처리, 토큰 갱신 등
request interceptor (요청 인터셉터)
axios가 서버로 요청을 보내기 직전에 실행됨
response interceptor (응답 인터셉터)
서버로부터 응답을 받은 직후, .then()이나 .catch()로 넘어가기 직전에 실행됨
// 요청 인터셉터
axios.interceptors.request.use(
(config) => {
// 요청을 가로채서 가공
return config;
},
(error) => {
// 요청 에러 처리
return Promise.reject(error);
}
);
// 응답 인터셉터
axios.interceptors.response.use(
(response) => {
// 응답 가공 (예: response.data만 추출)
return response;
},
(error) => {
// 응답 에러 처리
return Promise.reject(error);
}
);
요청 인터셉터
| 줄 | 설명 |
|---|---|
axios.interceptors.request.use(...) | Axios의 요청 인터셉터 등록 |
(config) => { ... } | 실제 요청이 서버에 전송되기 직전에 실행됨 |
config | Axios의 요청 설정 객체 (URL, headers, method, data 등 포함) |
return config; | 이 config를 그대로 혹은 수정하여 반환해야 요청이 정상 전송됨 |
(error) => { ... } | 요청 설정 중 오류 발생 시 실행되는 에러 핸들러 |
Promise.reject(error); | 오류를 외부로 전달 (catch 문에서 잡을 수 있도록 함) |
응답 인터셉터
| 줄 | 설명 |
|---|---|
axios.interceptors.response.use(...) | Axios의 응답 인터셉터 등록 |
(response) => { ... } | 서버로부터 응답을 받은 직후 실행 |
response | 응답 전체 객체 (status, data, headers 등 포함) |
return response; | 그대로 응답을 넘기거나 가공해서 return |
(error) => { ... } | 서버에서 에러 응답(404, 500 등)이 왔을 때 실행되는 핸들러 |
Promise.reject(error); | 외부에서 .catch()로 받을 수 있게 에러 던짐 |
요청 인터셉터 (Request Interceptor)
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem("accessToken");
// 토큰이 존재한다면, Axios 요청 헤더 중 `Authorization` 필드에 토큰을 삽입함
// `Bearer`는 인증 방식 중 하나로, "토큰 기반 인증"이라는 의미임
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
Authorization 헤더에 accessToken 추가응답 인터셉터 (Response Interceptor)
api.interceptors.response.use(
(response) => {
return response.data; // 응답 데이터만 추출해서 넘김
},
(error) => {
if (error.response?.status === 401) {
alert("로그인이 필요합니다.");
}
return Promise.reject(error);
}
);
data만 꺼냄요청 인터셉터
등록한 순서대로 실행 → use()를 먼저 등록한 것이 먼저 실행됨 (FIFO)
응답 인터셉터
등록한 순서의 역순으로 실행 → use()를 나중에 등록한 것이 먼저 실행됨 (LIFO)
Axios의 interceptors.request.use() 또는 interceptors.response.use()는 등록된 인터셉터에 고유한 ID를 반환한다.
const interceptorId = api.interceptors.request.use((config) => {
return config;
});
interceptorId는 숫자 (예: 0, 1, 2, ...)eject)문법
axios.interceptors.request.eject(interceptorId);
axios.interceptors.response.eject(interceptorId);
예시
const id = api.interceptors.response.use(
(res) => res,
(err) => Promise.reject(err)
);
// 나중에 인터셉터 제거
api.intercepters.response.eject(id);
사용 상황
로그인 페이지에서는 인터셉터를 쓰지 않도록 하고 싶을 때
특정 조건에서만 일시적으로 적용하고 싶을 때
메모리 누수 방지를 위해 SPA 페이지 전환 시 인터셉터 해제
🚨 SPA에서 발생하는 문제 시나리오: 페이지 전환 시 컴포넌트가 재마운트됨
- SPA에서는 페이지 이동이 전체 새로고침이 아니라 컴포넌트 언마운트 → 다른 컴포넌트 마운트 흐름이다.
- 예를 들어 로그인 페이지 → 마이페이지 → 게시판 등으로 전환할 떄, 아래처럼 인터셉터를 컴포넌트 안에서 등록하게 되면 문제가 발생한다.
// 잘못된 방식 (컴포넌트 안에서 인터셉터 등록) useEffect(() => { const id = api.interceptors.response.use( (res) => res, (err) => { if (err.response?.status === 401) alert("로그인 해주세요."); return Promise.reject(err); } ); }, []);useEffect가 실행될 때마다 인터셉터가 계속 새로 등록됨- 그런데
eject(id)하지 않으면 이전 인터셉터는 계속 살아있음- 그 결과
😱 응답 하나에 대해 여러 인터셉터가 동시에 실행됨
😱 중복 알림, 중복 재요청, 로직 꼬임 발생
😱 시간이 지나면서 인터셉터가 메모리에 계속 쌓여서 메모리 누수 발생
💡 해결 방법: 인터셉터 등록 → ID 저장 → 해제(
eject)useEffect(() => { const id = api.interceptors.response.use( (res) => res, (err) => { if (err.response?.status === 401) alert("로그인 해주세요."); return Promise.reject(err); } ); return () => { // 컴포넌트 언마운트 시 인터셉터 제거 api.interceptors.response.eject(id); }; }, []);
api.interceptors.response.eject(id);는 어디서든 실행해도 되지만, 해당 인터셉터가 등록된 인스턴스(api)에 대해서 실행해야 하고, 인터셉터 ID(id)를 기억하고 있어야만 제거가 가능하다.
전제: 인터셉터 등록 위치
// api.ts
export const interceptorId = api.interceptors.response.use(
(response) => response,
(error) => Promise.reject(error)
);
interceptorId는 api.ts 안에서 생성된 고유한 숫자이다.eject.(id)로 제거할 수 있다.인터셉터 제거 위치 (사용 함수 내부에서 해도 됨)
예를 들어 다음처럼 다른 API 함수 정의 파일에서 사용할 수 있다.
// somewhere.ts
import api, { interceptorId } from "./api"; // 인터셉터 ID도 함께 가져와야 함
export const fetchData = async () => {
// 인터셉터 제거
api.interceptors.response.eject(interceptorId);
const res = await api.get("/data");
return res;
}
api 인스턴스를 통해 등록된 인터셉터 ID(interceptorId)를 공유할 수 있어야 한다.api.ts 내에서만 지역 변수로 선언돼 있다면 외부에서 접근할 수 없으므로 제거도 불가능하다.| 팁 | 설명 |
|---|---|
interceptors는 전역 설정이므로 모든 요청에 적용됨 | 커스텀 인스턴스마다 따로 설정 가능 (axios.create()) |
request.use에서 config는 반드시 return 해야 함 | 안 하면 요청이 전송되지 않음 |
응답 인터셉터에서 response.data만 return하면 이후 .data.data 같은 중첩 문제를 방지할 수 있음 | |
인터셉터 안에서 async/await 사용 시 반드시 return 필요 | 안 그러면 undefined 반환됨 |
응답 재시도 시 무한 루프 방지 위해 _retry 플래그 사용 | originalConfig._retry = true 체크 |
실제 프로젝트에서 구현한 api.ts 코드이다.
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
import { useAuthStore } from "../stores/auth.store";
const BASE_URL = "https://api.tranner.com/api";
export const api = axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "application/json",
},
});
// 요청 인터셉터: 토큰 자동 부착
// 요청 인터셉터는 axios가 서버로 요청을 보내기 직전에 실행됨
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = useAuthStore.getState().accessToken;
if (token) config.headers.Authorization = token;
return config;
},
(error) => Promise.reject(error)
);
// 응답 인터셉터: 토큰 만료 → 자동 재발급
// 응답 인터셉터는 서버로부터 응답을 받은 직후, .then()이나 .catch()로 넘어가기 직전에 실행됨
api.interceptors.response.use(
(response) => response, // 응답이 성공한 경우는 그대로 응답 반환
// 응답이 실패한 경우(에러일 때) 처리
async (error: AxiosError) => {
// 실패한 원래 요청을 가져오고, _retry라는 커스텀 플래그 추가 (재시도 여부 확인용)
// error.config : Axios가 자동으로 넣어주는 실패한 요청의 설정 객체
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// 401 Unauthorized + 아직 재시도 안 한 요청일 때만 수행
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 중복 재시도를 막기 위해 플래그 설정
try {
// 1. refresh token 요청: httpOnly 쿠키로 보내기 때문에 withCredentials: true 설정
// POST 구조: axios.post(url, data, config)
// 인터셉터가 없는 순수한 axios 인스턴스 (무한루프 방지)
const res = await axios.post(
`{BASE_URL}/refresh-token`,
{}, // 요청 바디는 비움
{ withCredentials: true } // 쿠키 자동 첨부
);
// 2. 서버가 새 accessToken 반환 → Zustand에 저장
const newAccessToken = res.data.accessToken;
useAuthStore.getState().login(newAccessToken);
// 3. 원래 실패한 요청의 Authorization 헤더를 새 토큰으로 덮어쓰기
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
// 4. 원래 요청 재시도 → 새 토큰으로 요청 재전송
// api(...)는 내부적으로 axios.request(...)와 같음
// 즉, config를 통째로 넘기면 axios가 그것에 맞게 다시 요청을 보냄
return api(originalRequest);
} catch(refreshError) {
// 5. 리프레시 토큰도 만료 or 서버 오류 → 강제 로그아웃
alert("세션이 만료되었습니다. 다시 로그인해주세요.");
useAuthStore.getState().logout();
// 에러를 그대로 밖으로 던짐 → 이후 catch에서 처리 가능
return Promise.reject(refreshError);
}
}
// 위 조건에 해당하지 않으면 일반 에러로 그대로 처리
return Promise.reject(error);
}
);
❓ 액세스 토큰을 바로 헤더에 못 넣는 이유
- Zustand store는 초기화 시점 이후에 데이터가 들어옴
accessToken은 로그인 성공 후에 저장됨- 하지만
axios.create(...)는 앱 시작 시점에 단 한 번만 실행됨- 즉, 그 시점엔 아직 토큰이 없음 (null) →
Authorization: Bearer null이런 값이 들어가게 됨
- 토큰은 계속 바뀔 수 있는데, 이 방식은 고정 값이 됨
axios.create(...)에 넣은headers는 초기 설정일 뿐이고,- 로그인/로그아웃 이후의 변경 사항을 반영할 수 없음
- 따라서 동적으로
zustand에서 매번 가져와야 함 → interceptor 사용