2025. 12. 24. 16:49ㆍjavascript
console.log('시작');
Promise.resolve().then(() => {
console.log('프로미스1');
});
setTimeout(() => {
console.log('타임아웃');
}, 0);
Promise.resolve().then(() => {
console.log('프로미스2');
});
console.log('끝');
이 순서대로 호출시 콘솔이 어떻게 찍힐까?
microtask queue와 macrotask queue를 모른다면 시작 -> 프로미스1 -> 프로미스2 -> 끝 -> 타임아웃
이렇게 생각할 수 도 있을것 같다.
실제 결과는 아래와 같다.
시작 -> 끝 -> 프로미스1 -> 프로미스2 -> 타임아웃
왜 이런결과가 일어날까?
Microtask queue, Macrotask queue
자바스크립트는 싱글스레드 언어로 한번에 하나의 동작만 실행이 가능하다. 하지만 브라우저 환경에서는 Web API를 통해 비동기를 처리하고 이벤트루프가 콜스택과 태스크큐를 모니터링 하면서 실행 순서를 조율한다.
이때 태스크큐는 마이크로태스크큐와 매크로태스크큐로 나뉜다.
Microtask queue
Promise, mutation observer, async/await
Macrotask queue
setTimeout, setInterval, setImmediate
둘의 차이점은 바로 우선순위이다.
microtask queue가 우선순위가 높다. microtask queue의 모든 콜백들을 처리한 후 macrotask queue를 처리한다.
브라우저 렌더링 관점에서 알아보기
microtask queue는 현재 실행중인 js코드가 끝나고 콜스택이 비었을때, 브라우저 렌더링이 되기 전에 모두 처리가 되고 macrotask queue는 렌더링 이후 태스크큐에서 하나씩 실행된다. 아래 코드예시를 봐보자
setLoading(true)
Promise.resolve().then(() => {
setLoading(false)
})
결과가 어떻게 될까?
로딩 상태가 true였다가 비동기 작업 후 다시 로딩상태가 false로 변할것만 같다.
하지만 Promise는 마이크로테스크기 때문에 js실행이 끝난뒤 렌더링이 되기전에 실행된다.
로딩 상태가 true -> Promise 실행 -> 로딩상태 false 로 바뀌고 나서 렌더링이 실행되기 때문에
이미 화면에서는 false인 상태라 화면에서 로딩상태임을 확인 할 수 없다.
document.getElementById("btn").addEventListener("click", () => {
console.log('클릭!')
setLoading(true);
Promise.resolve().then(() => {
console.log('Promise 비동기 실행!')
setLoading(false);
});
});

start 버튼을 눌렀을때 로딩상태가 true -> false일것을 기대하지만 실제로는 계속 false인 상태인것을 확인 할 수 있다.
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 0)
그럼 macrotask queue인 settimeout으로 확인해보면 어떨까?
document.getElementById("btn").addEventListener("click", () => {
console.log('클릭!')
setLoading(true);
setTimeout(() => {
console.log('setTimeout 비동기 실행!')
setLoading(false);
},1000)
});

macrotask queue는 현재 실행중인 js와 microtask queue처리가 끝난뒤 브라우저 렌더링 이후 실행하기 때문에 등록된 비동기 콜백들을 화면에서 볼 수 있다.
await/async도 확인해보자
async function test() {
console.log('A')
await Promise.resolve()
console.log('B')
}
// 실행
test()
console.log('C')
await은 Promise가 resolve될때까지 async함수의 실행을 중단한다.
Promise가 resolve되면, await이후의 코드는 마이크로태스크큐로 등록이 되어 현재 실행중인 코드들이 끝난후 실행된다.
따라서 결과가 A->C->B 로 실행된다.
그럼 이상태에서 setTimeout을 추가해보면?
async function test() {
console.log('A')
await Promise.resolve()
console.log('B')
}
test()
setTimeout(() => {
console.log('G')
})
console.log('C')
1. test 함수 호출
2. 콘솔에 A 출력
3. await을 만나 코드 실행을 중단시키므로 다음 코드 실행
4. test 함수 종료.
5. setTimeout의 콜백을 macrotask queue에 등록
6. 콘솔에 C 출력
7. 모든 코드가 실행됬으므로 콜스택이 비워짐.
8. 마이크로태스크큐부터 실행
9. 콘솔에 B 출력
10. 매크로태스큐 실행
11. 콘솔에 G 출력
이와 같이 실행되는걸 알 수 있다.
왜 Microtask Queue와 Macrotask Queue의 차이를 알아야 할까?
- 이벤트 루프를 막는 문제를 피할 수 있다.
Microtask queue는 렌더링 직전에 실행된다. 너무 많은 Mircrotask를 생성하거나 오래걸리는 Mircrotask를 실행하면? 브라우저의 렌더링이 지연 된다. 사용자에게 좋지 못함 경험을 주며 성능저하 우려 할 수 있다. - 의도치 않은 버그를 예방할 수 있다.
차이를 모르고 코드 실행순서를 기대할경우 의도치 않은 결과나 버그 발생할 확률이 높다. - setTimeout(0)을 지연처리로 쓰지 않게됨.
setTimeout(0)은 즉시 실행을 보장않고, 모든 마이크로태스크가 처리되고 렌더링이 이루어진 이후에 매크로태스크로 실행된다. 따라서 정확한 지연 시간을 기대하는 용도로 사용하기에는 적합하지 않음.
'javascript' 카테고리의 다른 글
| JS 참조와 복사(깊은복사,얕은복사) feat. 리액트에서의 얕은 비교 (0) | 2025.11.28 |
|---|---|
| requestIdleCallback으로 LCP 개선 (0) | 2025.04.28 |
| 이벤트루프 드디어 정.리.해.보.았.다. (0) | 2024.02.22 |
| 논리연산자(논리합,옵셔널 체이닝,null 병합연산자)로 코드 간결하게 하기, if문 줄이기 (0) | 2022.06.28 |
| [javascript] 이벤트 버블링과 캡처링! 이벤트 흐름,전달 알아보기 (0) | 2022.05.28 |