자바스크립트의 비밀스러운 삶: 비동기
Source: Dev.to
Introduction
Timothy는 한숨을 쉬며 초안 테이블의 차가운 오크에 이마를 대고 있었다. 논리 다이어그램이 가득 펼쳐져 있었다.
“난 막혔어, Margaret. ‘Book Retrieval’ 시퀀스의 지시문을 작성하려고 하는데, 코드가 엔진에 방대한 양의 데이터를 가져오라고 요청할 때마다 인터페이스 전체가 멈춰. 아무것도 하지 않고 데이터가 도착하기만을 기다리고 있어. 사용자에게 정말 무례해.”
Margaret는 안심시키듯 미소를 지으며 대답했다:
“그건 무례한 게 아니라 동기식일 뿐이야. 이를 해결하려면 우리가 작업하고 있는 환경의 구체적인 아키텍처를 이해해야 해. Call Stack부터 시작하지.”
Call Stack
- JavaScript 엔진은 단일 스레드이며, 즉 하나의 Call Stack만 가지고 한 번에 한 줄의 코드만 실행할 수 있다.
- 느린 함수(예: 네트워크 요청이나 무거운 계산)가 Call Stack에 올라가면, 그 함수가 끝날 때까지 엔진은 다른 일을 할 수 없다. 이는 UI를 차단하고 멈춤 현상을 일으킨다.
Web APIs
Call Stack에서 작업을 수행하는 대신, JavaScript는 이를 Web APIs(Timer, Network 등)로 넘겨 메인 스레드 밖에서 실행한다.
setTimeout이나fetch를 호출하면 작업이 이러한 백그라운드 서비스에 전달된다.- 브라우저는 백그라운드에서 대기하거나 다운로드를 처리하고, Call Stack은 다음 코드 줄로 진행한다.
Task Queues
Web API가 작업을 마치면 임의로 코드를 Call Stack에 다시 넣을 수 없다. 대신 콜백을 두 개의 큐 중 하나에 넣어야 한다:
Macrotask Queue (Callback Queue)
- 예시:
setTimeout,setInterval, I/O 작업 - 역할: 일반적인 비동기 작업을 보관한다.
Microtask Queue
- 예시:
Promise.then,queueMicrotask,await - 역할: 더 높은 우선순위 큐; 엔진은 이를 긴급하게 처리한다.
Event Loop
Event Loop는 Call Stack과 큐들을 지속적으로 감시한다:
- Call Stack 확인: 비어 있지 않으면 대기한다.
- Microtask 실행: Stack이 비어 있으면 Microtask Queue에 있는 모든 작업을 비울 때까지 실행한다.
- Macrotask 실행: 모든 microtask가 정리된 후, Macrotask Queue에서 하나의 항목을 실행한다.
Example: Priority Order
Timothy는 우선순위 순서를 검증하기 위해 테스트 케이스를 작성했다:
console.log("Start");
setTimeout(function() {
console.log("Timeout");
}, 0);
Promise.resolve().then(function() {
console.log("Promise");
});
console.log("End");
Execution Walkthrough
console.log("Start")– 동기식이며 즉시 Call Stack에서 실행된다.setTimeout(..., 0)– Web APIs에 전달되고 즉시 완료되며, 콜백은 Macrotask Queue에 들어간다.Promise.resolve().then(...)– Web APIs에 전달되고 즉시 해결되며, 콜백은 Microtask Queue에 들어간다.console.log("End")– 동기식이며 즉시 실행된다.
동기식 코드가 끝나고 Call Stack이 비게 되면, Event Loop가 큐들을 처리한다:
- Microtask Queue에 Promise 콜백이 있어 먼저 실행된다.
- Macrotask Queue에
setTimeout콜백이 있어 microtask가 모두 정리된 뒤 실행된다.
Console Output
Start
End
Promise
Timeout
0초 지연이라도 setTimeout은 항상 Promise보다 뒤에 실행된다. 이는 macrotask가 microtask가 끝날 때까지 기다려야 하기 때문이다.