자바스크립트 동기와 비동기 6 - 이벤트 루프(Event Loop), 태스크큐(Task Queue)와 마이크로태스크큐(Microtask Queue)

RN·2024년 7월 2일

자바스크립트

목록 보기
7/11
post-thumbnail

1. 태스크 큐? 마이크로태스크 큐?


뜬금 없이 힙과 호출스택을 하다가 태스크큐와 마이크로태스크큐를 공부하는 것 같다.

하지만 이전 포스트에서 사용한 사진에서 보면

웹을 실행시키기 위해선 메모리, 힙을 제외하고도 콜백 큐(Callback Queue)도 필요하다.

찾아보니 콜백 큐라는 단어에 대해선 말이 나뉘었다.

  1. 콜백 큐에 태스크 큐와 마이크로태스크 큐가 들어있다.
  2. 콜백 큐는 태스크 큐의 다른 이름이다.

2번은 해외 레퍼런스에서 여러번 나왔다.

그래서 나는 2번이라고 생각하고 이야기 하겠다.
(사실 포스트 작성하고 보니 더 이상 콜백 큐라고 지칭할 일이 없더라...)


그럼 태스크 큐와 마이크로태스크 큐가 하는 일은 무엇일까?




1.1 마이크로태스크 큐(Microtask Queue)


마이크로태스크 큐는 어떤 일을 할까?

마이크로태스크 큐는 Promise와 MutationObserver의 콜백 함수를 담는다.

MutationObserver?

MutationObserver는 DOM의 속성, 텍스트, 자식 노드들에 대한 변경을 감지할 수 있는 API이며, 특정 노드 객체를 관찰하고, 변경이 발생했을 때 콜백 함수를 실행한다.

하지만 여기서 MutationObserver 는 중요하지 않다.

마이크로태스크 큐가 어떤 일을 하는지는 코드로 확인해보자.

이제 위의 코드 정도는 눈을 감고도 해석할 수 있다.

위의 코드가 어떤 순서로 실행되는지 한 번 생각해보자. 답은 아래에 있다.






Velog가 접기 펼치기 기능이 없어 공백으로 채운다...









답은...?

내가 처음 Promise에 대해 배웠을 때는 나는 1 -> 나는 3 -> 나는 2 -> 프로미스 Resolve 순서로 실행될 것이라고 생각했지만 아니었다;;

그럼 저 코드가 마이크로태스크 큐와 호출스택에서 어떻게 담기는지 확인해보자.

  1. console.log("나는 1") 이 먼저 실행되며 호출스택에 쌓인다.

  1. console.log("나는 1") 의 실행이 종료되어 스택에서 빠지며 promise()를 실행한다. 그리고 promise()를 스택에 쌓는다.

  1. promise() 가 Promise 객체를 반환하며 Promise 객체의 콜백 함수를 실행한다.

  1. console.log("나는 2")가 종료되고 resolve()를 실행한다.

  1. resolve()를 실행하며 그 후 promise().then() 의 .then() 을 실행한다.

  1. 그런데 .then()에 전달한 콜백함수가 호출 스택에서 마이크로태스크 큐에 옮겨진다.
    이 .then()의 콜백함수는 이벤트 루프에 의해 호출스택이 비어있을 때까지 큐에서 대기한다.
    이벤트 루프에 관해선 목차 1.2 에서 설명...

  1. .then()의 콜백이 마이크로태스크 큐에 담기는 동시에 console.log("나는 3")이 실행되며 호출 스택에 담긴다.("나는 3" 인데 "나는 2" 라고 오타가 났다...)

  1. 더 이상 호출 스택에 아무것도 담기지 않자 이벤트 루프에서 마이크로태스크 큐의 .then()의 콜백함수를 실행한다.

  1. .then()의 콜백 함수인 console.log(반환값)을 호출 스택에 담으며 실행한다.

  1. 이렇게 모두 실행하고 늦게온 순서대로 스택에서 빠져나가며 종료된다.

다시 한 번 마이크로태스크 큐에 대해서 말해보면

마이크로태스크 큐는 Promise와 MutationObserver의 콜백 함수를 담는다.

여기서 말하는 콜백 함수는 new Promise((res, rej)=>{...}) 의 콜백 함수가 아니라.

.then(()=>{}) 의 콜백 함수를 말하는 것이었다...




1.2 이벤트 루프(Event Loop)


이벤트 루프란 태스크 큐와 마이크로태스크 큐, 호출 스택을 계속해서 감시하며,
호출 스택이 비어있을 때 태스크 큐와 마이크로태스크 큐에서 콜백함수를 하나씩 꺼내어 호출 스택에 넣어준다.

위에서 큐에 대해서 공부하고 이벤트 루프를 보니 이제 이해가 가는 것 같다.

아래의 태스크 큐도 같이 공부해보면서 확인해보자.




