Skip to Content
WebJavaScriptECMAScript07. 이벤트 루프와 비동기 모델 개념

이번 편의 결과물: console.log, setTimeout, Promise를 섞은 스크립트의 실행 순서를 먼저 예측하고, 실행해 검증합니다. 08편(파일 저장)에서 쓸 async/await의 동작 원리를 이해합니다. · 다루는 개념: 콜스택, 태스크 큐·마이크로태스크 큐, 동기·비동기 실행 순서

이 편에서 만드는 파일

이번 편은 이론 편입니다. todo-cli 본체는 수정하지 않고, 실행 순서를 확인하는 임시 스크립트만 하나 만듭니다.

todo-cli/ └── playground/ └── event-loop-order.js (+, 확인용, 08편 이후에는 쓰지 않음)

개념 정리

JavaScript는 싱글 스레드다

JavaScript 엔진은 한 번에 한 가지 코드만 실행하는 콜스택(call stack) 하나를 씁니다. 함수를 호출하면 스택에 쌓이고, 끝나면 스택에서 빠집니다. 콜스택이 비어 있어야 다음 작업을 실행할 수 있습니다.

태스크 큐와 마이크로태스크 큐

setTimeout, Promise처럼 즉시 끝나지 않는 작업은 결과가 준비되어도 바로 실행되지 않고 대기열에 들어갑니다. 대기열은 두 종류가 있고 우선순위가 다릅니다.

대기열들어가는 작업실행 시점
마이크로태스크 큐Promise.then/catch/finally, async 함수의 await 이후 코드콜스택이 빌 때마다, 다음 매크로태스크보다 먼저
태스크 큐(매크로태스크 큐)setTimeout, setInterval, I/O 콜백콜스택이 비고 마이크로태스크 큐도 빈 뒤

이벤트 루프(event loop)는 “콜스택이 비었는지 반복 확인하다가, 비어 있으면 마이크로태스크 큐를 모두 비우고, 그다음 태스크 큐에서 하나를 꺼내 실행”하는 절차입니다. 마이크로태스크 큐는 한 번에 전부 비우지만, 태스크 큐는 한 번에 하나씩만 꺼냅니다.

실행 순서 규칙 정리

순서코드 종류
1동기 코드(위에서 아래로 즉시 실행)
2마이크로태스크(Promise 후속 처리) 전부
3태스크(setTimeout 콜백 등) 하나, 그 뒤 다시 2번부터 반복

await 뒤의 코드는 Promise.then 안의 코드와 같은 취급을 받습니다. async 함수 안에서 await를 만나면 그 시점에 함수 실행을 멈추고 제어권을 반환하며, await가 끝나는 시점에 마이크로태스크로 이어서 실행됩니다. 08편에서 fs/promisesawait로 파일을 읽고 쓸 때, 파일 읽기가 끝나기를 기다리는 동안 다른 동기 코드가 먼저 실행되는 이유가 이 순서 때문입니다.

실습

1. playground 폴더 만들기

todo-cli/playground/event-loop-order.js 파일을 만듭니다.

2. 코드 작성 전 순서 예측

아래 코드를 실행하기 전에 출력 순서를 먼저 종이에 적어봅니다.

// playground/event-loop-order.js console.log('1. 동기 코드 시작'); setTimeout(() => { console.log('2. setTimeout 콜백(태스크)'); }, 0); Promise.resolve().then(() => { console.log('3. Promise.then(마이크로태스크)'); }); console.log('4. 동기 코드 끝');

3. 실행

node playground/event-loop-order.js

4. 확인

  • 실제 출력 순서는 아래와 같습니다. 예측과 비교합니다.
1. 동기 코드 시작 4. 동기 코드 끝 3. Promise.then(마이크로태스크) 2. setTimeout 콜백(태스크)
  • setTimeout(..., 0)처럼 지연 시간을 0으로 줘도 동기 코드와 마이크로태스크보다 항상 늦게 실행됩니다. “0초 뒤 실행”이 아니라 “지금 실행 중인 코드와 마이크로태스크가 끝난 뒤 태스크 큐에 넣는다”는 뜻입니다.

직접 해보기

  1. event-loop-order.jsPromise.resolve().then(...)을 하나 더 추가해, 마이크로태스크 두 개가 태스크(setTimeout)보다 먼저 실행되는지 확인합니다.
  2. async function main() { console.log('A'); await null; console.log('B'); } main(); console.log('C');를 실행해 A, C, B 순서로 나오는 이유를 await 이후 코드가 마이크로태스크로 처리되기 때문이라고 설명해봅니다.

정답 보기

await null 이후의 console.log('B')Promise.resolve().then(() => console.log('B'))와 동일하게 취급되어 마이크로태스크 큐에 들어갑니다. main() 호출 시 await를 만나기 전까지(console.log('A'))는 동기로 실행되고, 그 뒤 제어권이 호출한 쪽으로 돌아가 console.log('C')가 먼저 실행됩니다. 콜스택이 빈 뒤 마이크로태스크 큐의 console.log('B')가 실행되어 A, C, B 순서가 됩니다.

자주 하는 실수

증상원인고치는 법
setTimeout(fn, 0)이 즉시 실행될 거라 기대함”0초”를 “즉시”로 착각setTimeout은 지연 시간과 무관하게 항상 태스크 큐를 거친다고 이해한다
Promise.then 콜백이 setTimeout보다 늦게 실행될 거라 예상마이크로태스크와 태스크 우선순위를 반대로 기억함마이크로태스크 큐가 항상 태스크 큐보다 먼저 전부 비워진다는 규칙을 외운다
await 이후 코드가 즉시 실행될 거라 기대await가 코드를 멈추고 마이크로태스크로 넘긴다는 점을 놓침await 뒤 코드는 .then() 콜백과 같은 취급이라고 생각한다

확인 문제

문제 14지선다
JavaScript 엔진의 콜스택에 대한 설명으로 옳은 것은?
문제 24지선다
마이크로태스크 큐와 태스크 큐의 우선순위 관계로 옳은 것은?
문제 34지선다
setTimeout(fn, 0)의 콜백이 실행되는 시점으로 옳은 것은?
문제 44지선다
async 함수 안의 await 이후 코드가 실행되는 방식으로 옳은 것은?

참고 자료

Last updated on