AJAX - 네트워크 요청을 비동기적으로 처리하기 위한

2025. 11. 15. 21:40·Develop/Javascript

초기 웹은 문서 교환이 목적이었다. 오직 HTML만 존재했고, CSS와 JS는 존재하지도 않았다. 

HTML 문서를 주고 받으며 브라우저를 통해 렌더링했다. 또한 form 요소를 통한 get, post만 존재했다. 

(혹은 a 요소를 이용해 새로운 링크로 요청을 보내 데이터를 가져오는 형식이었다.)

 

즉, 요청을 주고 받는다면 HTML 문서 전체를 주고 받아 처음부터 끝까지 다시 렌더링하는 방식이었다.

그래서 form의 기본 동작이 reload 였던 것이었다.

 

하지만 이후 "필요한 부분만 데이터를 가져와서 반영" 하기를 원하기 시작했다.

이를 위해 아주 간단한 스크립트 언어였던 자바스크립트를 이용하여 데이터를 주고 받을 수 있지 않을까 하는 아이디어가 나왔다.

이를 위해 비동기 처리라는 개념이 나왔고, AJAX가 탄생했다.

 

AJAX는 Asynchronous JavaScript And XML 라는 뜻으로, 말 그대로 자바스크립트와 XML 형식을 이용해서 비동기처리를 하겠다는 뜻이다. 하지만 XML 뿐만 아니라 JSON 형식 또한 처리가 가능하다. 

 

HTML은 화면에 UI를 그리기 위한 마크업 언어라면, XML은 데이터를 주고 받기 위한 마크업 언어였다.

XML 은 HTML과 달리 사용자가 직접 요소를 정해 데이터를 주고 받을 수 있다. 하지만 좀 더 제한적인 룰을 가지고 있긴 하다. 

HTML의 탄생 이후에 XML이 탄생했고, XML은 역으로 다시 HTML5에 영향을 주었다.

XML의 예시

<paragraph>
  오늘은 <strong>날씨</strong>가 좋다.
  <link href="https://example.com">관련 기사</link>
</paragraph>

 

 

여튼, XML 형식에 대한 이야기는 여기까지만 하고, 이제 AJAX에 대해 깊게 알아보자.


AJAX 방식

결국 AJAX는 자바스크립트와 XML 형식을 이용하여 비동기 처리를 하겠다라는 의미다. (참고로 이때부터 JS의 이벤트루프 개념이 도입)

이를 위해 초기에 사용했던 방식은 XMLHttpRequest(이하 xhr) 객체를 만들어 API 요청을 주고 받았다.

1. XHR 을 이용하여 비동기 처리

// XMLHttpRequest로 여러 API를 순차적으로 호출
const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/user/1");
xhr.onload = function() {
    const user = JSON.parse(xhr.responseText);
    
    const xhr2 = new XMLHttpRequest();
    xhr2.open("GET", `/api/posts?userId=${user.id}`);
    xhr2.onload = function() {
        const posts = JSON.parse(xhr2.responseText);

        const xhr3 = new XMLHttpRequest();
        xhr3.open("GET", `/api/comments?postId=${posts[0].id}`);
        xhr3.onload = function() {
            const comments = JSON.parse(xhr3.responseText);
            console.log("User:", user);
            console.log("Posts:", posts);
            console.log("Comments:", comments);
        };
        xhr3.send();
    };
    xhr2.send();
};
xhr.send();

 

xhr.onload 프로퍼티에 바인딩된 함수는 요청에 대한 응답이 왔을 때 실행이 되는 것이다. 

근데 이 방식을 사용하다보면 순차적으로 실행해야하는 코드의 경우 onload 내부에서 또 onload를 연결해두고, 그 내부에 또 onload를 연결하는... 위와 같은 코드가 만들어진다.

 

그게 "콜백 헬(Callback hell)" 이다.

콜백 안에 콜백을 넣는 형태라서 가독성이 좋지 않은 상태이다. 

현재는 이를 Promise를 반환하는 fetch를 이용하여 AJAX를 구현한다.

 

