JavaScript 동작 원리

핵심 요약

JavaScript는 기본적으로 싱글 스레드다.
실행 순서는 다음 규칙으로 반복된다.

  1. Call Stack에 있는 동기 코드를 끝까지 실행
  2. Microtask Queue를 전부 비움 (Promise.then, queueMicrotask)
  3. Task Queue(Macrotask)에서 하나 꺼내 실행 (setTimeout, setInterval, DOM 이벤트 등)
  4. 다시 1번으로 반복

실행 구조

JavaScript Engine
+----------------------+
| Call Stack           |  <- 지금 실행 중인 코드
+----------------------+

Browser / Web APIs
+----------------------+
| Timer, Network, DOM  |  <- 대기/비동기 작업 처리
+----------------------+

Queues
+----------------------+
| Microtask Queue      |  <- Promise.then, queueMicrotask
+----------------------+
| Task Queue           |  <- setTimeout, 이벤트 콜백
+----------------------+

setTimeout(0)이 바로 실행되지 않는 이유

console.log("1");

setTimeout(() => {
  console.log("3");
}, 0);

console.log("2");

// 1 -> 2 -> 3

setTimeout(0)도 바로 Stack에 들어가지 않는다.
Web API를 거쳐 Task Queue에서 기다리고, Stack이 비워진 뒤에야 실행된다.

Microtask가 Task보다 먼저 실행됨

console.log("1");

setTimeout(() => console.log("setTimeout"), 0); // Task Queue
Promise.resolve().then(() => console.log("Promise")); // Microtask Queue

console.log("2");

// 1 -> 2 -> Promise -> setTimeout

즉, “대기실”이 하나가 아니라 우선순위가 높은 Microtask 대기실이 따로 있다.

AJAX가 동시에 도는 것처럼 보이는 이유

작업1();

ajax({
  success: function () {
    성공처리();
  }
});

작업2();

실행 흐름:

  1. 작업1 실행
  2. AJAX 요청만 브라우저에 위임
  3. 작업2 실행
  4. 응답이 오면 콜백이 Queue에 들어가고, Stack이 비면 실행
JS(Stack)                      Browser(Web API)
작업1 실행
ajax 요청 전달  ------------->  네트워크 대기
작업2 실행
Stack 비움
                     응답 도착 -> 콜백 Queue 등록
콜백 실행

success는 무조건 작업2 뒤에 실행되나?

위처럼 같은 호출 흐름 안에서 작성했다면 그렇다.
응답이 매우 빨라도 현재 Stack이 비기 전에는 콜백이 실행되지 않는다.

다만 작업2가 무거운 연산이면, 콜백 실행은 더 늦어진다.

ajax vs fetch 차이

fetch는 Promise 기반이라 .then/.catch/.finally 콜백이 Microtask Queue로 들어간다.

ajax({ success: () => console.log("ajax") }); // 보통 Task 계열
fetch("/api").then(() => console.log("fetch")); // Microtask
setTimeout(() => console.log("setTimeout"), 0); // Task

console.log("작업2");

일반적으로:

작업2 -> fetch -> ajax/setTimeout

fetch 여러 개일 때 순서

fetch("/api1")
  .then(() => console.log("fetch1-then1"))
  .then(() => console.log("fetch1-then2"));

fetch("/api2")
  .then(() => console.log("fetch2-then1"))
  .then(() => console.log("fetch2-then2"));

정리:

결론

  1. Stack이 먼저다.
  2. Stack이 비면 Microtask를 먼저 전부 처리한다.
  3. 그 다음 Task를 처리한다.
  4. fetch.then은 Microtask라서 setTimeout류보다 먼저 실행될 수 있다.