언어의역사

Ajax fetch axios 차이, 10년 넘게 쓰며 알게 된 것들

Ajax fetch axios 차이, 10년 넘게 쓰며 알게 된 것들

 

Ajax, fetch, axios 차이를 검색하면 문법 비교표는 많이 나옵니다. 그런데 이 셋이 어떤 흐름으로 이어져 왔는지, 실무에서 어느 지점에서 문제가 터지는지를 다룬 글은 의외로 적습니다. 저는 XMLHttpRequest와 jQuery Ajax 시절부터 이 흐름을 현장에서 겪었고, fetch의 함정에 실제로 당해 본 적도 있습니다. 이 글에서는 그 경험을 바탕으로 세 도구의 관계와 차이를 코드와 함께 짚어보겠습니다.

"fetch가 있는데 axios를 왜 설치하나요?"
"404인데 왜 catch로 안 떨어지죠?"
"회사 코드는 axios인데 강의는 다 fetch로 가르치던데요."
"둘 중 뭘 배워야 하나요?"

주니어 개발자들에게 실제로 자주 받는 질문들입니다. 저도 처음 fetch를 만졌을 때 404 응답이 catch 블록으로 떨어지지 않아서 한참 헤맸습니다. jQuery의 $.ajax에 길들여진 습관 때문이었는데, 알고 보니 그건 습관 문제가 아니라 설계 철학의 차이였습니다.

이 글은 fetch와 axios를 처음 비교하는 분, 그리고 "왜 이렇게 만들어졌는지"까지 알고 싶은 분에게 도움이 될 겁니다. 문법 차이만 나열하지 않고, XMLHttpRequest부터 2026년 현재까지의 흐름 속에서 두 도구를 놓고 보겠습니다.

목차

  1. Ajax는 라이브러리가 아니라 기법이었습니다
  2. jQuery $.ajax가 사실상 표준이던 시절
  3. fetch API가 등장한 배경
  4. fetch의 첫 번째 함정 - 404는 에러가 아닙니다
  5. fetch의 두 번째 함정 - 타임아웃과 JSON 처리
  6. axios가 해결해 주는 것들
  7. fetch axios 차이 한눈에 비교
  8. 2026년 지금은 무엇을 써야 할까

1. Ajax는 라이브러리가 아니라 기법이었습니다

먼저 용어부터 정리할 필요가 있습니다. Ajax는 설치하는 라이브러리가 아닙니다. Asynchronous JavaScript and XML의 약자로, "페이지를 새로고침하지 않고 서버와 데이터를 주고받는 기법" 자체를 가리키는 말이었습니다. 그리고 그 기법의 실제 엔진이 브라우저 내장 객체인 XMLHttpRequest였습니다.

XMLHttpRequest를 날것으로 쓰면 이런 모양이 됩니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      var data = JSON.parse(xhr.responseText);
      console.log(data);
    } else {
      console.error('요청 실패: ' + xhr.status);
    }
  }
};
xhr.send();

 

readyState가 4가 될 때까지 기다리고, status를 직접 확인하고, 응답 문자열을 JSON.parse로 직접 파싱합니다. GET 하나에 이 정도 코드가 필요했습니다. POST로 폼 데이터를 보내려면 헤더 설정과 인코딩까지 손으로 다 해야 했고요. 이걸 매번 쓰는 사람은 사실 거의 없었습니다. 다들 무언가로 감싸서 썼는데, 그 "무언가"가 바로 jQuery였습니다.

2. jQuery $.ajax가 사실상 표준이던 시절

제가 실무를 시작한 2012년 무렵에는 비동기 통신 코드를 짠다는 말이 곧 $.ajax를 쓴다는 뜻이었습니다. 당시 만들던 모바일 웹 페이지들도 전부 이 패턴이었습니다.

$.ajax({
  url: '/api/users',
  type: 'GET',
  dataType: 'json',
  success: function (data) {
    console.log(data);
  },
  error: function (xhr, status, err) {
    console.error('요청 실패: ' + xhr.status);
  }
});

 

