1. 개요

이번에 JavaScript 언어 자체의 기초~중급 개념을 7편으로 정리한다. 이벤트 루프, 클로저, this, 프로토타입/class, Promise·async/await, 동등 비교, 모듈 시스템 순이다. 프레임워크 얘기가 아니라 순수 JS다. 첫 편은 이벤트 루프다.

자바스크립트는 싱글 스레드다. 그런데 setTimeout, 네트워크 요청, 이벤트 핸들러는 다 동시에 도는 것처럼 보인다. 어떻게 하나의 스레드로 이게 가능한지, 그리고 왜 setTimeout(fn, 0)이 “0초 뒤”가 아니라 “지금 하고 있는 일이 다 끝난 뒤”에 도는지를 정리한다. 콜스택, 태스크 큐, 마이크로태스크 큐 세 가지만 구분하면 실행 순서를 예측할 수 있다.


2. 핵심 내용

2-1. 콜스택은 하나뿐이다

자바스크립트 엔진은 함수를 호출할 때마다 콜스택에 쌓고, 끝나면 뺀다. 이 스택이 비어있어야 다음 일을 처리할 수 있다. 스택이 하나뿐이라는 게 핵심이다. 동시에 두 함수가 실행되는 일은 없다.

function a() { b(); }
function b() { console.log("b"); }
a();
// 콜스택: a 호출 -> b 호출 -> b 출력 -> b 빠짐 -> a 빠짐

2-2. 태스크 큐와 마이크로태스크 큐

setTimeout, setInterval, DOM 이벤트 콜백은 (매크로)태스크 큐로 간다. Promise.then, queueMicrotask, async 함수의 await 뒤 코드는 마이크로태스크 큐로 간다. 이벤트 루프는 매 사이클마다 “콜스택이 비면 마이크로태스크 큐를 전부 비우고, 그다음 태스크 큐에서 하나만 꺼내 실행”한다. 마이크로태스크가 항상 먼저다.

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 출력: 1, 4, 3, 2

동기 코드(1, 4)가 먼저 끝나고, 콜스택이 비면 마이크로태스크(3)를 처리하고, 그다음에야 태스크 큐의 타이머 콜백(2)이 돈다.

2-3. setTimeout(fn, 0)이 바로 안 도는 이유

setTimeout(fn, 0)은 “0밀리초 뒤에 큐에 넣어달라”는 뜻이지 “0밀리초 뒤에 실행”이 아니다. 큐에 들어간 순서대로, 그리고 콜스택이 완전히 비고 마이크로태스크까지 다 처리된 다음에야 차례가 온다. 지금 실행 중인 동기 코드가 길면 setTimeout(fn, 0)도 그만큼 늦게 돈다.

2-4. 흔한 오해: async 함수는 별도 스레드에서 돈다

아니다. async 함수도 콜스택 위에서 동기적으로 시작한다. await를 만나는 순간 그 함수의 나머지 실행을 마이크로태스크로 미루고 콜스택에서 빠질 뿐이다. 스레드는 여전히 하나고, await가 다른 코드를 막지 않는 이유도 여기 있다 — 기다리는 동안 콜스택을 비워서 다른 동기 코드나 다른 이벤트가 그 사이에 실행될 수 있게 해준다.

async function f() {
  console.log("a");
  await null;
  console.log("b");
}
f();
console.log("c");
// 출력: a, c, b

3. 마무리

요약

  • 콜스택은 하나, 동시 실행은 없다
  • 콜스택이 비면 마이크로태스크 큐를 다 비우고, 그다음 태스크 큐에서 하나 꺼낸다
  • setTimeout(fn, 0)은 즉시 실행이 아니라 “지금 할 일 다 끝난 뒤 순서대로”다
  • async/await도 싱글 스레드 위에서 돈다, 마이크로태스크로 미루는 것뿐이다

다음은 클로저는 왜 헷갈리나 이다.