본문으로 건너뛰기
Previous
Next
Axios 완벽 가이드 | HTTP 클라이언트·Interceptor·에러 처리·실전 활용

Axios 완벽 가이드 | HTTP 클라이언트·Interceptor·에러 처리·실전 활용

Axios 완벽 가이드 | HTTP 클라이언트·Interceptor·에러 처리·실전 활용

이 글의 핵심

Axios 완벽 가이드에 대해 정리한 개발 블로그 글입니다. Axios로 효율적인 HTTP 통신을 구현하는 완벽 가이드입니다. Instance, Interceptor, 에러 처리, Timeout, Retry까지 실전 예제로 정리했으며, *Adapter·설정 병합·인터셉터 체인… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: Axios,…

fetch도 충분한데 굳이 Axios 얹는 이유를 나한테 물으면, “표준 API + 짧은 유틸”로 팀이 합의돼 있으면 fetch로 가도 된다고 말하는 편이에요. JSON 파싱 한 줄, AbortController로 취소, 필요하면 ky 수준의 얇은 래퍼. 번들·의존성·온콜에서 싸울 이유가 없거든요. 그런 식으로 가다가 “토큰 붙이기 / 401이면 갱신하고 재시도 / 에러 모양 통일 / 로그 한 스타일”이 한꺼번에 밀려오면, 그때는 Axios가 인터셉터 하나로 그 지옥을 모아놓을 수 있어서 고민해볼 만해요. 반대로 “그냥 GET/POST 몇 개” 수준이면 fetch 질러도 돼요. 나는 둘 다 혐오하는 게 아니라, 뭘로 통일하느냐만 중요하다고 봅니다.

그리고 “완벽 가이드”라는 제목은 템플릿 잔재라서, 읽는 사람 입장에선 섹션 1, 2, 3보다 썰 하나가 더 도움 될 때가 많아요. 특히 인터셉터는 문서에선 예쁜데, 현장에선 순서·재진입 때문에 머리 썩는 경우가 많거든요.

인터셉터 디버깅으로 밤을 샜던 썰(요약). 예전에 401 뜨면 response 인터셉터에서 refresh 후 api.request(originalConfig)로 한 번 더 쏘는 패턴 썼는데, 갱신 요청 POST /refresh같은 axios 인스턴스를 타다 보니, 실수로 그 요청에도 “401이면 refresh”가 걸리거나(대부분은 아니겠지만 설정 꼬이면), 반대로 request 인터셉터가 토큰을 두 번 붙이는 케이스를 본 적 있어요. 가장 흔한 건 originalConfig._retry 플래그를 안 두고 401 루프가 도는 거고, 그다음이 “response 인터셉터를 둘 등록해놓고, 앞에 있는 쪽이 return을 이상하게 해서 뒤에 있는 토스트/로깅이 영원히 안 온다”는 것. 그때 한 건, 개발에선 인터셉터 안에 console.log 말고 요청마다 config.__debugId 하나 박고, 지연 추적하듯 “요청 7번 → 401 → 갱신 → 재요청 7번”이 한 줄에 찍히게 만드는 거였어요. 그리고 interceptors.request.eject(id)하나씩 끄면 범인이 바로 드러나요. “왜 503만 재시도하려다 전체 API가 느려졌지?”도 같은 박스에서 나와요. 체이닝은 문서에 “응답은 역순”이라고 돼 있지만, 손으로 설계하다 보면 누가 먼저 먹는지 머릿속에서 안 그려질 때가 있으니, 팀이면 “인증은 request 첫 칸, 에러 맵핑은 response 마지막” 같은 규칙을 박아두는 게 정신 건강에 좋아요.

뭐, 그렇다고 내부가 어댑터고 dispatchRequest고를 매번 외울 필요는 없어요. 대신 “timeout이 어댑터 쪽이랑 합쳐지고, transformRequest는 직렬화 직전” 정도의 감만 있으면, Content-Type이 꼬였다거나 FormData에 JSON transform이 먹었다 같은 사고를 빨리 의심할 수 있어요.

설치는 npm i axios고, axios.createbaseURL·timeout 박아둔 인스턴스 하나 두는 건 이제 권장 패턴이에요. 토큰은 request 쪽, 401/403 대응은 response 쪽—이렇게만 나눠도 파일이 읽힙니다.

import axios from 'axios';

export const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
});

api.interceptors.request.use((config) => {
  const t = localStorage.getItem('token');
  if (t) {
    config.headers = config.headers ?? {};
    (config.headers as Record<string, string>).Authorization = `Bearer ${t}`;
  }
  return config;
});

에러는 catch에서 axios.isAxiosError로 한 번 좁혀보면, error.response가 있는지(서버가 말한 경우), error.request만 있는지(망/타임아웃 쪽) 감이 잡혀요. 취소는 요즘은 signal: AbortController로 맞추는 게 fetch 생태랑도 같이 가요. CancelToken은 레거시로만 알아두면 됩니다.

Promise.all로 여 API 동시에 긁는 건 fetch든 Axios든 똑같고, “동시에 보내다 하나만 실패” 같은 건 어디서 합쳐서 에러 보여줄지 UI 문제라 HTTP 클라이언트가 해결해주진 않아요. 그건 TanStack Query 쪽 썰에 가깝고.

마지막으로 또 한 번 말하면, fetch도 충분한데 Axios를 고르는 지점은 “팀이 인터셉터로 횡단 관심사를 합의했을 때”에 가깝다고 봅니다. 나는 둘 다 싫어하는 사람한테 rpc를 권하진 않을게요(농담). 이 길 가다 꼬이면 인터셉터부터 의심하고, 그다음 baseURL이랑 SSRF(서버에서 유저 입력으로 URL 이어붙이는 코드) 쪽—그 순서로 보면 잠은 조금 덜 깰 거예요.

import axios from 'axios';
// 401 → refresh → 한 번만 재시도(무한 루프 방지 스케치) — api는 위에서 create한 인스턴스
api.interceptors.response.use(
  (r) => r,
  async (err) => {
    const cfg = err.config as typeof err.config & { _didRefresh?: boolean };
    if (!axios.isAxiosError(err) || err.response?.status !== 401 || cfg._didRefresh) {
      return Promise.reject(err);
    }
    cfg._didRefresh = true;
    const { data } = await axios.post('/auth/refresh', { refresh: localStorage.getItem('r') });
    localStorage.setItem('token', data.accessToken);
    return api.request(cfg);
  }
);

Axios, HTTP, API, JavaScript, TypeScript 같은 키워드로 이 길 찾아왔다면, 위에 적은 “인터셉터 의심 순서”만 챙겨가도 본전은 뽑은 거라고 봅니다. 난 문서보다 썰 쪽이 기억에 남는 편이거든요.