Skip to content

Latest commit

 

History

History
121 lines (86 loc) · 6.63 KB

16. Asynchronous JavaScript.md

File metadata and controls

121 lines (86 loc) · 6.63 KB

Asynchronous JavaScript

자바스크립트는 싱글 스레드 언어이다

자바스크립트는 싱글 스레드(single thread) 언어이다. 자바스크립트 엔진은 하나의 실행 컨텍스트 스택을 가지고 한 번에 하나의 실행 컨텍스트(스택의 top에 있는 실행 컨텍스트)만 실행한다. 즉, 자바스크립트 엔진은 하나의 콜 스택을 사용하여 한 번에 하나의 작업만을 처리한다. 그런데 브라우저는 화면에 애니메이션을 보여주는 한편 사용자의 마우스 클릭에 따라 모달을 보여주기도 하고, Node.js 웹 서버는 여러 개의 HTTP 요청을 동시에 보내고 받는다. 어떻게 자바스크립트로 비동기 작업을 할 수 있는 걸까? 그것은 자바스크립트 런타임이 멀티 스레드 환경을 제공하고 있기 때문이다. 브라우저를 중심으로 살펴보자.

브라우저는 멀티 스레드이다

자바스크립트 엔진은 소스코드의 평가와 실행 결과를 기록하는 **콜 스택(call stack)**과 객체가 저장되는 **힙(heap)**으로 구성된다. 자바스크립트 엔진은 소스코드의 평가와 실행만 하지 비동기 작업을 처리할 수 있는 능력이 없다. 비동기 처리는 자바스크립트 엔진을 임베디드한 런타임이 제공한다.

브라우저는 자바스크립트 코드를 평가하고 실행할 자바스크립트 엔진, 그 외의 비동기 처리를 담당할 이벤트 루프를 가진다. **이벤트 루프(event loop)**는 콜백 큐를 가지며, 콜백 큐는 태스크 큐와 마이크로태스크 큐로 이루어진다. **태스크 큐(task queue/event queue/callback queue)**는 비동기 함수의 콜백과 이벤트 핸들러를 일시적으로 보관한다. **마이크로태스크 큐(microtask queue/job queue)**는 프로미스 후속 처리 메서드의 콜백 함수를 일시적으로 보관한다.

이벤트 루프는 기본적으로 다음 동작을 반복한다.

  1. 콜 스택을 확인한다.
  2. 콜 스택이 비어있다면 큐에서 대기중인 첫번째 함수를 콜 스택으로 이동시킨다. 먼저 마이크로태스크 큐를 비운 후 태스크 큐의 함수를 이동시킨다. (렌더링은 마이크로태스크 큐가 빈 후 실행된다. ??)

별개로 자바스크립트 엔진은 실행 컨텍스트 스택의 top에 있는 실행 컨텍스트를 계속 실행한다. 어쨌든 자바스크립트의 실행 자체는 스택 하나를 사용하므로 엔진이 블로킹되면 모든 코드의 실행도 블로킹된다.

한편 브라우저는 ECMAScript 명세에 정의되어있지 않은 Web API를 제공하는데 이 중 비동기 API(DOM, ajax, setTimeout)는 각각의 API를 처리하는 스레드로 구성된 멀티 스레드에서 실행된 후 콜백이 태스크 큐에 푸시된다. 예를 들어 XMLHttpRequest로 ajax 요청하면 네트워크 요청을 하고 응답을 기다리는 곳은 연관한 스레드다.

function hello() { console.log('HELLO WORL!')}
setTimeout(hello, 1000);

위 예시를 살펴보자. setTimeout이 콜 스택에서 실행되면 타이머는 연관한 스레드에서 작동하고 1초가 지나면 태스크 큐에 hello()가 푸시되는 것이다. 콜 스택이 비면, 이벤트 루프는 태스크 큐로부터 hello()를 가져다 콜 스택에 푸시하고 최종적으로 hello()가 실행되고 콘솔에 HELLO WORLD가 출력될 것이다.

자바스크립트에서 비동기 처리하기

자바스크립트에서 비동기식 처리를 위한 두 가지 패턴이 있다.

  1. 콜백 패턴(전통적인 방식)
  2. 프로미스 패턴(ES6), 프로미스 기반의 async/await(ES8)

콜백 패턴

콜백 패턴은 콜백 함수를 사용하여 비동기식 처리를 하는 것을 뜻한다. XMLHttpRequest를 사용하여 네트워크 요청하고 응답을 처리해보자.

const get = url => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.send();

    xhr.onload = () => {
        if (xhr.status === 200) console.log(JSON.parse(xhr.response));
        else console.error(`${xhr.status}: ${xhr.statusText}`);
    };
}

이때 네트워크 응답을 가지고 어떤 일을 하고 싶지만, xhr.onload에 등록하는 콜백 함수 외부에서는 불가능하다. 콜백 함수는 값을 반환해도 의미가 없고, 상위 스코프의 변수에 값을 할당해도 그 값을 이용하는 시점에 네트워크 응답이 도착해서 콜백이 실행되고 할당이 완료되었으리라는 보장도 없다.

let res;

xhr.onload = () => {
    if (xhr.status === 200) res = JSON.parse(xhr.response);
    else console.error(`${xhr.status}: ${xhr.statusText}`);
};

console.log(res);	// undefined

따라서 응답에 대한 모든 처리는 xhr.onload에 할당하는 콜백 내부에서 이루어져야한다.

xhr.onload = () => {
    if (xhr.status === 200) {
        let res = JSON.parse(xhr.response);
        console.log(res);
    }
    else console.error(`${xhr.status}: ${xhr.statusText}`);
};

이를 쉽게 하기 위해 보통은 함수의 콜백으로 넘겨준다.

const get = (url, successCallback, failureCallback) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.send();

    xhr.onload = () => {
        if (xhr.status === 200) successCallback(JSON.parse(xhr.response));
        else failureCallback(xhr.status);
    };
}

그런데 get의 응답으로 또다른 get을 처리해야한다면, 다음과 같은 콜백 헬(callback hell)이 형성될 수도 있다.

get(`${url}`, result1 => {
    get(`${url}/${result1}`, result2 => {
       get(`${url}/${result2}`, result3 => {
           // 생략...
       }) 
    });
});

프로미스 패턴

프로미스 패턴은 ES6 Promise 표준 빌트인 객체를 바탕으로 한다. 프로미스async와 await를 참고한다.

참고