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

Node.js에서 아래와 같은 코드를 실행하면, 출력의 순서는 어떻게 될까요?
setTimeout(() => console.log("a"), 0);
console.log("b");
모두가 예상하시듯이 결과는 당연히! "b"가 먼저 출력되고, 그 다음에 "a"가 출력됩니다. setTimeout(fn, 0) 인데? 왜 늦게 출력이 될까요?
이것은 바로 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의 이벤트 루프를 구현합니다.
💡 setTimeout과 setInterval 콜백
- 지정된 시간이 지난 후에 큐에 추가
- setTimeout(fn, 0)은 실제로는 브라우저의 경우 최소 4ms,
Node.js에서는 1ms 정도 지연 발생
💡 I/O 작업 콜백
- 파일 읽기 및 쓰기 작업의 콜백
예시) Node.js의 fs 모듈을 사용한 파일 읽기/쓰기 작업
- 네트워크 요청의 콜백
예시) HTTP/HTTPS 요청
- 데이터베이스 쿼리의 콜백
예시) MySQL 연결 및 쿼리 실행
💡 setImmediate 콜백
- Node.js 환경에서만 사용 가능합니다.
- I/O 사이클 직후에 실행됩니다.
💡 UI 렌더링 및 이벤트 콜백 (브라우저 환경에서)
- 클릭, 키보드 입력 등 사용자 이벤트 처리
- DOM 조작 후 렌더링
💡 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의 콜백
마이크로태스크의 핵심은 현재 실행 중인 스크립트나 태스크가 완료된 직후, 그리고 다음 매크로태스크가 시작되기 전에 모두 처리된다는 것입니다.
마이크로태스크 큐에 있는 작업이 또 다른 마이크로태스크를 큐에 추가하면, 이벤트 루프는 매크로태스크로 돌아가기 전에 새로 추가된 마이크로태스크까지 모두 처리합니다.
다음 코드의 실행 결과는 어떻게 될까요? 🤔
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)은 "즉시" 실행되는 것처럼 보이지만, 실제로는 이벤트 루프의 매크로태스크 큐에 의해 지연된다는 사실 다들 이해하셨...을...거라고 믿...믿습니다?

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