자바스크립트는 싱글스레드 언어로 하나의 stack을 가지고 있으며 한번에 하나의 명령만 처리할 수 있다.
싱글 스레드 언어는 동시성 문제에 대해 걱정할 필요가 없기 때문에 코드를 단순화하지만, 메인 스레드를 차단하지 않고 네트워크 작업을 하는 등 긴 처리작업을 수행하게 되면 작업이 완료될때까지 대기시켜야 한다.
그렇다면, 어떻게 자바스크립트가 여러 작업을 비동기적으로 작업할 수 있을까?
자바스크립트의 메인 스레드는 작업을 다른 곳에 요청해 대신 실행하고, 작업이 완료되면 이벤트나 콜백 함수를 받아 결과를 실행한다. 예를 들자면 파일 다운로드 요청 작업을 백그라운드 작업으로 실행해 동시에 처리가 가능하도록 한 것으로 보면 된다.
이벤트 루프는 브라우저 내부의 Call Stack, Callback Queue, Web APIs등으로 구성이 된다.
이는 자바스크립트 엔진의 일부가 아니라 브라우저의 javascript 런타임 환경 혹은 Node.js의 자바스크립트 런타임 환경이 일부이다.
각 요소들을 모니터링하며 비동기적으로 실행되는 작업들을 관리하고, 순서대로 처리하도록 프로그램의 실행 흐름을 제어해준다. 이 동작 과정을 간단히 살펴보자
자바스크립트의 setTimeout 이나 fetch와 같이 비동기 자바스크립트 코드를 브라우저 WEB APIs에게 맡기고, 백그라운드 작업이 끝난 결과를 콜백 함수 형태로 콜백 큐에 넣고, 처리 준비가 되면 Call stack에 넣어 마무리 작업을 진행한다.
자바스크립트를 실행하는 데는 웹브라우저와 런타임 환경인 Node.js가 있다.
그렇다면 비동기 작업을 어떻게 수행하는지 알기 위해 내부 구성도를 살펴보자.

