본문 바로가기
우아한테크코스 8기/원정대

XHR - 왜 알아야하나?

by 쪼꼬에몽 2026. 4. 21.

1. XHR의 등장

XHR이 등장하기 전, 과거의 웹은 동기적으로만 동작했다.

버튼을 누르거나 데이터를 요청하면 무조건 브라우저 화면이 깜빡이며 새로고침되어야 했다.

MS가 처음 도입한 XHR을 통해, 브라우저 백그라운드에서 서버와 비동기적으로 데이터를 주고받는 것이 가능해졌다.

즉, 페이지 새로고침 없이 화면의 일부분만 업데이트할 수 있게 되었다. 

구글이 XHR 기술을 이용해 새로고침 없이 지도를 드래그 할 수 있게 하는 등의 성과를 보여주었다.

이후, XHR을 이용해 화면 깜빡임 없이 비동기적으로 통신하는 기법으로 AJAX(Asynchronous JS and XML)이라는 이름을 붙였다.

2. XHR의 작동 방식

function xml() {
  const xhr = new XMLHttpRequest();

  // xhr.open(method, url, [async, user, password])
  // true면 비동기적, false면 동기적 
  xhr.open('GET', BASE_URL + '/api/users/1', true);

  // 통신이 끝났을 때 실행됨 
  xhr.onload = function () {
    // 성공 시 
    if (xhr.status >= 200 && xhr.status < 300) {
      const data = JSON.parse(xhr.responseText);
      console.log(data);
    } else {
      // 400, 500 에러 
      console.error(xhr.status);
    };
  };

  xhr.onerror = function () {
    console.error("네트워크 에러");
  };

  // 세팅된 요청을 실제로 서버로 전송함 
  // xhr.send(JSON.stringify({ name: "John", age: 20 }));
  xhr.send();
}

 

XHR의 기본 코드는 다음과 같다.

XMLHttpRequest(XHR)를 참조하는 변수 xhr를 만든다.

xhr의 open()을 이용해 통신 준비 상태를 만든다.

xhr의 onload()와 onerror() 이벤트를 설정한다. 통신 결과 상태에 따라 어느 콜백 함수가 실행될지 결정된다.

xhr의 send()를 이용해 Web API의 Network thread에서 서버로 요청을 전송한다. 

해당 내용을 컨셉맵으로 그리면 다음과 같다.

xhr 컨셉맵

 

1. 생성된 xhr 객체가 내장된 open() 메서드를 호출하여 통신을 준비한다.

2. open() 메서드에서 설정한 통신이 성공하면 onload 프로퍼티의 콜백 함수가 실행된다.

3. open() 메서드에서 설정한 통신이 실패하면 onerror 프로퍼티의 콜백 함수가 실행된다.

4. onload 프로퍼티의 콜백 함수는 send()를 통해 서버로 보낸 요청이 완료되면 실행된다.

5. onerror 프로퍼티의 콜백 함수는 send()를 통해 서버로 보낸 요청에 네트워크 에러가 발생하면 실행된다.

6. xhr 객체에 내장된 이벤트 헨들러인 onload와 onerror 프로퍼티에 콜백 함수를 할당한다.

7. xhr 객체가 내장된 send() 메서드를 호출하여 서버로 요청을 전송한다.

8. open() 메서드는 서버로 보낼 요청 정보를 설정한다.

 

이와 같은 과정이 내부적으로 어떻게 처리되는지 확인해보자.

1. 브라우저와 연결 고리 생성 

const xhr = new XMLHttpRequest();

 

  • 브라우저가 제공하는 Web API인 XMLHttpRequest를 참조하는 xhr 객체를 만든다.
  • JS 메모리 힙에 xhr 객체가 생성된다.
  • xhr 객체에는 통신에 필요한 메서드와 프로퍼티가 담겨있다.
  • 동시에, 브라우저 내부에 C++로 작성된 네트워크 처리 객체가 생성된다.
  • 네트워크 처리 객체는 내부 상태 코드를 가진다. 
  • 현재 내부 상태 코드는 UNSENT (0) 이다.
  • xhr 객체와 네트워크 처리 객체는 포인터(메모리 주소)로 서로 바인딩되어 있다. 