2. fetch 을 이용하여 비동기 처리

2-1 . Promise의 탄생

Promise 객체는 비동기 처리를 위한 객체다. 

 

Promise.then 메서드나 Promise.catch 메서드를 통해 비동기적으로 데이터가 왔을 때를 처리할 수 있게끔 만들어준다.

또한 이외의 비동기 기능을 정적으로 제공해주는 Promise.allSettled, Promise.all, Promise.race 등을 제공하여 더욱 우아하게 비동기를 처리할 수 있게끔 도와준다.  

 

Promise 라는 이름이 붙은 것도 비동기 작업의 결과를 미래에 제공하겠다는 '약속'을 상징하기 때문이다. 

(물론, 약속은 어겨질 수도 있다. 영영 결과가 안오는 pending 상태일 수도 있다.)

 

2-2 . fetch의 탄생

fetch는 Promise를 반환하는 함수며, Promise를 기반으로 비동기 처리를 제공해주는 webAPI다. 
(물론 node.js에서도 동일한 이름의 함수가 제공되는 것으로 알고 있기는 하다)

 

fetch를 사용하여 위의 콜백헬과 동일한 동작을 하는 코드를 작성하면 아래와 같다.

fetch("/api/user/1")
  .then(response => response.json())
  .then(user => {
      return fetch(`/api/posts?userId=${user.id}`)
        .then(response => response.json())
        .then(posts => {
            return fetch(`/api/comments?postId=${posts[0].id}`)
              .then(response => response.json())
              .then(comments => {
                  console.log("User:", user);
                  console.log("Posts:", posts);
                  console.log("Comments:", comments);
              });
        });
  })
  .catch(err => console.error(err));

XHR을 사용할 때보다는 낫긴 하지만, 여전히 가독성이 좋지 않은 것은 마찬가지이다. 

 

3. fetch  + 제네레이터 함수를 이용하여 비동기 처리 

이에 정말 "동기적인것 처럼" 비동기를 처리하여 가독성을 올리려는 시도가 탄생했다.

fetch 함수와 제네레이터 함수를 결합하여 사용한 것이다. (이를 구현한 대표적인 라이브러리가 co 이다)

function* fetchDataGenerator() {
  // 1단계: 사용자 정보
  const userResponse = yield fetch("/api/user/1");
  const user = yield userResponse.json();
  console.log("User:", user);

  // 2단계: 사용자 포스트
  const postsResponse = yield fetch(`/api/posts?userId=${user.id}`);
  const posts = yield postsResponse.json();
  console.log("Posts:", posts);

  // 3단계: 첫 포스트 댓글
  const commentsResponse = yield fetch(`/api/comments?postId=${posts[0].id}`);
  const comments = yield commentsResponse.json();
  console.log("Comments:", comments);
}

// 실행 헬퍼 함수
function run(generatorFunc) {
  const gen = generatorFunc();

  function step(nextValue) {
    const result = gen.next(nextValue); // yield에서 멈춘 곳 재개
    if (!result.done) {
      // yield에서 받은 값이 Promise이면 처리
      if (result.value instanceof Promise) {
        result.value.then(res => step(res));
      } else {
        step(result.value);
      }
    }
  }

  step();
}

// 실행
run(fetchDataGenerator);
더보기

제네레이터 함수를 실행하면 제네레이터 객체를 반환하며, 제네레이터 객체의 .next 메서드를 사용하여 yield 된 값을 받아올 수 있고, yield 에 대입할 값을 next 메서드의 인자로 넘겨줄 수 있다. next 메서드는 value, done 프로퍼티를 갖는 객체를 반환한다. 

마지막 yield까지 이미 실행되어 더 이상 실행할 코드가 없는데 next가 다시 호출되면 {value: undefined, done: true} 가 반환된다. 

확실히 동기적인것처럼 코드를 작성하여 가독성이 좋아졌다.

 

4. async-await 를 이용하여 비동기 처리

하도 사람들이 3번의 방법을 사용하니, 아예 이걸 표준화한 async-await 구문이 도입되었다. 

