XHR에는 많은 단점이 존재한다.
이러한 단점 때문에 개발자는 XHR을 대체하는 방법을 생각했고,
결과적으로 fetch가 탄생하게 되었다.
대표적인 단점을 살펴보면서 왜 XHR을 대체하는 fetch가 탄생하게 되었는지 생각해보자.
1. 너무 장황하고 복잡한 보일러플레이트 코드
function xml() {
const xhr = new XMLHttpRequest(); // 1. 객체 생성
xhr.open('GET', BASE_URL + '/api/users/1', true); // 2. 요청 초기화
xhr.onload = function () { // 3. 요청 완료 이벤트 리스너 등록
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error(xhr.status);
};
};
xhr.onerror = function () { 4. 네트워크 에러 이벤트 리스너 등록
console.error("네트워크 에러");
};
xhr.send(); // 5. 전송
}
일반적으로 XHR을 이용하여 코드를 작성하면 위의 코드와 같이 작성한다.
단순히 서버에서 데이터를 가져올 때도 위와 같이 여러 단계를 거쳐야 했다.
1. XHR 객체를 생성하기.
2. 요청을 보내기 위한 초기화 설정하기.
3. onload와 onerror 이벤트 등록하기.
4. 서버에 전송하기.
직접 객체를 생성하고,
해당하는 요청의 (method, URL, header)를 등록하고,
각 이벤트에 콜백 함수를 등록해서 각 상태에 맞는 조건문을 작성하고,
서버에 전송을 요청하는 코드를 직접 작성해야했다.
지금은 괜찮아 보일지라도 이러한 함수가 서버에 요청할 때마다 계속 만들어지다 보면 코드가 길어지고 복잡해진다.
2. 콜백 지옥
콜백 지옥이라는 말을 들어보았을 것이다.
비동기 작업을 콜백 함수로 계속 이어 붙이면서 코드가 깊게 중첩되고 가독성이 매우 나빠지는 것이다.
예시를 들어 콜백 지옥을 만들어 보자.
1. 유저 정보를 가져오고,
2. 그 유저의 게시글을 가져오고,
3. 그 게시글의 댓글을 가져와보자.
function xml() {
const xhr1 = new XMLHttpRequest();
xhr1.open('GET', '/api/users/1', true);
xhr1.onload = function () {
const user = JSON.parse(xhr1.responseText);
// 1번째 통신이 끝난 콜백 함수 내부에서 2번째 통신이 시작된다.
const xhr2 = new XMLHttpRequest();
xhr2.open('GET', `/api/posts?userId=${user.userId}`);
xhr2.onload = function() {
const posts = JSON.parse(xhr2.responseText);
// 2번째 통신이 끝난 콜백 함수 내부에서 3번째 통신이 시작된다.
const xhr3 = new XMLHttpRequest();
xhr3.open('GET', `/api/comments?postId=${posts[0].id}`);
xhr3.onload = function () {
const comments = JSON.parse(xhr3.responseText);
};
xhr3.send(); // 해당 게시글의 댓글 목록을 요청한다.
};
xhr2.send(); // 해당 유저의 게시글을 요청한다.
};
xhr1.send(); // 유저 정보를 요청한다.
}
위의 코드와 같이 첫 xhr의 통신 성공 콜백 함수 내부에서 새로운 xhr 객체가 생성되고, 그 과정을 반복하면서 콜백 지옥이 시작되게 된다.
가장 간단하게 작성한 코드를 한 눈에 보기 어려운데, 통신이 더 복잡해질수록 콜백 지옥이 더 심해진다.
이러한 콜백 지옥을 예방하고자 개발자는 새로운 도구를 만들기 시작했다.
3. Promise 미지원
위와 같은 콜백 지옥을 없애기 위해 자바스크립트 생태계는 Promise라는 비동기 처리 표준이 등장했다.
하지만, XHR은 너무 옛날에 만들어진 API였기 때문에 Promise를 기본적으로 지원하지 않았다.
XHR을 사용할 때마다 직접 Promise로 한 번 감싸서 작성해야 했다.
직접 Promise로 한 번 감싸서 작성했는 코드는 아래와 같다.
function xml(url) {
// 함수가 실행되면 새로운 Promise 객체를 생성해서 반환한다.
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 성공했다면, 파싱된 데이터를 resolve(해결)에 담아 보낸다.
resolve(JSON.parse(xhr.responseText));
} else {
// 404, 500 등 HTTP 에러일 때 reject에 에러를 담아 보낸다.
reject(new Error(`HTTP 에러: 상태 코드 ${xhr.status}`));
}
};
xhr.onerror = function() {
// 네트워크 에러도 동일하게 reject로 보낸다.
reject(new Error("네트워크 연결 실패"));
};
xhr.send();
});
}
위의 함수를 프로젝트마다 유틸리티 폴더에 하나씩 만들어 두고 사용했다.
이러한 함수를 매번 만들어 사용하기 불편하기 때문에 ,
아예 브라우저 내장 기능으로 fetch를 만들어 주게 되었다.
참고로 위와 같은 함수를 만들어 사용하는 코드는 우리가 현재 사용하는 fetch나 axios의 문법과 동일하게 된다.
아래에 xml() 함수를 사용한 예시가 있다.
Promise 체이닝으로 사용할 때
xml('https://~~')
.then(data => {
console.log("성공", cata);
})
.catch(error => {
console.error("에러", error.message);
});
async/await로 사용할 때
async function getData() {
try {
const data = await xml("https:~~");
console.log("성공", data);
} catch (error) {
console.error("에러", error.message);
}
}
즉, 브라우저 개발자들은 개발자들이 매번 XHR을 Promise로 감싸서 사용하는 불편을 해결하기 위해,
매번 수동으로 만들던 Promise 래핑 함수를, 아예 브라우저의 표준 내장 API로 만든 것이 바로 fetch이다.
결과적으로, 지금 fetch나 axios에서 편하게 .then()이나 await를 사용할 수 있는 이유는, 과거 개발자들이 겪었던 불편함을 바탕으로 내부에서 성공(resolve)과 실패(reject)를 분류해주고 있기 때문이다.
4. 관심사 분리 실패
XHR은 이름부터가 XML을 위한 것처럼 생겼지만, 사실상 모든 데이터를 처리한다.
게다가 오직 하나의 xhr 객체 안에 요청(Request), 응답(Response), 헤더(Headers), 상태 코드 등이 뒤섞여 있다.
데이터의 흐름을 통제하기에 너무 무겁고 유연하지 못한 구조이다.
const xhr = new XMLHttpRequest();
// ---------------------------------------------------------
// 1. [Request: 내가 보낼 요청에 대한 관심사]
// ---------------------------------------------------------
xhr.open('POST', '/api/data');
xhr.setRequestHeader('Authorization', 'Bearer token123'); // 내가 보낼 헤더 세팅
xhr.timeout = 5000; // 내가 기다릴 최대 시간 세팅
xhr.send(JSON.stringify({ msg: "hello" })); // 내가 보낼 바디(데이터) 세팅
// ---------------------------------------------------------
// 2. [State & Event: 통신 흐름과 상태에 대한 관심사]
// ---------------------------------------------------------
xhr.onload = function() { ... }; // 성공 시 흐름 제어
xhr.onerror = function() { ... }; // 실패 시 흐름 제어
console.log(xhr.readyState); // 현재 통신 진행 상태 (0~4)
// ---------------------------------------------------------
// 3. [Response: 서버가 돌려준 결과에 대한 관심사]
// ---------------------------------------------------------
xhr.onload = function() {
console.log(xhr.status); // 서버가 보낸 상태 코드 (예: 200, 404)
console.log(xhr.getResponseHeader('Content-Type')); // 서버가 보낸 헤더 읽기
console.log(xhr.responseText); // 서버가 보낸 바디(데이터) 읽기
};
위와 같이 동일한 객체 안에 Request, Response, Headers, 상태 코드 등이 같이 있으면 다음과 같은 문제가 발생한다.
- 재사용 불가: 내가 세팅한 Request 정보만 따로 분리해서 다른 곳에 전달하거나 복사해서 재사용하고 싶은데, 어쩔 수 없이 Response 데이터와 State 값까지 통째로 묶여서 분리할 수 없게 된다.
- 무거움: 통신 끝난 후에는 순수히 데이터(Response)만 필요한데, 이미 쓸모가 없어진 요청 정보와 이벤트 리스너까지 하나의 덩어리로 메모리에 계속 남아있게 된다.
지금까지 살펴본 1) 장황한 보일러플레이트, 2) 콜백 지옥, 3) Promise 미지원, 그리고 4) 관심사 분리 실패라는 4가지 한계들은 개발자에게 새로운 HTTP 클라이언트의 필요성을 느끼게 했다.
그 결과, 비동기 처리를 Promise로 해결하고, 각 데이터의 관심사를 명확하게 분리해 낸 현대 브라우저의 새로운 표준 API인 fetch가 탄생하게 되었다.
우리가 현재 계속 fetch를 사용하고 있지만, 시간이 지남에 따라 새로운 기술이 생겨나고, 언젠가 fetch를 대체하는 다른 무언가가 새로운 표준으로 탄생하게 될지도 모른다.
'우아한테크코스 8기 > 원정대' 카테고리의 다른 글
| XHR과 fetch의 차이 - 메모리 처리 방식 (0) | 2026.05.04 |
|---|---|
| fetch - 기본 동작 방식 (0) | 2026.04.23 |
| XHR - 왜 알아야하나? (1) | 2026.04.21 |
| 원정대 - HTTP Client (0) | 2026.04.20 |