바인딩된 상태

  • xhr 객체는 코드가 요구할 때마다 포인터를 타고 넘어가 네트워크 처리 객체의 내부 상태 코드를 실시간으로 들여다볼 수 있다. (getter)
  • 네트워크 처리 객체는 백그라운드에서 통신을 진행하다 내부 상태 코드가 바뀔 때마다 Task Queue에 내부 상태 변화 이벤트를 던진다. (event push)

객체 간 상태 이동

 

2. 통신 규격 초기화 

xhr.open('GET', BASE_URL + '/api/users/1', true);

 

  • JS Call Stack에서 open() 메서드가 실행된다.
  • JS 엔진은 C++ 브라우저의 네트워크 처리 객체에 (GET, URL, true) 인자를 포인터를 통해 넘긴다.
  • 네트워크 처리 객체는 이 정보를 받아서, 자신의 메모리 영역에 HTTP 패킷의 Header에 저장한다.
  • 내부 상태코드를 UNSENT(0)에서 OPENED(1)로 변경한다.  
  • 상태가 변했으니, Task Queue에 readystatechange 이벤트를 던진다.
  • Event Loop가 이 이벤트를 Call Stack으로 끌어올려 실행한다.
  • JS가 내부 상태가 변했다는 것을 인지한다.
  • Call Stack에서 send()가 실행되면, C++ 객체에 만들어둔 HTTP 패킷을 조립하고, Byte Stream으로 변환하 외부로 보낸다.

내부 동작 방식

 

3. 메모리에 콜백 함수 등록 

 xhr.onload = function() {...};

 

  • JS 엔진은 해당 콜백 함수를 Heap Memory 특정 공간에 저장해둔다.
  • 생성된 함수의 메모리 주소를 Heap Memory에 있는 xhr 객체의 onload 프로퍼티에 값으로 넣는다.

콜백 함수 저장

 

그럼 통신 성공 시, 어떻게 xhr에 저장된 onload가 실행되는 것일까?

  • 통신이 완전히 끝나면 네트워크 상태 객체의 내부 상태 코드는 DONE(4)로 변한다.
  • 상태가 변했으니, 이전과 같이 Task Queue에 상태 변화 이벤트를 던진다.
  • 이때, 이 이벤트는 "xhr.onload에 연결된 함수를 실행"하라는 이벤트이다.
  • Event Loop가 Call Stack에 해당 이벤트를 push한다.
  • 해당 이벤트가 실행되면서 xhr.onload에 저장되어 있는 0x100 메모리 주소로 접근한다.
  • 따라서, 콜백 함수 A가 실행된다.

통신 완료 시, 내부 처리 과정

 

3. 메모리에 콜백 함수 등록 

xhr.onerror = function() {...}

 

  • onerror의 동작도 onload와 같다. 한 번, 다시 말해보자.
  • JS 엔진은 해당 콜백 함수를 Heap Memory에 저장한다.
  • 생성된 함수의 메모리 주소를 xhr.onerror 프로퍼티의 값으로 저장한다.

콜백 함수 저장

 

그럼 네트워크 통신 실패 시, 어떻게 xhr.onerror가 실행될 수 있을까?

  • send() 호출 이후, 네트워크 처리 객체가 통신 불가 상태를 감지한다.
  • 통신이 종료되었으므로, 내부 상태를 DONE(4)로 변경한다.
  • 내부 상태가 변경되었으므로, Task Queue에 event push를 한다. 
  • 이때, 전달되는 이벤트가 "xhr.onerror의 함수를 실행"하라는 이벤트이다.
  • Event Loop가 해당 이벤트를 Call Stack에 push하고, 실행하게 된다.
  • xhr.onerror에 저장된 메모리 주소로 이동하여 해당 콜백 함수가 실행된다.

네트워크 에러 발생 시, 내부 처리 과정

 

4. 컨텍스트 전환 및 비동기 위임 