XMLHttpRequest의 잡일을 전부 숨겨주고, dataType만 지정하면 JSON 파싱도 알아서 해 줬습니다. 그리고 중요한 점 하나. 서버가 404나 500을 돌려주면 error 콜백이 호출됐습니다. HTTP 상태 코드가 실패면 에러로 취급한다는, 개발자 입장에서 직관적인 동작이었습니다. 이 감각이 나중에 fetch를 만났을 때 발목을 잡습니다.

물론 이 시절이 좋기만 했던 건 아닙니다. 요청 결과를 받아 다음 요청을 보내고, 그 결과로 또 요청을 보내는 화면을 만들다 보면 success 안에 success가 계속 중첩됐습니다. 흔히 콜백 지옥이라고 부르던 그 코드입니다. 당시엔 그게 이상하다는 생각조차 못 하고 들여쓰기를 여섯 단계씩 파고 들어갔습니다.

📌 시대 배경
2012년 전후엔 IE 하위 버전 호환이 필수 요구사항이었습니다. XMLHttpRequest조차 브라우저마다 생성 방식이 달랐기 때문에, 이 차이를 흡수해 주는 jQuery는 선택이 아니라 생존 도구에 가까웠습니다. 지금 기준으로 당시 코드를 평가하면 안 되는 이유입니다.

3. fetch API가 등장한 배경

2015년을 전후해 상황이 바뀝니다. ES6에서 Promise가 언어 표준이 되면서, 콜백 기반의 XMLHttpRequest를 대체할 새로운 표준 API로 fetch가 브라우저에 들어왔습니다. 라이브러리 없이 브라우저가 직접 Promise 기반 HTTP 통신을 제공하게 된 겁니다.

// 기본 사용법 - async/await와 결합하면 더 깔끔해집니다
const response = await fetch('/api/users');
const data = await response.json();
console.log(data);

XMLHttpRequest 코드와 비교하면 차이가 큽니다. readyState 확인도, 수동 JSON.parse도 없습니다. 이후 async/await까지 언어에 들어오면서 비동기 코드가 동기 코드처럼 읽히게 됐고, fetch는 그 흐름의 중심에 있었습니다.

브라우저만의 이야기도 아닙니다. Node.js도 18 버전부터 fetch를 내장하기 시작해서, 이제는 브라우저와 서버가 같은 표준 API로 HTTP 요청을 보낼 수 있습니다. "Ajax 한다"는 말이 사라지고 "fetch 한다"는 말이 그 자리를 차지한 배경입니다.

그런데 fetch는 jQuery Ajax의 후계자가 아닙니다. 설계 철학이 다릅니다. fetch는 편의 기능을 얹은 도구가 아니라, HTTP 통신의 저수준 기본기를 표준으로 만든 API입니다. 이 차이를 모르고 쓰면 함정에 빠집니다. 제가 빠졌던 것처럼요.

4. fetch의 첫 번째 함정 - 404는 에러가 아닙니다

fetch로 옮겨온 개발자가 가장 먼저 당하는 지점입니다. 아래 코드를 보겠습니다.

try {
  const response = await fetch('/api/users/999'); // 존재하지 않는 유저
  const data = await response.json();
  console.log(data);
} catch (err) {
  // 404여도 여기로 안 옵니다
  console.error('에러 발생:', err);
}

 

서버가 404를 돌려줘도 catch 블록은 실행되지 않습니다. fetch 입장에서 "서버가 응답을 줬다"는 사실 자체는 통신 성공이기 때문입니다. fetch가 reject되는 경우는 네트워크 단절, CORS 차단처럼 응답 자체를 못 받은 상황뿐입니다. 그래서 상태 코드 확인은 개발자 몫입니다.

const response = await fetch('/api/users/999');

if (!response.ok) { // status가 200번대가 아니면 false
  throw new Error(`HTTP 에러: ${response.status}`);
}

const data = await response.json();

 

