카테고리 없음

JS 비동기에 대하여 공부하자(3)

양바삭 2025. 1. 26. 22:43

지난 시간까지는 비동기가 필요로 하게 된 배경 및 JS에서 비동기적인 처리를 하는 방법에 대해서 알아보았다. 오늘은 이러한 비동기적 처리를 개발자들이 어떻게 코드로 처리하게 되는 지 정리해볼 것이다. 

 

 

AJAX 이전의 비동기 처리 방법

AJAX 이전에는 비동기 함수 자체가 적었다. 기껏해야 타이머 함수나 이벤트 핸들러정도일까? 그 때는 HTTP 요청조차 동기적으로 진행했기에 비동기 처리에 대한 필요성은 더욱 부각되지 않았다. 따라서 비동기를 처리하는 방법은 태스크 큐와 이벤트 루프를 통한 콜백 함수의 실행으로 구성되어 작업되었다. 그렇기에 초기 비동기 함수들은 애초에 인자에 콜백 함수를 입력하도록 되어있는 듯 하다.

 

 

AJAX 이후의 비동기 처리 방법의 발전

1. 비동기 처리 방법: 콜백 함수

AJAX라는 개념이 등장하면서 비동기를 사용할 일이 급증하였다. HTTP 요청을 비동기적으로 사용하기 시작했기 때문이다. XMLHttpRequest를 활용하여 브라우저에게 HTTP 요청을 처리할 수 있도록 하고, 브라우저는 이전에 쓰고있던 비동기 처리를 이용하여(태스크 큐, 이벤트 루프, 콜백함수) 작업 후 JS에게 작업 결과를 줌으로써 HTTP 요청을 비동기적으로 처리할 수 있는 환경이 구축된 것이다.

< 동기에서 비동기로 발전하게된 HTTP 요청의 과정? >
AJAX가 XMLHttpRequest를 이용하여 비동기 요청 기술을 구현한 것을 지칭하는데, HTTP 요청을 비동기로 하려는 시도가 이미 존재했던 것인가 궁금해서 AI 선생에게 물어보았다.
AJAX의 개념과 유사한 비동기적 HTTP 요청은 마이크로소프트에서 자체적으로 도입되어 사용되고 있었다고 한다. 하지만 무거운 XML 데이터를 HTTP로 받아온다는 개념이 사람들에게 받아들여지지 못했기에 그 기술이 널리 퍼지지는 못한 듯. 그러나 시간이 지나면서 다양한 브라우저 회사들이 꾸준히 이를 연구하였고, 추후 구글에서 Gmail과 구글 맵을 해당 기술을 사용하여 혁신적인 경험을 보여줌에 따라 완전히 조명 받게 되었다. 그 이후에 Jasse James Garrett가 이러한 경험을 AJAX라 지으면서 웹 세계를 지배하게 된 것!
(https://web.archive.org/web/20150910072359/http://adaptivepath.org/ideas/ajax-new-approach-web-applications/ :AJAX를 처음 사용한 글)

 

* XMLHttpRequest를 활용한 비동기 처리 예시

아래는 XMLHttpRequest을 통해 비동기 요청을 브라우저에 하여, 콜백 함수인 onload 혹은 onerror로 받아오는 코드이다.

// 비동기 WebAPI인 XMLHttpRequest를 콜백함수로 받음
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.onload = function () {
  if (xhr.status === 200) {
    console.log("Response:", xhr.responseText);
  } else {
    console.error("Error:", xhr.status);
  }
};
xhr.onerror = function () {
  console.error("Network Error");
};
xhr.send();

 

콜백 지옥

이렇게 시작한 비동기 HTTP 요청은 압도적인 UIUX의 향상을 보여줌으로써, 시간이 지나면서 어디서든 당연하게 쓰이게 되었다. 거의 모든 웹에서 비동기 HTTP 요청을 적용하기 시작하면서 개발자들은 이에 맞춰 작업을 하기 시작했는데, 브라우저가 비동기 요청을 처리 후 전달하는 콜백 함수 방식은 코드량이 많아질수록 작업하기 너무 불편해졌다.

그것이 그 유명한 콜백 지옥인데 브라우저로부터 처리된 데이터를 콜백함수로 받아오고 그 데이터를 기반으로 작업을 이어나가는 코드는 가독성이 떨어지고 디버깅이 매우 어려워졌으며 또한 유지보수가 힘들었다.

 

이렇게 비동기 HTTP 요청의 활성화 + 기존 비동기 처리의 불편함이 상승하면서 JS에서 새로운 기술을 내놓게 된다.

 

 

2. 비동기 처리의 개선: ES6

ES6에서는 멋진 기능들이 많이 등장한 시점으로 특히 Promise 객체가 새로 등장함으로써 단순히 HTTP 요청뿐만이 아니라 모든 비동기 작업에서 코드의 가독성, 유지보수성, 에러 처리의 일관성을 개선시켰다. 또한 비동기 작업의 상태가 명확히 보이고 .then과 같은 체인 메서드를 통해 직관적으로 코드를 읽을 수 있도록 가시성을 크게 향상시켰다.

 

 

* Promise 객체

promise 객체가 어떤 것인지 간단하게 설명을 남겨놓는다. 

 

Promise는 비동기 작업의 완료 또는 실패를 나타내주는 객체로 주어진 작업이 끝난 후 결과 값을 반환하거나 실패 시 에러를 처리할 수 있도록 도와준다. 

 

📌 상태
promise 객체는 pending(대기), fulfiled(이행), rejected(거부) 의 상태값을 갖는다.
pending은 비동기 작업이 아직 완료되지 않음을 나타내고, fulfiled는 작업이 성공적으로 완료되어 결과 값을 반환해주며 rejected는 작업이 실패하며 에러를 반환한다.

📌 주요 메서드
.then() : 상태가 fulfiled일 시 실행하는 메서드
.catch(): 상태가 rejected일 시 실행하는 메서드
.finally(): 성공 여부와 관계없이 작업이 끝나면 실행되는 메서드

 

function fetchData(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);

        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText)); // 성공 시 resolve
            } else {
                reject(new Error(`❌ 요청 실패: ${xhr.status}`)); // 실패 시 reject
            }
        };

        xhr.onerror = function () {
            reject(new Error("❌ 네트워크 오류"));
        };

        xhr.send();
    });
}

