JavaScript를 쓰다 보면, 코드 실행 순서가 예상과 다르게 흘러가는 경우가 자주 있다.
특히 setTimeout, Promise, fetch 같은 비동기 작업을 사용할 때 더욱 그렇다.
예를 들어 아래 코드를 보자.
console.log('Start');
setTimeout(() => {
console.log('Task');
}, 0);
Promise.resolve().then(() => {
console.log('Microtask');
});
requestAnimationFrame(() => {
console.log('Animation Frame');
});
console.log('End');
코드를 읽는 순서대로 실행될 것 같지만 실제 출력은 다음과 같다.
Start
End
Microtask
Animation Frame
Task
이처럼 예상과 다른 실행 순서는 JavaScript의 명령어 처리 구조와 관련이 있다.
[ 위 결과는 환경에 따라 달라질 수 있다.
일부 문서에서는 Animation Frame이 Task보다 먼저 실행된다고 설명되지만,
브라우저 렌더링 타이밍에 따라 Task가 먼저 실행되는 경우도 있다. ]
명령어 처리의 핵심 구성요소
1. Call Stack (호출 스택)
- 현재 실행 중인 함수나 작업을 저장하는 LIFO 구조의 스택
- 함수가 호출되면 위로 쌓이고(push), 실행이 끝나면 제거(pop)
- 만약 무거운 연산(예: 무한 반복문)이 호출 스택을 계속 차지하면, 비동기 작업조차 실행되지 않음 (이벤트 루프가 콜백을 가져올 수 없기 때문)
2. Web APIs
- 브라우저는 setTimeout, DOM 이벤트, HTTP 요청(Fetch/AJAX), requestAnimationFrame 등 다양한 비동기 작업을 처리할 수 있도록 Web APIs를 제공한다.
- 이러한 작업들은 **자바스크립트 호출 스택(Call Stack)**만으로는 직접 처리할 수 없어서, 브라우저의 Web APIs가 대신 처리한다.
- 예를 들어:
- setTimeout: 일정 시간이 지난 뒤 콜백 실행
- DOM 이벤트: 사용자 입력 이벤트 처리
- HTTP 요청: 서버와 통신 후 결과 수신
- requestAnimationFrame: 다음 프레임에 콜백 실행
- Web APIs가 작업을 완료하면, 콜백을 Callback Queue에 등록한다.
3. Callback Queue
- Web APIs가 완료한 작업의 콜백, 그리고 프라미스와 같은 비동기 콜백이 큐에 쌓인다.
- 콜백의 종류에 따라 처리 시점과 우선순위가 다르다.
1️⃣ Microtask Queue (가장 높은 우선순위)
- 예: Promise.then, queueMicrotask, MutationObserver
- 현재 실행 중인 작업(Call Stack)이 끝나면 가장 먼저 처리됨
- 한 이벤트 루프 틱(턴)에서 Microtask Queue가 비워질 때까지 모두 처리한 후 다음 단계로 넘어감
2️⃣ Task Queue (Macro Task Queue)
- 예: setTimeout, setInterval, I/O, fetch
- Microtask Queue가 완전히 비워진 후에, 이벤트 루프가 하나씩 꺼내서 처리
3️⃣ Animation Frame Callbacks
- 예: requestAnimationFrame(callback)
- 정확히는 큐라기보다는 브라우저의 렌더링 스케줄에 따라 예약된 콜백 목록
- 보통 Microtask가 끝나고, 다음 Task가 실행되기 전에 렌더링 준비를 위해 실행됨
- 다만 브라우저의 렌더링 타이밍에 따라 실행 시점은 유동적일 수 있음
4. Event Loop
- 호출 스택이 비었는지 확인
- 비어 있다면, 다음 순서로 콜백을 실행함:
- Microtask Queue의 모든 작업 실행
- (렌더링 타이밍이라면) requestAnimationFrame 콜백 실행
- Task Queue에서 하나의 작업 실행
- 이 과정을 매우 빠르게 반복하며 프로그램을 실행함
실행함: Call Stack으로 옮겨져 처리
그래서 이걸 왜 알아야 할까?
비동기 코드는 단순히 "나중에 실행된다"고만 생각하면 곤란하다.
작업 우선순위, 호출 스택 상태, 이벤트 루프 구조까지 이해하고 있어야 비동기 동작을 제대로 예측하고 문제 상황에 대처할 수 있다.
예를 들어:
- 호출 스택에 무거운 작업이 쌓이면 비동기 콜백이 큐에 있어도 실행되지 않는다.
- setTimeout보다 먼저 작성된 Promise.then이 먼저 실행되는 이유는 Microtask Queue가 Task Queue보다 우선이기 때문이다.
- 이 구조를 이해하지 못하면 UI가 멈추거나 이벤트가 반응하지 않는 이유를 파악하기 어렵다.
요약
구성 요소 | 설명 |
Call Stack | 현재 실행 중인 함수의 저장 공간 (LIFO) |
Web APIs | 비동기 작업을 처리하고 콜백을 큐로 전달 |
Callback Queues | 실행 대기 중인 콜백 함수들이 대기하는 공간 |
Event Loop | 호출 스택이 비면 큐에서 작업을 꺼내 실행함 |
애니메이션 출처
https://github.com/Esoolgnah/Frontend-Interview-Questions/blob/main/Notes/important-4/event-loop.md
참고하면 좋을 자료
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
'FE > JS' 카테고리의 다른 글
JavaScript - this (0) | 2025.05.19 |
---|---|
자바스크립트 실행 컨텍스트 (0) | 2025.05.19 |
자바스크립트 호이스팅 정리 (1) | 2025.05.18 |
null, undefined, undeclared, NaN (1) | 2025.05.17 |
JavaScript: var, let, const (0) | 2025.05.13 |