response.ok 체크 한 줄을 모든 요청마다 넣어야 합니다. 저는 이걸 몰라서 에러 페이지 대신 undefined 데이터로 화면을 그리는 버그를 만든 적이 있습니다. 서버 로그엔 404가 찍히는데 프론트 콘솔은 조용하니, 원인을 찾는 데 시간이 꽤 걸렸습니다. jQuery 시절의 "실패하면 error 콜백"이라는 감각이 오히려 독이 된 경우였습니다.

💡 왜 이렇게 설계했을까
fetch는 "HTTP 통신이 이루어졌는가"와 "응답 내용이 원하는 것인가"를 분리해서 봅니다. 404도 서버가 보낸 정상적인 HTTP 응답이라는 관점입니다. 표준 API로서는 일관된 철학이지만, 매번 ok 체크를 강제한다는 점에서 실무 편의와는 거리가 있습니다.

5. fetch의 두 번째 함정 - 타임아웃과 JSON 처리

fetch에는 타임아웃 옵션이 없습니다. 서버가 응답을 안 주면 브라우저 기본 한계까지 하염없이 기다립니다. 사내망이나 모바일 환경처럼 네트워크가 불안정한 곳에서는 치명적입니다. 예전에는 setTimeout과 AbortController를 조합해 직접 구현해야 했는데, 지금은 AbortSignal.timeout()이 표준에 들어와서 한 줄로 처리됩니다. 2024년부터 주요 브라우저에서 기본 지원됩니다.

try {
  const response = await fetch('/api/slow-endpoint', {
    signal: AbortSignal.timeout(5000) // 5초 제한
  });
  if (!response.ok) throw new Error(`HTTP 에러: ${response.status}`);
  const data = await response.json();
} catch (err) {
  if (err.name === 'TimeoutError') {
    console.error('5초 안에 응답이 오지 않았습니다');
  }
}

 

JSON 전송도 손이 갑니다. POST로 객체를 보내려면 헤더 지정과 문자열 변환을 직접 해야 합니다.

await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: '홍길동' }) // 직접 문자열로 변환
});

 

한두 번이면 괜찮은데, 프로젝트 전체에서 매번 이 보일러플레이트를 반복하다 보면 결국 공통 함수로 감싸게 됩니다. ok 체크, 타임아웃, JSON 변환, 에러 가공까지 넣다 보면 어느새 작은 HTTP 라이브러리를 하나 만들고 있는 자신을 발견합니다. 그리고 그 지점에서 이런 생각이 듭니다. 이거, 누가 이미 만들어 놓지 않았을까?

6. axios가 해결해 주는 것들

그 답이 axios입니다. 2014년에 나온 Promise 기반 HTTP 클라이언트로, 방금 나열한 불편을 대부분 기본값으로 해결합니다. 2026년 현재도 npm에서 주간 수천만 건씩 내려받는, 이 분야에서 가장 널리 쓰이는 라이브러리입니다.

import axios from 'axios';

try {
  // JSON 변환, Content-Type 헤더 자동 처리
  const res = await axios.post('/api/users',
    { name: '홍길동' },
    { timeout: 5000 } // 타임아웃 옵션 내장
  );
  console.log(res.data); // 파싱된 JSON이 바로 나옴
} catch (err) {
  // 404, 500 등 실패 상태 코드도 여기로 떨어짐
  if (err.response) {
    console.error('서버 응답 에러:', err.response.status);
  } else {
    console.error('요청 자체가 실패:', err.message);
  }
}

fetch에서 손으로 하던 일들이 전부 사라졌습니다. 4xx, 5xx 응답이 catch로 떨어지는 동작은 jQuery 시절의 감각과 같아서, 저처럼 그 시절을 지나온 개발자에게는 오히려 axios 쪽이 자연스럽게 느껴집니다.

규모가 있는 프로젝트에서 진짜 힘을 발휘하는 건 인터셉터입니다. 모든 요청과 응답이 지나가는 길목에 공통 로직을 심을 수 있습니다.

const api = axios.create({ baseURL: '/api', timeout: 5000 });

