1.5.3 태스크 큐와 마이크로 태스크 큐
이벤트 루프는 하나의 마이크로 태스크 큐를 갖고 있는데, 기존의 태스크 큐와는 다른 태스크를 처리함. 여기에 들어가는 마이크로 태스크에는 대표적으로 Promise
가 있음. 이 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 가짐. 즉, setTimeout
과 setInterval
은 Promise
보다 늦게 실행됨. 명세에 따르면, 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미뤄짐.
function foo() {
console.log('foo')
}
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
setTimeout(foo, 0)
Promise.resolve().then(bar).then(baz)
위 코드를 실행하면 bar
, baz
, foo
순으로 실행됨. 확실히 Promise
가 우선권이 있음.
각 태스크에 들어가는 대표적인 작업은 다음과 같음.
- 태스크 큐:
setTimeout
,setInterval
,setImmediate
- 마이크로 태스크 큐:
process.nextTick
,Promises
,queueMicroTask
,MutationObserver
그렇다면 렌더링은 언제 실행됨? 태스크? 마이크로 태스크 큐? 태스크 큐를 실행하기에 앞서 먼저 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐를 실행한 뒤 렌더링이 일어남. 각 마이크로 태스크 큐 작업이 끝날 때마다 한 번씩 렌더링할 기회를 얻게 됨.
<html>
<body>
<ul>
<li>동기 코드: <button id="sync">0</button></li>
<li>태스크: <button id="macrotask">0</button></li>
<li>마이크로 태스크: <button id="microtask">0</button></li>
</ul>
<button id="macro_micro">모두 동시 실행</button>
</body>
<script>
const button = document.getElementById('run')
const sync = document.getElementById('sync')
const macrotask = document.getElementById('macrotask')
const microtask = document.getElementById('microtask')
const macro_micro = document.getElementById('macro_micro')
// 동기 코드를 버튼에 1부터 렌더링
sync.addEventListener('click', function() {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i
}
})
// setTimeout으로 태스크 큐에 작업을 넣어서 1부터 렌더링
macrotask.addEventListener('click', function() {
for (let i = 0; i <= 100000; i++) {
setTimeout(() => {
macrotask.innerHTML = i
}, 0)
}
})
// queueMicrotask로 마이크로 태스크 큐에 넣어서 1부터 렌더링
microtask.addEventListener('click', function() {
for (let i = 0; i <= 100000; i++) {
queueMicrotask(() => {
microtask.innerHTML = i
}, 0)
}
})
macro_micro.addEventListener('click', function() {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i
setTimeout(() => {
macrotask.innerHTML = i
}, 0)
queueMicrotask(() => {
microtask.innerHTML = i
}, 0)
}
})
</script>
</html>
위 코드의 결과를 정리하면 다음과 같음.
- 동기 코드는 해당 연산, 즉 100,000까지 숫자가 올라가기 전까지는 렌더링이 일어나지 않다가
for
문이 끝나야 비로소 렌더링 기회를 얻으며 100,000이라는 숫자가 한 번에 나타남. - 태스크 큐(
setTimeout
)는 모든setTimeout
콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1부터 100,000까지 순차적으로 렌더링 됨. - 마이크로 태스크 큐(
queueMicrotask
)는 동기 코드와 마찬가지로 렌더링이 전혀 일어나지 않다가 100,000까지 다 끝난 이후에야 한 번에 렌더링이 일어남. - 모든 것을 동시에 실행했을 경우 동기 코드와 마이크로 태스크 큐만 한 번에 100,000까지 올라가고, 태스크 큐만 앞선 예제처럼 순차적으로 렌더링 됨.
이러한 작업 순서는 브라우저에 다음 리페인트 전에 콜백 함수 호출을 가능하게 하는 requestAnimationFrame
으로도 확인할 수 있음.
console.log('a')
setTimeout(() => {
console.log('b')
}
Promise.resolve().then(() => {
console.log('c')
})
window.requestAnimationFrame(() => {
console.log('d')
})
위 코드를 실행하면 a, c, d, b 순서로 출력됨. 즉, 브라우저에 렌더링하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어남.
결론적으로 동기 코드는 물론이고 마이크로 태스크 또한 마찬가지로 렌더링에 영향을 미칠 수 있음. 따라서 만약 특정 렌더링이 자바스크립트 내 무거운 작업과 연관이 있다면 이를 어떤 식으로 분리해서 사용자에게 좋은 애플리케이션 경험을 제공해 줄지 고민해 보아야 함.
'Modern React Deep Dive > 01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트' 카테고리의 다른 글
1.6.1 구조 분해 할당 (0) | 2024.05.14 |
---|---|
1.5.2 이벤트 루프란? (1) | 2024.05.14 |
1.5.1 싱글 스레드 자바스크립트 (0) | 2024.05.14 |
1.4.4 주의할 점 (0) | 2024.04.23 |
1.4.3 클로저의 활용 (1) | 2024.04.09 |