setTimeout(fn, 0)을 보면 '0ms니까 지금 실행되겠지' 싶다. 그런데 콘솔에는 그 아래 줄이 먼저 찍힌다. 시계가 고장 난 게 아니다. 0은 바로 호출하라는 명령이 아니라, 타이머 콜백을 나중에 실행할 작업으로 예약할 때 주는 대기 시간이다.
setTimeout 0인데 왜 다음 줄이 먼저 실행될까?
다음 코드를 그대로 실행해 보자.
console.log('A');
setTimeout(() => console.log('D'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('B');A
B
C
DA와 B는 지금 실행 중인 코드다. setTimeout은 콜백 D를 등록하고 곧바로 돌아오므로 B가 먼저 나온다. 0을 1이나 10으로 바꿔도 현재 코드가 끝나기 전에 D가 끼어들지는 않는다.
Promise.then은 왜 타이머보다 먼저 실행될까?
위 예제의 Promise.resolve()는 이미 이행된 Promise다. 하지만 .then()의 C도 그 자리에서 실행되지는 않는다. 현재 코드의 B까지 끝난 뒤, Promise 콜백을 담는 마이크로태스크가 먼저 처리된다. 그다음 타이머 콜백을 담는 태스크로 넘어가 D가 나온다.
| 단계 | 실행되는 것 | 콘솔 |
|---|---|---|
| 현재 코드 | 첫 출력, 타이머 예약, Promise 콜백 예약, 마지막 출력 | A, B |
| 마이크로태스크 | 이미 이행된 Promise의 .then() | C |
| 다음 타이머 태스크 | setTimeout 콜백 | D |
두 콜백 모두 '비동기'라고 한 묶음으로 외우면 순서가 헷갈린다. 현재 코드 → 마이크로태스크 → 타이머 태스크로 추적하면 이 예제는 풀린다. 더 자세한 실행 흐름은 MDN의 JavaScript 실행 모델에서 태스크와 마이크로태스크 부분을 참고하자.
0ms를 작업 완료 신호로 써도 될까?
안 된다. setTimeout(fn, 0)은 타이머를 예약할 뿐, 앞서 시작한 저장·네트워크 요청이 끝났는지 모른다. '다음에 돌면 저장도 끝나겠지'라고 기대하면 가끔은 맞고 가끔은 틀린 코드가 된다. 작업이 Promise를 반환한다면 그 Promise를 기다려 완료를 확인하자.
async function saveAndNotify(save) {
await save();
console.log('저장 완료');
}이 함수는 save()가 실제 완료를 나타내는 Promise를 반환한다는 전제에서만 '저장 완료'라고 말할 수 있다. 반환값이 없는데 내부에서 따로 요청을 시작한다면 await도 기다릴 대상이 없다. 호출한 함수의 계약부터 확인해야 한다.
또한 타이머의 0ms는 정확한 실행 시각이 아니다. 현재 작업이 오래 걸리거나 실행 환경이 바쁘면 더 늦어진다. 이 글의 A → B → C → D는 간단한 한 예제의 순서이고, 모든 비동기 API의 상대 순서나 실제 경과 시간을 보장하는 공식은 아니다.
핵심 요약
setTimeout(fn, 0)은 즉시 실행이 아니라 타이머 콜백 예약이다. 현재 코드가 A, B를 끝낸 다음 이미 이행된 Promise의 마이크로태스크 C, 타이머 태스크 D 순서로 진행된다. 타이머를 작업 완료 확인용으로 쓰지 말고, 끝나야 하는 작업이 반환한 Promise를 기다리자.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

