Task Queue는 Web APIs에서 특정한 이벤트가 발생했을때, 우리가 등록한 콜백 함수를 넣어두는 영역이다.
그럼 Microtask Queue는 무엇일까? 🤔
Microtask Queue는 흔히 쓰는 promise에 등록된 콜백함수가 쌓이는 영역이다. 즉 promise가 다 수행이 되고 나면, then에 등록한 콜백함수가 쌓인다
예를 들어, 백엔드에서 데이터를 받아오는 fetch를 이용해서 promise를 만들었다고 가정하자. 그 promise에 then 이라고 콜백함수를 등록해놓으면 이 promise가 잘 끝나서 resolve가 됬을때 등록된 콜백이 Microtask Queue에 들어온다.

(추가로 mutation observer라는 웹 API 에 등록된 콜백도 같이 쌓인다.)
Render은 주기적으로 브라우저에서 요소를 움직이거나 애니메이션 처리를 할때, 주기적으로 화면을 업데이트하는 함수가 담긴다.
Request Animation Frame web api를 통해서 콜백을 등록해두면, 다음에 브라우저가 업데이트 되기 전에 등록한 콜백을 실행시킬 수 있다. (Request Animation Frame queue에 쌓임)

정리하자면,
- Task Queue : 흔하게 쓰는 콜백 함수
- Microtask Queue : Promise에 등록한 콜백 함수
- Render : 브라우저에서 주기적으로 업데이트되기 위한 코드들이 들어가는 영역
- request animation frame이라는 api를 부르면 그 때 등록한 콜백은 Requset Animation Frame 에 하나하나 쌓임
이렇게 브라우저에 많은 Queue 가 있는데 어떻게 함수들을 순서적으로 잘 실행시키는걸까 ? 🤔
Event Loop의 구현상을 보면 while(true) 같은 아이를 이용해서 계속해서 빙글빙글 도는루프 중에 하나이다. (그래서 이름이 이벤트 루프 😂)
이벤트 루프는 계속해서 돌다가, Call Stack에 실행시킬 함수가 있으면 그 Call Stack에 머물러 있는다.

즉, 상기 이미지에서 이벤트 루프는 Call Stack에 있는 function이 끝날때까지 그 자리에 머물러있는다. 그래서 만약 콜 스택에 등록한 함수가 굉장히 시간이 오래걸리는 일이라면, 사용자에게 더 이상 화면이 업데이트 되어져서 보이지가 않는다. 그리고 다른 클릭이 발생해도 그 클릭에 등록된 콜백 함수가 실행되지 않는다. (이벤트 루프는 계속 function에 머물러 있기 때문…)
Call Stack의 function이 끝나게 되면, 다시 이벤트 루프는 빙글빙글 돌기 시작하는데 render 쪽으로는 갈 수도 있고 안 갈 수도 있다.
이게 무슨 말일까??!
브라우저는 업데이트하는 내용들을 사용자에게 60 frames / 1 sec (60 fps)로 보여주도록 노력한다. (사람 눈에 애니메이션이 자연스러워 보이기 위해서 1초당 60개의 그림이 필요함)
✏️ 60 fps = 16.7 ms
단, 이벤트 루프가 한바퀴를 도는데에는 1ms 도 걸리지 않는다고 한다. 매번 1ms 마다 render 업데이트를 할 필요가 전혀 없으므로 브라우저마다 지정된 시간(maybe , 사용자가 자연스럽다고 느낄수있는 시간)마다 render를 업데이트 한다. 즉, 이벤트 루프는 항상 render 을 업데이트하는게 아닌, 브라우저마다의 지정된 값에 따라 render에 들러 업데이트한다.

이렇게 이벤트 루프가 돌다가 Microtask queue에 콜백함수가 있는걸 확인하게 되면, 이벤트 루프는 Microtask queue 에 머무르게되고, 그 queue에서 순서대로 하나씩 Call Stack에 집어넣는다.

Microtask Queue에 머물러 있는 동안 또 다른 콜백함수가 Queue에 들어오면 ?
나중에 들어온 함수도 전부 끝날때까지 마이크로 테스크 큐에서 머무른다. (텅텅 빌때까지)
Microtask Queue 에서는 나중에 들어온 콜백함수가 끝나고, 큐가 텅텅 빌때까지 머무른 반면에, Task Queue는 아이템 하나만 Call Stack에 넣고 다시 순회를 시작한다.