// 사용 예제
fetchData("https://jsonplaceholder.typicode.com/posts/1")
    .then(data => console.log("✅ 데이터 성공:", data))
    .catch(error => console.error("🚨 에러 발생:", error));

 

fetch나 요새 흔히쓰이는 것들은 기본적으로 promise가 포함되어있어서 구분해내기 어려웠으므로 XMLHttpRequest 예시로 가져왔다. 

 

1. fetchData 함수의 실행

2. new Promise 함수가 실행되어 내부 함수가 진행됨. xhr.send를 통해 네트워크 요청을 브라우저에게 넘겨 비동기로 작동함

3. 브라우저가 작업을 완료하면 onload에 달려있던 함수를 콜백함수로써 태스크 큐에 등록함

4. JS의 콜 스택이 비면 이벤트 루프를 통해 onload가 실행됨

5. onload 안의 resolve 혹은 reject를 만나고 이와 연결되어있는 .then() 혹은 .catch()를 마이크로태스크큐에 등록함

6. 콜 스택이 비면 then 혹은 catch의 함수가 실행되어 완료

 

위와 같이 then, catch 메서드를 통해 가독성이 뛰어나졌고, 기존의 비동기 처리 방식으로 사용하는 XMLHttpRequest도 promise 객체로 하여금 동일하게 처리할 수 있도록 모양을 갖추게 만들었다. 

 

< 마이크로태스크 큐? >
이벤트 루프는 1. 콜 스택이 모두 실행되고 2. 마이크로태스크 큐가 모두 실행되고 3. 태스크 큐 실행할 만큼 마이크로 태스크 큐는 태스크 큐보다 높은 우선순위의 큐이다. 이러한 마이크로 태스트 큐는 언제 등장한 것을 시간 흐름대로 적어놓았다.

