BOYEON
자바스크립트는 어떻게 논블로킹처럼 동작할까요?2025.09.28.

“자바스크립트는 단일 스레드인데, 어떻게 논블로킹처럼 동작할까요?”

핵심은 역할 분담입니다. 자바스크립트 엔진은 코드 실행기(콜스택) 역할만 하고, 브라우저/Node 런타임(Web APIs/Node APIs) 이 I/O·타이머·네트워크 같은 오래 걸리는 일들을 백그라운드에서 처리합니다.


Call stack

  • 자바스크립트 엔진은 함수를 호출할 때 콜스택에 push하고, 끝나면 pop합니다.
  • 싱글 스레드이기 때문에, 스택이 바쁜 동안에는 다른 자바스크립트 코드를 실행할 수 없습니다.
  • 그래서 무거운 동기 연산(예: 큰 루프, 복잡한 계산)이 스택을 오래 점유하면 블로킹이 발생합니다.

논블로킹 환경: 오래 걸리는 일은 “맡기고 결과만 받기”

  • I/O, 타이머, 네트워크 같은 작업은 엔진이 직접 하지 않습니다. 브라우저의 Web APIs(혹은 Node의 런타임)가 백그라운드 스레드에서 처리합니다.
  • 예를 들어 setTimeout을 호출하면, 엔진은 콜백 + 타이머 정보를 타이머 시스템에 넘기고 자기 할 일을 계속합니다. 타이머가 끝나면 콜백이 큐에 등록됩니다.
  • 콜백 함수 자체의 실행은 언제나 메인 스레드(콜스택) 에서 이루어집니다. 백그라운드는 “준비만” 하고, 실행권은 이벤트 루프를 통해 다시 JS에게 돌아옵니다.

이벤트 루프와 두 개의 큐

이벤트 루프는 “지금 콜스택이 비었는지”를 계속 확인합니다. 비어 있으면 큐에서 작업을 하나 꺼내 스택으로 올립니다. 여기에는 두 종류의 큐가 있습니다.

매크로태스크 큐 (Macrotask Queue)

  • 큰 단위 작업들이 대기합니다. 예: setTimeout, 사용자 입력 이벤트, MessageChannel 등
  • 매 틱(tick) 마다 매크로태스크는 1개씩만 처리합니다. (여러 개를 한 번에 처리하면 프레임을 쉽게 다 써버리기 때문입니다.)

마이크로태스크 큐 (Microtask Queue)

  • 즉각 처리해야 하는 작은 후처리가 대기합니다. 예: Promise.then/catch/finally, queueMicrotask, MutationObserver
  • 한 틱에서 매크로태스크 1개를 끝낸 뒤, 마이크로태스크는 전부 비웁니다. 그래서 Promise.then 체인이 많으면 렌더링이 잠시 미뤄질 수도 있습니다. (마이크로태스크 “기아” 현상)
  • 적절하게 쓰면 좋지만, 과하면 렌더링이 뒤로 밀릴 수 있습니다. 아래처럼 재귀로 마이크로태스크를 무한 생성하면 화면이 굶습니다.
    (function starve() {
      Promise.resolve().then(starve);
    })();

(참고) 한 틱의 순서(브라우저 기준)

  1. 매크로태스크 1개 실행 → 2) 마이크로태스크 전부 비우기 → 3) 렌더 단계(스타일/레이아웃/페인트, requestAnimationFrame) → 4) 다음 틱

예시

console.log('A');

setTimeout(() => console.log('macro: setTimeout(0)'), 0);

Promise.resolve()
  .then(() => console.log('micro: then 1'))
  .then(() => console.log('micro: then 2'));

queueMicrotask(() => console.log('micro: queueMicrotask'));

console.log('B');
A
B
micro: then 1
micro: then 2
micro: queueMicrotask
macro: setTimeout(0)
  • 동기 코드(A, B)가 먼저 실행되고,
  • 마이크로태스크가 모두 처리된 뒤,
  • 다음 틱에 매크로태스크가 실행됩니다.

Copyright ⓒ 2023 정보연 All rights reserved.