1. 소개
* 소개
- 지난 장에서 봤듯이 비동기 작업은 태스크큐에서 대기하다가 콜스택으로 넘어가서 실행된다.
- 그렇다면 여러개의 비동기 작업이 들어가면 어떻게 될지 알아보자.
- 아래에서 사용되는 이미지와 코드는 이전 포스트의 연장선이므로 이전 포스트를 보지 않았다면 보는 것을 권장한다.
2. 실행
* 태스크 큐
- 먼저 동일한 비동기 동작이 여러 개가 있을 경우를 살펴보자.
function myFunc1() {
Promise.resolve().then(() => {
console.log("promise1");
});
myFunc2();
Promise.resolve().then(() => {
console.log("promise3");
});
console.log("bye");
}
function myFunc2() {
Promise.resolve().then(() => {
console.log("promise2");
});
console.log("hello");
}
myFunc1();
- 지난 장에서 봤던 코드에 즉시 실행되는 promise를 3개 삽입해두었다. 모든 비동기 API는 작업이 완료되면 콜백 함수가 태스크 큐에 추가된다. 즉, "hello" -> "bye" 까지는 예측이 가능한데, 태스크 큐에 추가된 각 promise는 어떤 순서일까.
- 태스크 큐라는 이름에서 예상가능하듯이 선입선출로 실행된다. "hello" -> "bye" -> "promise1" -> "promise2" -> "promise3"
- 이벤트 루프에서 이들이 어떻게 동작하고 있는지 확인해보자.
- 콜백함수가 태스크 큐에 순서대로 추가되고, 콜스택이 모두 비워지면 먼저 들어왔던 "promise1" 이 콜스택으로 들어간다.
- 이 때, 마찬가지로 콜스택이 비워지면 태스크 큐에 먼저 들어왔던 "promise2" 가 콜스택으로 들어간다.
- 이번엔 타이머 함수를 살펴보자.
- 만일 1초후실행되는 함수와 0초후 실행되는 함수가 순서대로 들어간다면 이벤트 루프는 이를 어떻게 처리할까?
function myFunc1() {
setTimeout(() => {
console.log("time 1sec");
}, 1000);
myFunc2();
setTimeout(() => {
console.log("time 0sec");
}, 0);
console.log("bye");
}
function myFunc2() {
console.log("hello");
}
myFunc1();
- 위의 코드가 실행된다면 "time 1sec"이 먼저 들어갔으니 "time 1sec" 이 찍히고 0초를 센 후 "time 0sec" 이 실행될까? 결론부터 말하자면 그렇지 않다. 사실 이렇게 동작하면 동시성이 있는 작업이라고 할 수도 없을 것이다.
- 위의 결과는 "hello" -> "bye" -> "time 0sec" -> "time 1sec" 이다. 당연히 이렇게 되어야한다고 생각은하지만 어떻게 이벤트 루프가 처리되는 것인지 모를 수도있다. 아래의 이미지를 확인하자.
- 이 때 동작하는 것이 Background, 즉 웹API 또는 노드API이다. 브라우저나 OS단에서 계산을 하게된다.
- 그동안 "time0" 은 계산이 끝나 태스크큐로 먼저 넘어간다. 이후로는 이미 알다시피 선입선출의 순서로 처리되는 것이다.
* 마이크로 태스크 큐와 매크로 태스크 큐
- 이제 마지막으로 프라미스와 타이머 함수가 들어간 코드를 살펴보자.
function myFunc1() {
setTimeout(() => {
console.log("time");
}, 0);
myFunc2();
Promise.resolve().then(() => {
console.log("promise");
});
console.log("bye");
}
function myFunc2() {
console.log("hello");
}
myFunc1();
- 위 코드의 순서를 예측해보라. 지금까지 봤던 개념대로 예상해보면 "time" 후에 "promise"가 찍힐 것처럼 느껴진다.
- 하지만 놀랍게도 순서는 "hello" -> "bye" -> "promise" -> "time" 이다. 대체 왜 그럴까. 백그라운드가 사실 0초를 0.1초라고 계산이라도 하는 것일까.
- 이는 사실 태스크 큐가 우선순위를 갖고 있기 때문이다. 위의 그림까지는 태스크큐를 하나로 두었지만 실제로는 마이크로 태스크(micro task)와 매크로 태스크(macro task)를 분리하여 처리한다. 참고로 매크로 태스크는 일반 태스크로 불리기도 한다.
- 마이크로 태스크 큐는 매크로 태스크 큐에 비해 우선순위가 높아서, 콜 스택이 비워지면 마이크로 태스크 큐를 먼저 확인하게 된다. 모든 마이크로 태스크 큐가 비워지면 매크로 태스크 큐를 실행한다. 이러한 과정을 위 코드와 아래 이미지를 통해 확인해보자.
- 타이머 콜백은 매크로태스크큐에, 프라미스 콜백은 마이크로 태스크큐에 들어갔음을 확인할 수 있다. 콜스택이 비워지자 마이크로태스크 큐에서 작업을 먼저 꺼낸다.
* 분류
- 콜백 함수를 매크로 태스크 큐로 넣는 함수들은 다음과 같다.
∙ setTimeout
∙ setInterval
∙ setImmediate
∙ I/O
∙ UI 렌더링
- 콜백 함수를 마이크로 태스크 큐로 넣는 함수들은 다음과 같다
∙ Promise
∙ process.nextTick
∙ Object.observe
∙ MutationObserver
- 참고로 브라우저의 requestAnimationFrame 와 같은 애니메이션의 경우 Animation Callback Queue에서 담당한다. 위와는 별개로 픽셀의 리페인트 이전에 동작하며, 애니메이션 작업중에 들어온 추가적인 작업들은 다음 프레임으로 미루는 방식이다.
참고
'Language > JavaScript' 카테고리의 다른 글
<ES10> ES2019 Features (0) | 2021.10.11 |
---|---|
<ES9> ES2018 Features (0) | 2021.10.10 |
<이벤트루프> JS와 이벤트루프 (2) | 2021.10.03 |
<ES8> ES2017 Features (0) | 2021.09.26 |
<ES7> ES2016(ES7) Features (0) | 2021.08.07 |