태초의 JS는 웹페이지 내에서 사용자 인터페이스를 조작하기 위한 언어였다. 따라서 JS는 이벤트 중심으로 동작하도록 구성되어있었는데 시간 흐르며 사용자 조작이 더 많아지고 실행되는 이벤트들은 점점 더 많아졌다. 이벤트는 보통 즉시 실행되는 것이 주였지만 가끔 즉시 아닌 나중에 실행해도 되는 이벤트도 필요하게 되었지만 JS는 싱글 스레드였기에 여러 이벤트를 한 번에 처리할 수는 없었다. 따라서 이를 위해 브라우저에 이벤트 루프와 태스크 큐 개념이 도입되었고 이로 인해 UI가 멈추지 않고도 여러 이벤트가 동시에 실행될 수 있는 환경이 구축되었다.

시간이 계속 지나면서 웹페이지는 더 많은 사용자 인터페이스가 요구되고 더 빠른 UI 업데이트를 바라게 된다. 이러한 요구는 HTML5에서 MutationObserver와 같은 API가 나오게 되는 계기가 되었는데 이는 더 즉각적인 UI 업데이트를 위해 나오게 된 터라 기존 태스크 큐에다가 같이 넣으면 목적을 해소할 수 없기에 태스크 큐보다 더 높은 우선순위인 마이크로태스크큐를 넣었다.

즉 태스크 큐는 이벤트들을 동시에 처리하고 UI가 멈추지 않도록 이벤트 실행 순서를 조정하기 위해 등장하였고, 
마이크로는 즉각적으로 실행해야하는 작업을 처리하기 위해 등장하였다.

 

 

3. 사용성 업그레이드: async/await

promise 객체를 사용하는 방법인 then, catch가 여전히 가독성 문제가 있어서 ES8에서 새로 도입된 기능이다. 

비동기를 처리하는 방법인 then과 catch는 분명 콜백 함수의 나열보다는 나았지만 코드가 복잡한 것은 여전했다. 이러한 배경에서 나온 것이 async/await이다. 이는 promise 객체를 그대로 이용하지만 처리하는 방식을 더 동기적으로 보이게끔 개선한 것으로 코드의 가독성을 한층 더 성장시키게 되었다.

 

async 

async 키워드는 함수 앞에 붙여 해당 함수를 비동기 함수로 만든다. async가 붙은 함수는 항상 promise를 반환하게 되며, 반환 값을 자동으로 promise.resolve()로 감싸서 반환한다. 따라서 해당 함수에 .then을 붙여서 그 결과값을 받을 수 있게 된다. 다만 아래에 서술할 await를 통해 완료될 값을 반환받을 수 있기에 async 함수를 반환시켜 사용하는 경우는 그닥 많지 않다. 

async function fetchData() {
    return "데이터 로드 완료";
}

console.log(fetchData()); // Promise {<fulfilled>: "데이터 로드 완료"}

fetchData().then(result => console.log(result)); // "데이터 로드 완료"

 

await

await는 promise가 완료될 때까지 기다렸다가 완료된 값을 반환하도록 하는 키워드로 Promise.then()을 더 쉽게 사용할 수 있도록 도와준다. async 함수 내에서만 사용할 수 있다.

 

 

 

< HTTP 요청 처리의 발전 >
아무래도 HTTP 비동기 요청이 많아지는 세상이 되어 비동기 처리의 방법론이 다양하게 늘었다보니 HTTP 요청이 어떻게 발전했는지에 대해서 슬쩍 보게 되었다. 버리긴 아까워서 기록해둔다.

* form 요청 - 동기적으로 처리함

* XLMHttpRequest - 콜백 지옥의 문제가 일어남

* jQuery.ajax - 제이쿼리!

* fetch - ES6의 일부는 아니지만 promise 도입 시기와 맞물려서 등장. Promise를 이용하여 요청 처리

* axios - 에러처리가 약한 fetch 대신 등장한 라이브러리

* graphQL - REST API의 문제를 보완하고 더 유연한 데이터 요청을 가능하게 한 쿼리 언어

 

 

 


 

3편에 걸쳐 비동기에 대해서 공부하였다. 이번 기회에 비동기에 대해서 깊게 공부하여 앞으로 좀 더 영리하게 코드를 짤 수 있을 것만 같다!