그동안 async/await, Promise만 쓰면서 "어떻게 동작하는지는 나중에 알아보지" 했는데, 막상 팀 내 기술 공유에서 설명하려니 버벅거렸다.
오늘은 JavaScript의 비동기 처리 메커니즘을 제대로 정리해보자. 특히 Microtask Queue vs Macrotask Queue 차이는 실무에서 디버깅할 때 꼭 알아야 하는 부분이다.
JavaScript는 왜 싱글 스레드일까?
JavaScript는 싱글 스레드 언어다. 한 번에 하나의 작업만 처리할 수 있다는 뜻이다. 그럼 어떻게 버튼 클릭하면서 동시에 API 호출도 하고 애니메이션도 실행할 수 있을까?
답은 이벤트 루프에 있다. 브라우저(또는 Node.js)가 제공하는 메커니즘으로, JavaScript 엔진과 함께 협력해서 비동기 작업을 처리한다.
JavaScript Engine의 구성 요소
Memory Heap
객체와 데이터가 저장되는 메모리 공간이다. 우리가 변수에 할당한 값들이 여기에 저장된다.
Call Stack
함수 호출을 추적하고 실행 순서를 관리하는 스택이다. LIFO(Last In First Out) 구조로 동작한다.

function first() {
second();
console.log('첫 번째');
}
function second() {
console.log('두 번째');
}
first();
실행 순서:
- first() 호출 → 콜 스택에 추가
- second() 호출 → 콜 스택에 추가
- console.log('두 번째') 실행 → 콜 스택에서 제거
- second() 완료 → 콜 스택에서 제거
- console.log('첫 번째') 실행 → 콜 스택에서 제거
- first() 완료 → 콜 스택에서 제거
단순하다. 하지만 setTimeout이나 fetch 같은 비동기 함수가 끼어들면 이야기가 달라진다.
Web API (비동기를 담당)
브라우저는 JavaScript 엔진 외에도 Web API를 제공한다:
- 타이머: setTimeout, setInterval
- DOM 조작: addEventListener
- 네트워크: fetch, XMLHttpRequest
이런 API들은 별도 스레드에서 실행되기 때문에 콜 스택을 블로킹하지 않는다.

console.log('시작');
setTimeout(() => {
console.log('타이머 완료');
}, 1000);
console.log('끝');
실행 결과
시작
끝
타이머 완료
setTimeout이 Web API에서 처리되는 동안 다음 코드들이 계속 실행된다.
Task Queue, 비동기 작업의 대기실
Web API에서 처리가 완료된 콜백 함수들은 Task Queue로 이동한다. 여기서 콜 스택이 비기를 기다린다.
Task Queue는 두 종류로 나뉜다.
Macrotask Queue
일반적인 비동기 작업들이 들어간다.
- setTimeout, setInterval
- DOM 이벤트
- I/O 작업
Microtask Queue
더 높은 우선순위를 가진 작업들이 들어간다.
- Promise.then, Promise.catch
- async/await
- MutationObserver
핵심: Microtask가 Macrotask보다 먼저 처리된다.
우선순위 테스트
이 코드를 실행해보면 우선순위를 확실히 알 수 있다.
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
실행 결과:
script start
script end
promise1
promise2
setTimeout
실행 순서 분석:
- console.log('script start') → 즉시 실행
- setTimeout 콜백 → Macrotask Queue로 이동
- Promise.then 콜백 → Microtask Queue로 이동
- console.log('script end') → 즉시 실행
- 콜 스택이 비면 Microtask Queue 먼저 처리
- 모든 Microtask 완료 후 Macrotask Queue 처리
setTimeout의 딜레이가 0이어도 Promise보다 늦게 실행된다는 게 핵심이다.
Event Loop의 동작 원리
Event Loop는 콜 스택과 Task Queue를 계속 모니터링한다.

- 콜 스택이 비어있나? 확인
- Microtask Queue에 작업이 있나? 확인 → 있으면 모두 처리
- Macrotask Queue에 작업이 있나? 확인 → 하나씩 처리
- 1번으로 돌아가서 반복
이 과정이 무한 루프로 반복되면서 비동기 작업들이 순차적으로 처리된다.
실무에서 자주 보는 패턴
const fetchData = () => {
console.log('API 호출 시작');
setTimeout(() => {
console.log('타이머 작업');
}, 0);
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log('API 응답:', data);
})
.catch(error => {
console.error('API 에러:', error);
});
console.log('API 호출 요청 완료');
};
fetchData();
실행 순서:
- API 호출 시작
- API 호출 요청 완료
- API 응답: {...} (Promise가 먼저)
- 타이머 작업 (setTimeout이 나중)
실제 개발할 때 이 순서를 모르면 "왜 내 코드가 예상대로 안 돌지?" 하며 헤맬 수 있다.
디버깅할 때 유용한 팁
콘솔 로그의 순서가 예상과 다르다면 이벤트 루프 때문일 가능성이 크다.
// 안티패턴
setTimeout(() => {
console.log('A');
}, 0);
Promise.resolve().then(() => {
console.log('B');
});
console.log('C');
// 결과: C → B → A
개발자 도구의 Performance 탭에서 이벤트 루프 동작을 시각적으로 확인할 수도 있다.
마무리
이벤트 루프 개념을 제대로 알고 나니까, 그동안 "왜 이렇게 동작하지?" 했던 비동기 코드들이 이해되기 시작했다.
특히 Microtask vs Macrotask 우선순위 차이는 React의 useEffect나 상태 업데이트 타이밍을 이해하는 데도 도움이 됐다.
어떻게 동작하는지 모르고 쓰는 것과 원리를 알고 쓰는 것은 완전히 다르다. 디버깅 실력도 확실히 늘었고, 코드 리뷰할 때도 더 정확한 피드백을 줄 수 있게 됐다.
JavaScript 개발자라면 반드시 알아야 할 기본기라고 생각한다.
'Frontend > Javascript' 카테고리의 다른 글
| JavaScript - JavaScript의 복사, 비교, 그리고 Immer (0) | 2025.11.05 |
|---|---|
| JavaScript - 프로토타입과 상속 (0) | 2025.09.29 |
| Javascript - Promise 비동기 처리 (0) | 2025.09.25 |