순회를 반복하다 브라우저를 업데이트할 주기가 찾아오면 render 시퀀스에 가서, 먼저 Request Animation Frame을 통해 등록된 콜백함수를 하나하나 다 실행한 후에 Renter Tree로 가서 트리를 만들고, 레이아웃을 계산한 다음 페인트를 통해 브라우저에 업데이트한다.
렌더를 마친 후에 다시 루프를 시작하여 Task Queue의 마지막 click 콜백함수를 Call Stack에다 넣으면 끝! 🙂
그럼 만약 아래 코드처럼 스크립트에서 엘리멘트를 만들고 appendChild()로 추가 한후에 스타일을 지정하는 것이 좋을까 ? 아니면, 모든 스타일을 지정해둔 다음 appendChild()로 마지막에 추가해주는게 좋을까? 🤔
보통의 경우, 모든 스타일을 먼저 지정한 후에 appendChild() 시키는게 맞다고 생각할 수 있지만 그렇지 않다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button>Click to add</button> <script> const button = document.querySelector('button'); button.addEventListener('click',()=>{ const element = document.createElement('h2'); document.body.appendChild(element); element.style.color = 'red'; element.innerText = 'hello'; }); </script> </body> </html>
위 코드에서는 button에 클릭 이벤트를 정의해두었다.
- 웹 APIs 에서 클릭 이벤트 발생
- 이벤트 루프는 이 등록된 콜백을 Task Queue에 넣어줌
- Call Stack 이 비면, 콜백 함수를 Task Queue => Call Stack에 넣어줌
- 이벤트 루프는 등록된 콜백 함수가 모두 완료되어 Call Stack이 빌때까지 기다린다.
- call stack에 등록된 콜백 함수가 모두 처리 되면, 렌더링 영역으로가서 render tree(렌더링)를 만든다.