// 모든 요청에 인증 토큰 자동 첨부
api.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${getToken()}`;
  return config;
});

// 401 응답이 오면 어디서든 로그인 페이지로
api.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status === 401) {
      location.href = '/login';
    }
    return Promise.reject(err);
  }
);

인증 토큰 첨부, 공통 에러 처리, 로깅 같은 횡단 관심사를 요청 코드 바깥에서 한 번에 관리할 수 있습니다. fetch로 이걸 하려면 래퍼 함수를 직접 설계해야 하는데, 팀 규모가 커질수록 그 래퍼의 유지보수가 부담이 됩니다.

참고로 axios는 브라우저에서 내부적으로 XMLHttpRequest를 사용합니다. fetch보다 오래된 엔진 위에서 돌아간다는 뜻인데, 업로드 진행률 이벤트처럼 fetch에 아직 없는 기능을 제공할 수 있는 이유이기도 합니다.

7. fetch axios 차이 한눈에 비교

여기까지의 내용을 표로 정리하면 이렇습니다.

반응형
항목 fetch axios
설치 불필요 (브라우저·Node 18+ 내장) npm 설치 필요 (십수 KB 수준)
4xx/5xx 처리 reject 안 됨, response.ok 직접 확인 에러로 reject되어 catch로 이동
JSON 처리 json() 호출·stringify 직접 요청·응답 모두 자동 변환
타임아웃 AbortSignal.timeout()으로 지정 timeout 옵션 내장
인터셉터 없음 (래퍼 직접 구현) 요청·응답 인터셉터 내장
내부 엔진(브라우저) 브라우저 네이티브 구현 XMLHttpRequest 기반
엣지 런타임 Cloudflare Workers 등에서 동작 Node 전용 모듈 의존으로 동작 불가
업로드 진행률 표준 이벤트 없음 onUploadProgress 제공

표만 보면 axios가 편의성에서 앞서는 것처럼 보이지만, 마지막에서 두 번째 줄에 주목할 필요가 있습니다. Cloudflare Workers나 Vercel Edge 같은 엣지 런타임에서는 Node의 http 모듈이 없어서 axios가 아예 동작하지 않습니다. 실행 환경이 다양해질수록 표준 API인 fetch의 가치가 올라가는 구조입니다.

8. 2026년 지금은 무엇을 써야 할까

제 기준은 이렇습니다. 확실한 정답이라기보다 현장에서 쓰는 판단 기준 정도로 봐 주시면 좋겠습니다.

요청이 몇 개 안 되는 작은 프로젝트, 정적 사이트, 서버리스 함수라면 fetch면 충분합니다. 의존성 하나 없이 표준만으로 해결되고, AbortSignal.timeout이 들어온 뒤로는 예전만큼 불편하지도 않습니다. 반대로 인증 토큰 갱신, 공통 에러 처리, 여러 API 서버를 오가는 규모의 서비스라면 axios의 인터셉터와 인스턴스 구조가 주는 이득이 큽니다. 이미 axios로 짜인 코드베이스를 fetch로 갈아엎을 이유는 더더욱 없고요.

요즘은 제3의 선택지도 있습니다. fetch를 얇게 감싸서 axios 수준의 편의를 제공하는 ky, ofetch 같은 라이브러리들입니다. 용량이 axios의 몇 분의 일 수준이고 표준 fetch 위에서 돌아가기 때문에 엣지 런타임에서도 그대로 쓸 수 있습니다. 새 프로젝트라면 이쪽을 검토해 볼 가치가 있다고 봅니다. 다만 생태계와 레퍼런스 양은 아직 axios가 앞서니, 팀원 구성에 따라 답이 달라질 수 있습니다.

돌아보면 XMLHttpRequest에서 jQuery로, fetch로, 다시 fetch 래퍼로 이어지는 이 흐름은 결국 하나의 방향이었습니다. 플랫폼 표준이 저수준 기반을 다지면, 그 위의 편의는 가벼운 라이브러리가 채우는 구조로 계속 이동해 온 겁니다. 지금 뭘 배워야 하냐고 묻는다면, 도구 이전에 fetch가 왜 404를 에러로 안 던지는지부터 이해하시라고 답하고 싶습니다. 그 설계 의도를 알면 axios를 쓰든 ky를 쓰든 헤맬 일이 줄어듭니다.

자주 묻는 질문

jQuery Ajax는 이제 배울 필요가 없나요?

새로 배울 필요는 없습니다. 다만 오래된 사내 시스템이나 레거시 프로젝트에는 여전히 $.ajax 코드가 살아 있어서, 유지보수 업무를 맡으면 읽을 줄은 알아야 합니다. 콜백 구조만 이해하면 읽는 건 어렵지 않습니다.

axios도 결국 XMLHttpRequest면 구식 아닌가요?

엔진이 오래됐다고 도구가 구식인 건 아닙니다. XMLHttpRequest는 여전히 모든 브라우저에서 잘 동작하고, axios는 이를 기반으로 업로드 진행률처럼 fetch가 못 하는 기능도 제공합니다. 2026년 현재도 꾸준히 새 버전이 나오며 관리되고 있습니다. 다만 엣지 런타임 미지원 같은 구조적 한계는 이 엔진 선택에서 비롯된 것이 맞습니다.

React나 Vue에서는 뭘 쓰는 게 좋나요?

프레임워크 자체는 어느 쪽이든 상관없습니다. 실무에서는 TanStack Query 같은 서버 상태 라이브러리와 조합하는 경우가 많은데, 이때 실제 요청 함수는 fetch든 axios든 자유롭게 끼울 수 있습니다. 팀의 기존 코드베이스를 따라가는 게 보통 가장 나은 선택입니다.

fetch만 쓰면 인터셉터 같은 건 포기해야 하나요?

직접 래퍼 함수를 만들거나, ky처럼 훅(hook) 시스템을 제공하는 fetch 래퍼를 쓰면 됩니다. ky의 beforeRequest, afterResponse 훅이 axios 인터셉터와 비슷한 역할을 합니다. 규모가 작다면 async 함수 하나로 감싸는 것만으로도 충분한 경우가 많습니다.

📚 함께 보면 좋은 글

 

타입스크립트는 왜 만들어졌을까 - 자바스크립트가 커지자 마이크로소프트가 내놓은 답

"자바스크립트는 너무 자유분방해서 탈이야." "이거 실행해봐야 에러가 나는지 알 수 있잖아." "남이 짠 자바스크립트 코드는 대체 뭘 주고받는지 모르겠어." 웹 개발을 오래 하다 보면 자바스크

devwar.tistory.com

 

자바와 자바스크립트 차이는? 10일 만에 만들어진 자바스크립트 역사 이야기

자바(Java)와 자바스크립트(JavaScript)의 차이는 개발 공부를 막 시작한 분들이 가장 많이 헷갈려 하는 부분입니다. "자바랑 자바스크립트랑 같은 거 아니에요?" 제가 13년째 가장 자주 듣는 질문이

devwar.tistory.com

 

HTML 역사, 누가 왜 만들었을까 (태그 18개의 시작)

HTML 역사를 조금만 거슬러 올라가 보면, 우리가 매일 아무렇지 않게 넘기는 웹페이지가 원래는 스위스의 한 물리학 연구소에서 문서 몇 개를 편하게 주고받으려고 급조한 도구였다는 사실과 마

devwar.tistory.com

 

Node.js 탄생 비화, 자바스크립트가 서버를 삼킨 날

Node.js는 브라우저 안에서만 돌던 자바스크립트를 서버로 끌어낸 런타임입니다. 2009년에 등장한 이 도구 하나가 이후 십수 년간 웹 개발의 판을 바꿔 놨는데요. 오늘은 그 탄생 비화를, 당시 업계

devwar.tistory.com

 

Git 역사, 리누스가 열흘 만에 만든 버전관리 도구

Git 역사를 거슬러 올라가 보면 시작은 의외로 초라합니다. 지금은 전 세계 개발자 대부분이 매일 쓰는 도구지만, 애초에는 리누스 토르발스 한 사람이 자기 리눅스 커널을 관리하려고 급하게 만

devwar.tistory.com

반응형