1.3 태스크 큐(Task Queue)


음... 보기엔 마이크로태스크 큐 혼자만 있어도 충분할 것 같은데 태스크 큐는 다른 역할을 하나?

태스크 큐는 setTimeout(), DOM 이벤트 또는 Fetch 요청과 같은 비동기 작업을 담는다.

위의 API들은 전부 WebAPIs 들이다. 이 WebAPIs 는 단일 스레드로 동작하는 자바스크립트에서 멀티 스레드처럼 동작해야 하는 것들이다.


예를 들어 setTimeout(()=>{}, 2000) 라고 작성하면 setTimeout에 의해 생성되는 타이머 객체가 다른 호출 스택에 쌓여서 2초를 보내야 하기 때문이다.

그런데 그러지 않고 만약 단일 스레드로만 동작하면

  1. setTimeout 실행.
  2. setTimeout이 태스크 큐에 적재.
  3. 다른 명령어들 전부 수행하여 호출 스택 비어 있음.
  4. setTimeout이 스택에 쌓임.
  5. 겨우 스택에 쌓이고 나서야 2초를 보냄.
  6. 그 후 setTimeout에 전달한 콜백 함수를 실행.

이런 식으로 실행되기 때문에 다른 곳에서 2초를 미리 보내놓고 호출 스택에 담기자마자 실행하는 효과를 얻을 수가 없다.

이 setTimeout가 수행하는 멀티스레드 역할을 웹 브라우저에서 대신 해준다.



갑자기 태스크 큐 얘기 하다가 말고 무슨 멍소리하냐... 라고 할 수 있지만

이러한 배경을 가지고 코드가 어떻게 실행되어 태스크 큐에 적재되는지 확인해보자.

우선 이 코드가 어떤 순서로 출력될지 예상해보자.

참고로 첫 번째 Promise는 setTimeout으로 resolve를 감쌌지만 0초를 줬기 때문에 딜레이는 없다.

그리고 for문을 사용한 이유는 promise1() 와 promise2() 의 실행 사이에 적당한 텀을 주기 위해서다.

어떤 순서로 실행될까?








접기/펼치기 기능 만들어주세요...






정답은

처음 공부할 때 내가 예상했던 것은 나는 1 -> 나는 3 -> 나는 2 -> 프로미스2 -> 프로미스1 순서일 것이라 생각했다.

하지만 아니었다.

우선 왜 그런지 미리 얘기하자면 태스크 큐는 마이크로태스크 큐보다 우선순위가 떨어진다.

이것을 가지고 어떻게 태스크 큐와 마이크로 태스크 큐에 적재되는지 그림으로 보자.

  1. 먼저 console.log("나는 1")이 실행된다.

  1. 그 후 promise1() 이 실행된다.

  1. promise()가 Promise 객체를 반환되며 setTimeout() 이 실행된다.

  1. setTimeout 에 전달된 시간 인자 만큼 브라우저나 node.js 등 런타임 환경에서 흐르도록 하여 멀티스레딩으로 실행하도록 한다.

    그렇게 setTimeout이 실행되는 동안 아래의 for문을 실행한다.

  1. setTimeout에 전달된 시간이 0초 였기때문에 바로 태스크 큐에 담긴다.

    그 후 for문이 전부 돌며 for문이 종료. promise2()를 실행한다.

  1. promise() 가 반환한 resolve()를 실행한다.

  1. resolve()를 실행하여 .then()이 실행된다.

  1. .then()이 마이크로태스크 큐에 담기며 console.log("나는 3")이 실행된다.

  1. 더 이상 호출 스택에 담기지 않아 우선순위가 더 높은 마이크로태스크 큐에서 먼저 꺼내어 실행한다.

  1. 그 후 태스크 큐에서 setTimeout을 꺼내어 실행한다.

  1. 나는 2를 출력한다.

  1. resolve()를 실행한다.

  1. .then()을 실행한다.

  1. .then()이 마이크로태스크 큐에 담긴다.

  1. 호출 스택에 아무것도 담기지 않아 .then()이 실행되고 프로미스1 resolve가 출력되며 종료된다.



1.4 async/await은 어떤 방식으로 마이크로태스크큐에 담길까?


해당 포스트에서 작성하려고 했으나 다음 포스트인 틀린 코드 고치기의 1.1 에서 틀린 코드가 async/await을 사용하고 있으므로 거기서 같이 알아보자.




1.5 마이크로태스크 큐 vs 태스크 큐


태스크 큐마이크로태스크 큐
무엇이 담기나DOM 이벤트, setTimeout, fetch 등 WebAPIsPromise와 MutationObserver의 콜백 함수
우선순위낮음높음
얼마나비우는 도중 마이크로태스크 큐에 함수가 담긴다면 멈춤비우기 시작하면 전부 비울때까지 비움.

0개의 댓글