xhr.send();

 

  • Call Stack에서 xhr.send(데이터)가 실행된다.
  • JS 엔진은 send()의 인자로 전달된 데이터(Body)를 네트워크 처리 객체로 넘긴다.
  • 이전에 open()에서 저장해둔 (Method, URL, Header)와 현재 넘겨 받은 데이터(Body)를 합쳐서 완전한 하나의 HTTP Request Message를 조립한다.
  • 조립한 메세지를 Byte Stream으로 변환하고, 외부 서버로 전송한다.

서버로 요청 전송 과정

  • send() 명령을 네트워크 처리 객체로 전달한 직후, xhr.send(데이터)가 Call Stack에서 pop된다.
  • JS 메인 스레드는 네트워크 응답을 기다리지 않고, 다른 동기적 코드를 계속 실행할 수 있다.

그럼 서버부터 응답을 받았을 때, 내부는 어떻게 변하는가?

 

  • 브라우저의 네트워크 스레드가 서버로부터 응답을 받기 시작한다.
  • 내부 상태 코드는 다음과 같이 변한다.
    • HEADERS_RECEIVED (2): HTTP 응답 헤더가 도착했을 때
    • LOADING (3): 응답 본문(Body)을 다운로드하고 있을 때
    • DONE (4): 모든 데이터 수신이 완전히 끝났을 때 
  • 내부 상태 코드가 DONE(4)이 되었다면 우리는 무엇을 하는지 이미 알고 있다.
  • 혹시 기억이 나지 않는다면 이전의 xhr.onload와 xhr.onerror를 다시 읽어 와라.
  • 이처럼 XHR은 내부 상태 코드가 어떻게 변했는가에 따라 이벤트 작동 방식이 달라진다.

3. 왜 우리는 구시대의 유물인 XHR을 알아야 할까?

초기 XHR의 등장은 브라우저와 서버 간의 비동기 통신을 가능하게 하며 웹 생태계에 엄청난 혁명을 일으켰다.

하지만, 현재 프론트엔드 실무에서 XHR 코드를 직접 작성하는 개발자는 거의 없다.

우리는 대부분 fetch, axios, ky와 같은 더 새롭고 간편한 도구들을 사용하고 있다.

 

그렇다면 왜 우리는 이미 쓰지도 않는 XHR의 내부 동작 원리까지 이토록 깊게 파고 들어야 할까?

 

현재 브라우저의 표준 HTTP 클라이언트 규격은 fetch이다. 

그리고 우리가 애용하는 axios나 ky는 이러한 기본 API들의 불편함을 덜어내고 개발자 경험을 극대화하기 위해 탄생한 라이브러리이다.

 

여기서 이 라이브러리들이 내부적으로 어떻게 동작하는지, 무엇이 근본인지를 알고 있는가?

ky는 최신 표준인 fetch를 기반으로 동작하지만, axios는 브라우저 환경에서 여전히 XHR을 핵심 엔진으로 삼아 동작하고 있다.

 

바로 이것이 우리가 지금까지 XHR의 내부 동작을 파헤친 이유이다.

비록 겉으로 쓰지 않는 낡은 도구처럼 보일지 몰라도, 사실 우리는 axios라는 포장지를 통해 매일같이 XHR을 사용하고 있다.

포장지 안에 있는 XHR의 진짜 동작 원리를 정확이 알고 있어야만, 앞으로 우리가 axios를 사용할 때 발생하는 수많은 네트워크 흐름과 예외 상황들을 비로소 100% 통제하고 이해할 수 있다.

 

XHR의 기본적인 내부 동작 방식을 공부해 보았다. 

다음 글은 원정대에서 XHR을 공부하면서 궁금했던 점을 정리한 글을 작성해볼 것이다.

'우아한테크코스 8기 > 원정대' 카테고리의 다른 글

XHR과 fetch의 차이 - 메모리 처리 방식  (0) 2026.05.04
fetch - 기본 동작 방식  (0) 2026.04.23
XHR의 단점  (0) 2026.04.22
원정대 - HTTP Client  (0) 2026.04.20