이를 통해 이제 아주 편리하게 동기적인것처럼 비동기코드를 처리할 수 있게 되었으며, 에러 처리 또한 try-catch 블록을 안에 두어 해결할 수 있었다. 

async function loadData() {
  try {
    // 1단계: 사용자 정보 가져오기
    const userRes = await fetch("/api/user/1");
    const user = await userRes.json();
    console.log("User:", user);

    // 2단계: 사용자 포스트 가져오기
    const postsRes = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsRes.json();
    console.log("Posts:", posts);

    // 3단계: 첫 번째 포스트 댓글 가져오기
    const commentsRes = await fetch(`/api/comments?postId=${posts[0].id}`);
    const comments = await commentsRes.json();
    console.log("Comments:", comments);

    // 화면에 반영
    document.getElementById("username").textContent = user.name;
    document.getElementById("postTitle").textContent = posts[0].title;
    document.getElementById("comment").textContent = comments[0].text;

  } catch (err) {
    console.error("데이터 로딩 중 에러 발생:", err);
  }
}

// 실행
loadData();

 

fetch 함수는 Response 객체를 결과값으로 가지는 Promise를 반환한다. 

또한 Response 객체의 인스턴스 메서드인 response.json()도 비동기적으로 동작하며, Promise를 반환한다. 

(정적 메서드인 Response.json(객체) 의 경우에는 동기적으로 동작하긴 한다.)

 

Promise의 결과값을 사용하려면 반드시 await 을 해서 사용해야만 한다. (혹은 then 메서드의 콜백 내부에서만 사용 가능)

이렇게 설계된 이유는 "어차피 Promise를 만들었는데 즉시 값을 얻고 싶다는 요구는 설계 철학에 맞지 않다" 때문이지 않을까?

 

Promise든 Response든 비동기적인 처리를 해야하고, 비동기처리에는 항상 이벤트 루프가 개입된다.

또한 이벤트루프가 개입된다는 이야기는 반드시 WebAPI 라는 뜻이다. (하지만 역으로, WebAPI라고 해서 반드시 이벤트 루프가 개입되는 것은 아니다. DOM 조작 API도 WebAPI 지만 이벤트 루프가 개입하지는 않는다.)

 

더보기

종종 axios를 자주 사용하다보면 fetch의 동작 방식과 헷갈리곤 한다. 

axios 라이브러리는 자체적으로 data 라는 프로퍼티에 response를 json화한 값을 바인딩해서 반환해주고,

fetch는 response.json()을 하여 직접 사용해야한다.

 

종종 response.data.data 같은 로직을 작성해봤거나 본 적이 있다면, 분명 axios를 사용했을 것이다.

서버에서는 response로 보통 {code: number, message: string, data: T} 를 넘겨주기 때문이다. 

 

 

https://developer.mozilla.org/ko/docs/Learn_web_development/Core/Scripting/Network_requests#xmlhttprequest_api

'Develop > Javascript' 카테고리의 다른 글

ESM과 CJS 그리고 모듈 객체  (0) 2025.09.22
이벤트 (Event)  (0) 2025.05.25
클래스 (Class)  (0) 2025.04.13
프로토타입 (ProtoType)  (0) 2025.02.11
클로저 (Closure)  (0) 2025.02.06
'Develop/Javascript' 카테고리의 다른 글
  • ESM과 CJS 그리고 모듈 객체
  • 이벤트 (Event)
  • 클래스 (Class)
  • 프로토타입 (ProtoType)
ocahs
ocahs
개발 내용을 담습니다.
  • ocahs
    ocahs 개발 블로그
    ocahs
  • 전체
    오늘
    어제
    • 분류 전체보기 (48)
      • Develop (2)
        • Frontend (25)
        • Javascript (7)
        • Algorithm (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    번들러
    line sweeping
    Promise
    요청의 역사
    vite
    Working Set Model
    JS
    promise reject
    비트 연산자 활용 예시
    재귀타입
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ocahs
AJAX - 네트워크 요청을 비동기적으로 처리하기 위한
상단으로

티스토리툴바