setTimeout(fn, 0)의 비밀

대호 Dorant·2025년 4월 10일

안녕하세요. 오늘은 흥미로운 주제를 가져와봤습니다!

Node.js에서 아래와 같은 코드를 실행하면, 출력의 순서는 어떻게 될까요?

setTimeout(() => console.log("a"), 0);

console.log("b");

모두가 예상하시듯이 결과는 당연히! "b"가 먼저 출력되고, 그 다음에 "a"가 출력됩니다. setTimeout(fn, 0) 인데? 왜 늦게 출력이 될까요?

지금부터 이유를 Araboza

이것은 바로 Node.js의 이벤트 루프와 비동기 실행 메커니즘으로 인해 "b"가 먼저 출력이 되는거랍니다?

이벤트 루프란 무엇인가?

Node.js가 싱글 스레드임에도 불구하고, 비동기 작업을 처리할 수 있게 해주는 친구라고 보시면 됩니다. 이벤트 루프는 여러 단계(=phases)로 구성되어 있으며, 각 페이즈마다 특정 유형의 작업을 처리합니다. (이벤트 루프에 대한 자세한 내용은 추후에 정리해서 따로 다루겠습니다 ㅎㅎ!)

주요 구성 요소

1. 콜 스택 (Call Stack)
- Javascript 엔진이 코드를 실행하는 장소라고 보시면 됩니다.
- LIFO (Last In, First Out) 방식으로 동작합니다. 마지막에 들어온 함수가 먼저 실행되는 구조입니다.
- 한 번에 하나의 함수만 처리합니다.

2. 큐 (Queue)
- 매크로태스크 큐 (Macrotask Queue)
- 마이크로태스크 큐 (Microtask Queue)

3. libuv
- C로 작성된 라이브러리로 Node.js의 이벤트 루프를 구현합니다.

Queue에 대해 좀만 더 Araboza

매크로태스크 큐 (Macrotask Queue)

  • 매크로태스크 큐는 "태스크 큐" or "콜백 큐"라고도 불립니다. 이 큐에는 대표적으로 다음과 같은 작업들이 포함됩니다.
💡 setTimeout과 setInterval 콜백
	- 지정된 시간이 지난 후에 큐에 추가
    - setTimeout(fn, 0)은 실제로는 브라우저의 경우 최소 4ms,
      Node.js에서는 1ms 정도 지연 발생
💡 I/O 작업 콜백
	- 파일 읽기 및 쓰기 작업의 콜백
    	예시) Node.js의 fs 모듈을 사용한 파일 읽기/쓰기 작업

    - 네트워크 요청의 콜백
    	예시) HTTP/HTTPS 요청

    - 데이터베이스 쿼리의 콜백
    	예시) MySQL 연결 및 쿼리 실행
💡 setImmediate 콜백
	- Node.js 환경에서만 사용 가능합니다.
    - I/O 사이클 직후에 실행됩니다.
💡 UI 렌더링 및 이벤트 콜백 (브라우저 환경에서)
	- 클릭, 키보드 입력 등 사용자 이벤트 처리
	- DOM 조작 후 렌더링
  • 매크로태스크는 이벤트 루프의 한 사이클에서 하나씩만 처리됩니다.
  • 하나의 매크로태스크가 실행된 후, 마이크로태스크 큐가 비워질 때까지 모든 마이크로태스크를 처리한 다음에야 다음 매크로태스크가 실행됩니다.

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

  • 마이크로태스크 큐는 매크로태스크보다 높은 우선순위를 가진 작업들을 저장합니다.
💡 Promise 콜백
	- Promise의 then(), catch(), finally() 메서드에 전달된 콜백
		예시) Promise.resolve().then(() => console.log('Promise 콜백'))
💡 process.nextTick()
	- 모든 마이크로태스크 중에서도 가장 높은 우선순위를 가짐
    - 현재 실행 중인 코드가 완료된 직후, 다른 어떤 이벤트 루프 활동보다 먼저 실행
    	예시) process.nextTick(() => console.log('NextTick 콜백'))
💡 queueMicrotask() (모던 브라우저와 Node.js v11 이상)
	- 명시적으로 마이크로태스크를 예약할 수 있는 API
    	예시) queueMicrotask(() => console.log('마이크로태스크'))
💡 MutationObserver 콜백 (브라우저 환경에서)
	- DOM 변형을 감지하는 API의 콜백
  • 마이크로태스크의 핵심은 현재 실행 중인 스크립트나 태스크가 완료된 직후, 그리고 다음 매크로태스크가 시작되기 전에 모두 처리된다는 것입니다.

  • 마이크로태스크 큐에 있는 작업이 또 다른 마이크로태스크를 큐에 추가하면, 이벤트 루프는 매크로태스크로 돌아가기 전에 새로 추가된 마이크로태스크까지 모두 처리합니다.

예시를 통해 태스크 큐 차이를 Araboza

다음 코드의 실행 결과는 어떻게 될까요? 🤔

console.log('시작');

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

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

console.log('종료');

실행 결과‼️

시작
종료
Promise 1
Promise 2
setTimeout 0

차근차근 이유를 설명해드릴게요.

1. console.log('시작')과 console.log('종료')는 동기 코드로 즉시 실행
2. setTimeout 콜백은 매크로태스크 큐에 추가됩니다.
3. Promise 콜백들은 마이크로태스크 큐에 추가됩니다.
4. 동기 코드 실행이 완료된 후, 마이크로태스크 큐의 모든 작업(Promise 1, Promise 2)이 실행됩니다.
5. 마지막으로 매크로태스크 큐의 작업(setTimeout 0)이 실행됩니다.

결론

위와 같은 이유로, setTimeout(fn, 0)은 "즉시" 실행되는 것처럼 보이지만, 실제로는 이벤트 루프의 매크로태스크 큐에 의해 지연된다는 사실 다들 이해하셨...을...거라고 믿...믿습니다?

profile
안녕하세요. 멋쟁이 백엔드 개발자입니다😎😎

2개의 댓글

comment-user-thumbnail
2025년 4월 11일

그러니까 타임아웃콜백을 먼저 처리한 다음에 SetTimeout 0이 찍히느라 저렇게 된다는 거죠?

1개의 답글