Call stack
자바스크립트 엔진이 코드 실행을 위해 사용하는 메모리 구조
Heap
동적으로 생성된 자바스크립트 객체가 저장되는공간
WEB APIs
브라우저에서 제공하는 API모음으로, 비동기적으로 실행되는 작업을 전담하여 처리함
Callback Queue
비동기 작업이 완료되면 실행되는 함수들의 대기공간
Event Loop
비동기 함수들을 적절한 시점에 실행시키는 관리자
Event Table
특정 이벤트(timeout, click, mouse)등이 발생했을 때 어떤 콜백함수가 호출되어야 하는지 알고 있는 자료 구조
Web APIs는 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API들을 일컫는다. WEB API는 브라우저에서 멀티 스레드로 구현되어 있다.
그렇기 때무에 브라우저는 비동기 작업에 대해 메인 스레드를 차단하지 않고 다른 스레드를 사용해 동시에 작업을 처리할 수 있게 된다.
예를 들어, setTimeout 비동기 작업은 wEb api의 한 종류이느 ㅅTImer API에서 타이머 스레드를 사용해 타이머를 수행한다.
DOM : HTML 문서의 구조와 내용을 표현하고 조작할 수 있는 객체
XMLHttpRequest: 서버와 비동기적으로 데이터를 교환할 수 있는 객체. AJAX기술의 핵심.
Timer API: 일정한 시간 간격으로 함수를 실행하거나 지연시키는 메소드들을 제공
Console API : 개발자 도구에서 콘솔 기능을 제공
Canvas API: 요소를 통해 그래픽을 그리거나 애니메이션을 만들 수 있는 메소드들을 제공
Geolocation API: 웹 브라우저에서 사용자의 현재 위치 정보를 얻을 수 있는 메소드들을 제공
마이크로태스크 큐 : promise.then, process.nextTick, MutationObser와 같이 우선적으로 비동기로 처리되는 함수들의 콜백함수가 들어가는 큐
매크로 태스크 큐 : setTimeout, setInterval, fetch, addEventListner와 같이 비동기로 처리되는 함수들의 콜백함수가 들어가는 큐
일반적으로 마이크로 태스크 큐가 가장 우선순위가 높아 먼저 콜스택에서 처리되어 비워지고, 이후에 태스크 큐의 콜백을 처리한다.
이벤트 루프가 자바스크립트 엔진과 브라우저의 웹 API를 연결하여 비동기적인 일 처리를 가능케 하기 때문이다
비동기 함수 작업을 WEB api에 옮기는 역할을 하고, 작업이 완료되면 콜백을 큐에 적재했다가, 다시 자바스크립트 엔진에 적재해 수행시키는 일종의 작업을 옮기는 역할만 한다.
function bar() {
setTimeout(() => {
console.log("Second")
}, 500);
}
function foo() {
console.log("First");
}
function baz() {
console.log("Third");
}
bar();
foo();
baz();
이 코드는 setTimeout으로 실행한다.
bar()함수가 호출되면 그 안의 setTimeout 함수가 호출되어 콜스택에 쌓인다.
이후, setTimout() 함수의 매개변수에 할당된 콜백함수를 Timer WEb APId에 전달하.ㄴ 그리고 백그라운드로 500 ms를 설정한다.
다음 foo 함수가 호출되고 콘솔창에 First가 출력된다.
이때 500ms 대기 시간이 만료되어서 이벤트 루프는 Timer Web API에서 가지고 있떤 콜백함수를 Task Queue로 옮긴다
그다음 baz()함수가 호출되고 콘솔창에 ㅅThirdrk 출력된다.
스택에 있는 모든 메인 자바스크립트 코드가 샐힝 완료되어 콜스백이 비워진다.
이벤트 루프는 콜스택이 비어있는 경우 탐지하여, 태스크 큐에 있는 콜백함수를 콜스택으로 옮긴다.
콜스택에서 콜백함수 코드를 실행하게 되고 콘솔창에 Second가 출력된다.
##웹브라우저와 Node.js의 Web API 차이
웹브라우저의 Web APIs 와 Node.js 의 Node.js APIs 들은 구성은 비슷하지만 동작 측면에서 약간 차이가 있다. 웹브라우저의 Web APIs는 비동기 작업이 끝나면 스스로 callback queue에 적재하지만, Node.js API들은 이벤트 루프가 직접 옮겨준다. 예를들어 Timer Web API에서 타이머가 모두 지나가면, 자바스크립트 환경이 웹브라우저냐 Node.js 냐에 따라 차이가 갈린다.
Node.js : Timer API가 타이머 완료 이벤트를 발생시키고, 이벤트 루프가 이를 감지하여 Task Queue에 콜백 함수를 추가한다.
웹브라우저 : Timer API가 스스로 Task Queue에 콜백 함수를 추가한다.
콜백큐는 Web API가 수행한 비동기 함수를 넘겨받아 event loop가 해당 함수를 콜스택에 넘겨줄때까지 비동기 함수들을 쌓아놓는 곳이다.
그중 Promise객체의 콜백이 쌓이는 곳이 MicroTask Queue 이다. 이 는 그 어떤곳보다 가장 우선으로 콜백이 처리된다.(심지어 브라우저 렌더링 전보다 말이다
실제 예시 코드에서 setTimeout 가 Promise 객체의 콜백이 동시에 주어졌을때 어떻게 처리되는지 확인해보자. 코드 내용을 살펴보면 먼저 setTimeout 을 통해서 0초동안 대기 하였다가 "Timeout!" 을 출력하는 콜백 함수를 실행한다. 그다음 Promise 객체에 의해 "Promise!" 라는 텍스트를 출력하는 then 핸들러의 콜백 함수를 실행한다. 이 코드를 실행해보면 아래와 같이 동작 애니메이션이 발생한다.
console.log('Start!');
setTimeout(() => {
console.log('Timeout!');
}, 0);
Promise.resolve('Promise!').then(res => console.log(res));
console.log('End!');
Copy
console.log start가 출력된다.
setTimeout코드가 콜스택에 적재되고 실행되면, 그 안의 콜백함수가 이벤트 루프에 의해 Web API로 옮겨지고 타이머가 작동한다.
타이머가 종료됨에 따라 setTimeout의 콜백함수는 MacroTask Queue에 이벤트 루프에 의해 적재된다.