이벤트 루프와 Promise

Jinux·2022년 8월 30일
2
post-thumbnail

아래 코드가 어떻게 동작할지 예측이 가능하고 설명이 가능하다면 뒤로 가기를 눌러주세요!

const prom1 = () => {
  return new Promise((res, rej) => {
    console.log('prom1');
    res();
  });
};
const prom2 = () => {
  return new Promise((res, rej) => {
    console.log('prom2');
    setTimeout(() => {
      console.log('promise setTimeout');
      res();
    }, 0);
  });
};
const prom3 = () => {
  return new Promise((res, rej) => {
    console.log('prom3');
    rej();
  });
};

console.log('before promise');
setTimeout(() => console.log('setTimeout before promise'), 0);
prom1().then(() => console.log('prom1 -> then'));
prom2().then(() => console.log('prom2 -> setTimeout -> then'));
prom3()
  .then(() => console.log('prom3 -> then'))
  .catch(() => console.log('prom3 -> catch'));
console.log('after promise');

위 코드의 실행 순서를 살펴보면 Promise의 실행 함수가 호출되는 시점에 바로 실행이 됩니다. 하지만 실행 함수에서 resolve, reject 콜백함수는 바로 처리되지 않고 콜백 큐로 이동되어 after promise까지 출력이 완료된 이후에 처리가 되는 것을 볼 수 있습니다.

또, prom1 보다 앞서 호출된 setTimeout 함수의 콜백과 prom2의 콜백 실행 순서는 setTimeout before 후에 마지막으로 prom2 의 setTimeout 함수가 실행됩니다.

왜 그렇게 동작할까요?

0의 지연시간

먼저 setTimeout의 지연 시간 0초가 수상합니다. 0초이면 바로 실행이 되어야 할텐데 바로 동작하지 않습니다. 왜 그럴까요?

콜백 함수의 실행 시점은 콜스택과 태스크 큐에서 대기 중인 작업의 수에 따라 다릅니다.

(function() {

  console.log('시작');

  setTimeout(function cb() {
    console.log('콜백 1: 콜백 메시지');
  }); // has a default time value of 0

  console.log('평범한 메시지');

  setTimeout(function cb1() {
    console.log('콜백 2: 콜백 메시지');
  }, 0);

  console.log('종료');

})();

// "시작"
// "평범한 메시지"
// "종료"
// "콜백 1: 콜백 메시지"
// "콜백 2: 콜백 메시지"

지연시간은 요청을 처리하기전에 대기할 '최소'시간이고, 보장 시간이 아니기 때문입니다. 그래도 0초이면 바로 실행을 해야하는 것이 아닐까요? 왜 이렇게 동작할까요.

콜스택

자바스크립트는 함수를 호출하면 콜스택이라는 곳에 추가됩니다. 콜스택은 말그대로 스택으로 LIFO 구조를 가지는데요. 함수가 값을 반환하면 스택으로부터 튀어나오며 처리되는 순서입니다.

태스크 큐

Web API에 의해 제공되는 setTimout 함수는 전달한 콜백함수를 일정시간 지연된 후 실행됩니다. 이 콜백함수는 콜스택에 들어가 바로 실행되어야 할 것 같지만 그렇지 않습니다. 태스크 큐라는 곳에 순차적으로 들어가 '대기'하게 되죠.

언제까지 대기하냐면 콜스택이 전부 비워질때까지 대기하게 됩니다. 이제 콜스택이 다 비워졌다면 (이전에 호출된 모든 함수가 값을 반환하고 모두 스택에서 튀어나온 상태라면) 이벤트 루프는 태스크 큐에서 호출 스택에 추가합니다. 호출 스택에 들어간 콜백 함수는 값을 반환하고 튀어나가게 되며 끝이 나는 것이죠.

이 태스크 큐에 들어가는 대표적인 태스크 들은 setTimeout 외에도

  • 외부 스크립트
  • 사용자가 마우스를 움직일 때 mousemove 이벤트와 이벤트 핸들러를 실행하는 것
  • 기타 등등

이 있습니다.

이벤트 루프

이벤트 루프는 태스크가 들어오길 기다렸다가 처리하고, 처리할 태스크가 없는 경우엔 잠드는, 끊임없이 돌아가는 자바스크립트 내 루프입니다.

while (queue.waitForMessage()) {
   queue.processNextMessage()
 }

MDN의 설명을 보면 간단한 코드로 설명하고 있는데요.

queue.waitForMessage() 함수는 현재 처리할 수 있는 메시지가 존재하지 않으면 새로운 메시지가 도착할 때까지 동기적으로 대기합니다.

즉, 현재 콜스택에서 실행할 작업이 없는지, 태스크 큐에 있는지 반복적으로 확인하는 것 입니다.

정리하자면 이벤트 루프는 현재 실행중인 작업이 없을 때(콜스택에서) 태스크 큐의 첫 번째 태스크 큐를 꺼내와 콜스택에 넣어주는 역할을 하는 것 입니다.

마이크로태스크

앞서 알아본 태스큐 큐가 콜백함수를 받아 콜스택이 비워진 후에 실행되는 것을 알아보았습니다.

const prom1 = () => {
  return new Promise((res, rej) => {
    console.log('prom1');
    res();
  });
};
const prom2 = () => {
  return new Promise((res, rej) => {
    console.log('prom2');
    setTimeout(() => {
      console.log('promise setTimeout');
      res();
    }, 0);
  });
};
const prom3 = () => {
  return new Promise((res, rej) => {
    console.log('prom3');
    rej();
  });
};

console.log('before promise');
setTimeout(() => console.log('setTimeout before promise'), 0);
prom1().then(() => console.log('prom1 -> then'));
prom2().then(() => console.log('prom2 -> setTimeout -> then'));
prom3()
  .then(() => console.log('prom3 -> then'))
  .catch(() => console.log('prom3 -> catch'));
console.log('after promise');

다시 위 코드를 살펴보면 promise로 호출한 코드는 태스크 큐에 담기는 setTimeout 콜백보다 '먼저' 호출되는 것을 알 수 있습니다.

그 이유는 Promise는 마이크로 태스크 큐에 담기기 때문인데요. ES6를 지원하는 자바스크립트 런타임에는 마이크로 태스크 큐가 추가된 후 가장 우선적으로 처리되기 때문입니다.

자바스크립트 엔진은 태스크 큐 하나를 처리할 때마다 우선적으로 마이크로 태스크 큐에 쌓인 마이크로 태스크 전부를 처리합니다.

참고
https://devlog.changhee.me/posts/Promise%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84/
https://meetup.toast.com/posts/89
https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop
https://ko.javascript.info/event-loop
https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif
(번역: https://chanmi-lee.github.io/articles/2020-06/JavaScript-Visualized-Event-Loop)

0개의 댓글