이번에는 Event loop에서 javascript를 특히 비동기 함수들을 어떤 순서로 실행 순서로 실행시키는지 예시 위주로 알아보자.
첫번째로 기본적인 동기 함수와 SetTimeout의 콜백 함수가 어떤 방식으로 실행되는지 보자.
const foo = () => console.log('First');
const bar = () => setTimeout(() => console.log('Second'), 500);
const baz = () => console.log('Third');
bar();
foo();
baz();
정답 : Frist -> Third -> Second
이런 식으로 Event Loop는 실행할 함수를 관리하는 역할로 Call Stack과 Task Queue의 함수를 계속 확인한다. 이렇게 반복되는 매 순회(iteration)를 tick 이라고 부른다.
SetTimeout같은 콜백 함수는 Task Queue로 들어가게 되고 Task Queue에 있는 task들은 Call Stack이 비어 있어야지만 실행된다.
function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
bar();
console.log('foo!'); // (3)
}
function bar() {
delay();
console.log('bar!'); // (2)
}
function baz() {
console.log('baz!'); // (4)
}
setTimeout(baz, 10); // (1)
foo();
출력은 어떤 순서로 될까? delay 함수는 10만의 연산을 해야하므로, 꽤 올래 걸리기 때문에 baz가 가장 먼저 찍힐까? 아니다. setTimout이 Task Queue에 넣은 후, Call Stack이 비어있을 경우 Event Loop가 Task Queue에 있는 baz를 Call Stack으로 넘겨줄 것이기 때문에 baz가 가장 나중에 찍힌다. setTimeout의 두번째 인자인 10 은 10ms 라는 의미를 가진다. 즉, 0.01초다. 그럼에도 불구하고 10ms 보다 더 늦게 실행될 것이다. 즉, 자바스크립트의 타이머는 정확한 타이밍을 보장해주지 않는다.
setTimeout(function () {
// (A)
console.log('A');
}, 0);
Promise.resolve()
.then(function () {
// (B)
console.log('B');
})
.then(function () {
// (C)
console.log('C');
});
Promise도 비동기로 실행되니까 Task Queue에 추가되어 순서대로 A -> B -> C로 찍힐까? 아니다 답은, B -> C -> A다. 이유는 바로 Promise가 MicroTask Queue를 사용하기 때문이다.
MicroTask Queue는 일반 Task Queue보다 더 높은 우선순위를 갖는 태스크다. Task Queue에 대기중인 태스크가 있더라도 MicroTask Queue가 먼저 실행된다. setTimeout은 콜백 A를 Task Queue에 추가하고 Promise의 then() 메서드는 콜백 B를 Task Queue가 아닌 MicroTask Queue에 추가한다. 콜백 B가 실행되고 나면 두번째 then() 메서드가 콜백 C를 MicroTask Queue에 추가한다. Event Loop는 다시 MicroTask Queue를 확인하고, 큐에 있는 콜백 C를 실행한다.
이후에 MicroTask Queuerk 비었음을 확인한 다음 Task Queue에서 콜백 A를 꺼내와 실행한다. 즉, MicroTask Queue에는 Promise가 담기며 Event Loop가 Task Queue 보다 먼저 실행한 후, 다시 then 절이 있는지 확인하고 다시 MicroTask Queue에 집어넣었다.
MicroTask Queue에는 Promise뿐 아니라, Observer API, Node.js의 process.nextTick 등이 그 대상이 된다.
📍 마이크로 태스크 vs 매크로 태스크
마이크로 태스크들은 실행하면서 새로운 마이크로 태스크를 큐에 추가할 수도 있다. 새롭게 추가된 마이크로 태스크도 큐가 빌 때까지 계속해서 실행된다.
반대로, 이벤트 루프는 매크로 태스크 큐에 있는 것을 실행시키기 시작할 때 있는 매크로 태스크만 실행시킨다. 매크로 태스크가 추가한 매크로 태스크는 다음 이벤트 루프가 실행될 때까지 실행되지 않는다.
console.log('Start!');
setTimeout(() => {
console.log('Timeout!');
}, 0);
Promise.resolve('Promise!').then(res => console.log(res));
console.log('End!');
정답 : Start => End => Promise! => TimeOut!
Promise는 Macro Queue, Timeout!은 Micro Queue에 들어가고 Call stack이 빌때까지 기다렸다 Micro Queue부터 실행되기 때문이다.
비동기 함수가 Promise를 반환하는데 await 키워드를 비동기 함수 앞에 붙여주면 비동기 함수가 Promise를 반환할 때까지 코드를 일시 중지 할 수 있다. 다음 코드가 어떻게 실행되는지 살펴보자.
const one = () => Promise.resolve('One!');
async function myFunc() {
console.log('In function!');
const res = await one();
console.log(res);
}
console.log('Before function!');
myFunc();
console.log('After function!');
Before function!이 실행되었고, myFunc 함수 내부의 In function!이 먼저 찍혔다.
이 과정으로 Promise.then 과 async 함수의 차이점을 알 수 있다.
async 함수에서는 await 를 만나면 함수가 중단되고 MicroTask Queue로 들어간다.
Promise는 곧바로 MicroTask Queue에 들어간다.
function a() {
console.log('a1');
b();
console.log('a2');
}
function b() {
console.log('b1');
c();
console.log('b2');
}
async function c() {
console.log('c1');
setTimeout(() => console.log('setTimeout'), 0);
await d();
console.log('c2');
}
function d() {
return new Promise(resolve => {
console.log('d1');
resolve();
console.log('d2');
}).then(() => console.log('then!'));
}
a();
정답
a1
b1
c1
d1
d2
b2
a2
then!
c2
setTimeout
참고
https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/
https://hmk1022.tistory.com/entry/task-queue-micro-task-queue
https://pozafly.github.io/javascript/event-loop-and-async/
https://velog.io/@devstone/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EA%B8%B0%EB%B0%98%EC%9C%BC%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0
너무 좋은 글이네요. 공유해주셔서 감사합니다.