결과적으로, render tree를 만들때 쯤에는 이미 등록한 모든 것들이 적용된 상태기 때문에 appendChild()를 먼저하던, 스타일 정의를 먼저하던 상관없다.
또 하나의 예로,
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .box{ width: 300px; height: 300px; background-color: brown; } </style> </head> <body> <button>Click</button> <div class="box"></div> <script> const button = document.querySelector('button'); const box = document.querySelector('.box'); button.addEventListener('click',()=>{ box.style.transition = 'transform 1s ease-in'; box.style.transform = 'translateX(800px)'; box.style.transform = 'translateX(500px)'; box.style.backgroundColor = 'blue'; box.style.backgroundColor = 'orange'; box.style.backgroundColor = 'yellow'; }); </script> </body> </html>
상기 코드는 어떻게 동작할까 ?
‘box 가 translateX 방향으로 800px 움직이고, 500px 더 움직이겠다.’ 라고 생각했다면 틀렸다.
Call Stack 내에 들어가있는 콜백 함수에서 transform값 혹은 컬러값을 아무리 업데이트해도 이 코드 블럭이 수행되는 동안에는 브라우저에게 변경된 사항이 보여지지 않는다.
왜냐하면 콜백 함수에서 아무리 transform, color 를 변경해도 이벤트 루프는 Call Stack에 쌓인 콜백 함수가 끝날때까지 기다리므로, Call Stack에 머무는 동안에는 렌더링으로 갈 수 없다.
box.style.transform = 'translateX(500px)'; box.style.backgroundColor = 'yellow';
최종적으로는 마지막으로 정의된 ‘translateX(500px)’ 와 ‘yellow’ 만 render tree로 만들어지고, layout이 발생하고 , Paint를 통해 브라우저에 표기된다.
증명 ✅
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> button { font-size:18px; background:color:white; } button:hover { background-color:antiquewhite; cursor:pointer; } </style> </head> <body> <button>While(True)</button> <script> const button = document.querySelector('button'); button.addEventListener('click',()=>{ while(true){ //repeat ! } }); </script> </body> </html>
위 코드로 button을 클릭해보면 버튼 이후 아무런 변화가 없는걸 확인할 수 있다.
이는 등록한 콜백 함수가 Call Stack 으로가서 영원히 끝나지 않아서 그런 것이다. 콜 스택이 끝나지 않기 때문에 브라우저는 더 이상 클릭을 해도 반응이 없고, 마우스를 올리거나 떨어트려도 반응이 없다.
setTimeout 의 비밀🤭
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> button { font-size:18px; background:color:white; } button:hover { background-color:antiquewhite; cursor:pointer; } </style> </head> <body> <button>Continue with setTimeout</button> <script> function handleClick(){ console.log('handle click'); setTimeout(()=>{ console.log('setTimeout'); handleClick(); },0) } const button = document.querySelector('button'); button.addEventListener('click',()=>{ handleClick(); }); </script> </body> </html>
위의 코드는 콜백함수를 계속해서 호출하고 있다.
Task Queue에 등록해둔 콜백 함수(setTimeout)가 계속 쌓일거라는 걸 이제 알고있다.

버튼을 눌러보면 콘솔에 위 사진과 같이 무한정 출력이 되는걸 볼 수 있다. 이런데도 브라우저와 인터렉션(마우스 오버시 하이라이트 등) 은 여전히 잘된다. 이건 어떻게 가능한걸까 ? 🤔
이벤트 루프는 태스크 큐에서 하나씩만 콜스택에서 가져 오고, 콜 스택에 처리할 일이 없으면, 다시 순회를 시작한다.
위의 코드에서 이벤트 루프는 setTimeout에 등록한 콜백 함수를 콜스택으로 가져가서 처리한 이후에 , 다시 순회를 시작하는데 가끔씩 Render쪽으로 가서 브라우저에 렌더링 처리 및 사용자가 입력한 이벤트까지 처리를 하게 되는 것이다. 어느 정도 처리를 해놓고 Task Queue에 가져가야할 아이템이 있으면 다시 콜 스택에 넣어두고 처리한다. 계속해서 Task Queue 의 작업만 처리하는게 아니라, Render 영역도 주기적으로 들리기때문에 인터렉션이 가능한 것이다.
Promise 의 비밀🤭
setTimeOut 이 위치하던 Task queue 의 task 들은 이벤트 루프가 하나씩만 call stack으로 가져와서 처리하고 순회를 하기 때문에 브라우저 인터렉션이 가능한 것을 확인했다.
Promise가 위치하는 Microtask Queue는 조금 다르다. 앞서 말했듯이, 이벤트 루프는 Microtask Queue 안에 있는 모든 task들이 완료될 때까지 머물러있고, 머물러 있는 동안 새로 들어온 task까지 다 처리한다. (텅텅 빌때까지)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button>Continue with promise</button> <script> function handleClick(){ console.log('handleClick'); Promise.resolve(0) .then(()=>{ console.log('then'); handleClick(); }); } const button = document.querySelector('button'); button.addEventListener('click',()=>{ handleClick(); }); </script> </body> </html>

promise 생성과 동시에 resolve 되며 계속해서 handleClick() 을 호출하게 되는데, setTimeout과 달리 도중에 브라우저와의 인터렉션이 되지 않는다.
- 동작 순서
- 버튼이 클릭 되면, 버튼 클릭 리스너에 등록된 콜백을 Web APIs가 Task Queue에 보내줌
- Task Queue에 들어온 task는 이벤트 루프에 의해 Call Stack으로 옮겨짐
- Call Stack에서 Promise를 만들고 then의 콜백함수를 등록한다
- then의 콜백함수를 Microtask Queue에 넣는다
- 이벤트루프가 돌다가 Microtask Queue에 있는 then의 콜백함수를 Call Stack으로 가져온다.
- Call Stack에서 콜백함수를 수행하다가, 수행하는 도중 또 then을 만남
- Callback으로 또 다른 then의 Callback이 Microtask Queue에 쌓임
- 콜백함수가 끝나기전에 계속해서 새로운 Callback이 Queue에 쌓여서 이벤트 루프가 Microtask queue에 계속 머무르게됨
RequestAnimationFrame 의 비밀🤭
다음은 requestAnimationFrame web api에 대해 알아보자.
이 api는 브라우저에서 다음 렌더링이 발생하기 전에 등록한 콜백 함수가 수행되는 것을 보장해준다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button>RequestAnimationFrame</button> <script> const button = document.querySelector('button'); button.addEventListener('click',()=>{ requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'beige'; }); requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'orange'; }); requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'red'; }); }); </script> </body> </html>
위 코드의 결과는 어떻게 될까?
- 동작 순서
- Web APIs에서 버튼 리스너가 등록되고, 버튼에 이벤트가 발생하면 web APIs 가 Task Queue에 콜백 함수를 등록한다.
- 이벤트 루프가 루프를 돌다가 Task Queue에 있는 아이템을 Call Stack으로 가져온다.
- Call stack에서 등록한 콜백함수가 실행된다.
- requestAnimationFrame(()=>{});
- Call Stack에서는 총 3개의 web api 콜백함수(requestAnimationFrame) 가 호출되는데, 이 Task들은 Render 내의 ‘Request Animation Frame’ 영역에 쌓인다. (베이지 > 오렌지 > 레드 순)
- 코드가 마무리되면 이벤트 루프는 다시 빙글빙글 돌다가 렌더링 주기가 되어 렌더 시퀀스 영역으로 간다.
- Request Animation Frame 내에 쌓여있는 3개의 Task를 FIFO 원리에 맞게 차례로 처리한다.
- 최종적으로 빨간색의 코드가 적용된 다음 render tree > layout > paint 를 진행한다.
결론은 Request Animation Frame 내에 아무리 색상을 많이 바꾸는 콜백함수들이 있다고 하더라도 마지막으로 색상을 바꾼 것만 적용이 된다. (렌더링 되기 전에 콜백함수를 다 처리하기 때문)
당연히 css 속성이 다른 font-color 라던지… font-size 같은 것은 먹힌다.
✏️ requestAnimationFrame
이 웹 api는 상당히 많이 쓰니까 기억해둘것. 클릭 이벤트 리스너가 수행될때는 코드를 변경하지않고, 나중에 브라우저가 화면을 업데이트 하기 전에 등록한 콜백함수 (변경사항)을 적용해 넣는다.
클릭이 발생한 시점에는 코드를 변경하지않고, 렌더링 이전에 코드를 수행하고싶을때 사용할 것
✏️ setTimeout(0) 사용 예제
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button>RequestAnimationFrame</button> <script> const button = document.querySelector('button'); button.addEventListener('click',()=>{ requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'beige'; }); requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'orange'; }); requestAnimationFrame(()=>{ document.body.style.backgroundColor = 'red'; }); setTimeout(()=>{ // do something },0); }); </script> </body> </html>
setTimeout에 원하는 콜백을 등록해두고, 0 ms 있다가 콜백을 실행하게끔 코드를 짜는 경우도 많다.
위 코드를 보면 call stack에서 코드들이 실행되다가, setTimeout(0)를 만나면 웹 APIs는 0ms(즉시) 후에 Task Queue에 Task를 등록한다.
이벤트 루프는 Call Stack이 끝날때까지 기다렸다가 Call Stack에 아무것도 남아 있지 않을때, 다시 한바퀴를 돌면서 Task Queue에 있는 setTimeout 콜백을 Call Stack으로 가져온다.
이것은 콜 스택 안에서 이 코드 블럭이 실행되는 순간 말고, 끝나고 이벤트 루프가 한바퀴 더 돌 때 setTimeout의 콜백 코드 블럭을 실행시키고싶을때 사용한다.
정리 👏
- 콜백 내에서 아무리 DOM 요소를 조작해서 업데이트 한다고 해도 브라우저에는 변경된 사항이 바로 보여지지 않는다. 콜백이 끝난 다음에, 그때서야 브라우저에 업데이트된 사항이 나타난다.
- 콜 스택에 등록하는 함수를 작성할 때 오랫동안 일을 하는 로직을 작성하는 것은 좋지 않다.
- 이벤트 루프가 오랫동안 콜 스택에 머물러있는 동안, 브라우저는 업데이트 처리나 사용자의 클릭처리 , 이벤트 처리가 불가능하므로 최대한 콜백은 간단하게 작성하는 것이 좋다.
- 재귀함수나 끝나지 않는 loop 를 조심해서 사용할 것
- Microtask queue vs Task Queue
- Microtask queue 에는 보통 promise의 콜백함수가 쌓임
- 이벤트 루프는 Microtask queue에 들어있는 모든 콜백 함수 뿐만 아니라, 머물러 있는 동안 새로 추가되는 아이템들까지도 다 실행될때까지 기다렸다가 다시 루프
- Task Queue는 하나씩만 Call Stack으로 가져가서 처리하고 다시 루프