본문 바로가기

Web Development/JAVASCRIPT

자바스크립트의 비동기 실행 흐름 정리

자바스크립트는 싱글 스레드 기반으로 동작하기 때문에, 한 번에 하나의 작업만 처리할 수 있습니다.
따라서 콜 스택(Call Stack), 이벤트 루프(Event Loop), Web API, 큐(Queue)를 통해 동기/비동기 작업을 처리하게 됩니다.


✅ 예제 코드

console.log(1);

setTimeout(() => {
	console.log(2)
}, 100);

console.log(3);

1. 동기 작업 진행

  1. console.log(1)이 콜 스택에 올라가 실행됩니다.
  2. setTimeout(...) 실행
    • setTimeout 함수 자체는 콜 스택에서 실행되지만,
    • 내부 콜백 함수 () => console.log(2)는 Web API 영역으로 넘겨집니다.
    • Web API가 100ms 타이머를 실행하고, 끝나면 Task Queue에 콜백을 넣습니다.
  3. console.log(3)이 콜 스택에 올라가 실행됩니다.

2. 비동기 콜백 대기 및 실행

  1. 이 시점에서 Call Stack은 비어 있습니다.
  2. 100ms 후 Web API가 콜백을 Task Queue에 넣습니다.

3. 이벤트 루프가 큐를 확인

  1. 이벤트 루프는 Call Stack이 비었는지 확인하고,
  2. 비어 있으면 Task Queue에서 콜백을 꺼내 Call Stack에 넣어 실행합니다.

🎯 최종 출력 순서

1
3
2

🧱 각 영역 정리

개념 역할
Call Stack 현재 실행 중인 동기 코드 저장소
Web APIs setTimeout, fetch, DOM 이벤트 등을 비동기로 처리하는 브라우저 제공 영역
Task Queue 비동기 콜백이 완료되면 저장되는 큐 (Macrotask Queue)
Event Loop Call Stack이 비면 큐에서 작업을 꺼내 Stack으로 보냄

중요: Event Loop는 Call Stack이 완전히 비어야 Task Queue에서 작업을 꺼낼 수 있습니다.

⛔ 동기 작업이 길어지면?

예를 들어, setTimeout(..., 3000)으로 예약된 함수가 있어도 동기 코드가 3초 이상 실행 중이라면, 정확히 3초 뒤에 실행되지 않습니다.

상황 이유
CPU 오래 점유하는 동기 코드 타이머 콜백이 뒤로 밀림
UI 작업이 멈추는 경우 사용자 경험 저하
정확한 시간 제어가 필요한 경우 requestAnimationFrame 또는 Web Worker 사용 고려

📌 Microtask vs Macrotask

큐 종류 대표 예시 우선 순위
Microtask Queue Promise.then, queueMicrotask, async/await 높음
Macrotask Queue setTimeout, setInterval, DOM 이벤트 등 낮음

작동 순서:

  1. Call Stack이 비면
  2. Microtask Queue 먼저 모두 처리
  3. 그 다음 Macrotask를 하나 처리
  4. 다시 1번부터 반복
console.log(1);

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

Promise.resolve().then(() => {
	console.log('promise');
});

console.log(2);

출력 순서:

1
2
promise
setTimeout

📎 2초가 걸리는 Promise는 어떻게 동작할까?

1. 동기적으로 2초를 블로킹하는 경우

console.log(1);

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

Promise.resolve().then(() => {
	const start = Date.now();
	while (Date.now() - start < 2000) {} // 2초 동안 블로킹
	console.log('promise');
});

console.log(2);

콜스택과 마이크로태스크 흐름

출력 순서:

1
2
promise
setTimeout

설명:

  • Microtask(Promise)는 Macrotask(setTimeout)보다 우선
  • 하지만 Microtask에서 블로킹 코드가 있으면 Macrotask도 기다려야 함
---

2. 비동기 2초 후 실행되는 경우

console.log(1);

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

function fakeFetch(delay = 1000) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🧪 fake data");
    }, delay);
  });
}

async function run() {
  const result = await fakeFetch(2000);
  console.log("promise:", result);
}

run();

console.log(2);

await 이후 마이크로태스크 등록

출력 순서:

1
2
setTimeout
promise: 🧪 fake data

왜 이렇게 동작할까?

  1. run() 호출 → await fakeFetch(2000) → Promise 반환 → 중단
  2. setTimeout(..., 0) → Macrotask로 등록
  3. console.log(2) 실행
  4. 0ms Macrotask 먼저 실행 → setTimeout 출력
  5. 2초 뒤 Promise resolve → await 이후 코드가 Microtask로 등록됨
  6. Call Stack이 비면 → Microtask 실행 → promise 출력

📘 의문 정리

1. 왜 await을 만나면 pause되는가?

  • awaitPromise.then(...)과 유사하게 동작
  • Promise가 resolve될 때까지 함수 실행을 중단(pause)함
  • 그 이후 코드는 Microtask Queue에 등록되어 실행됨

2. 왜 await 아래 코드는 Microtask Queue에 등록되는가?

  • awaitPromise.then(...)과 같은 방식으로 동작
  • .then() 안의 콜백은 Microtask로 등록되기 때문에 await 아래 코드도 Microtask로 처리됨