개발자
류준열
setTimeout은 정확한 타이밍 측정을 하지 못한다.
타이머 구현
setTimeout으로 타이머를 걸어서 1분 후 모달이 뜨는 기능을 만들었다. 그런데 조금 찾아보니 setTimeout은 call stack이 비는것을 대기하느라 타이밍을 못맞추게 되는 경우가 있다. (MDN 참고)
왜 setTimeout은 정확한 타이밍 보장을 못할까?
싱글스레드인 JS가 논블로킹을 구현하는 방법
JS는 싱글스레드이다. 싱글스레드인 JS는 한번에 한가지 일만 처리하기 때문에 필연적으로 병목이 생기는데 블로킹을 만들지 않고 논블로킹을 구현할 수 있는 이유는 JS런타임(Web API)를 이용한 비동기처리 덕분이다.
console.log("첫 번째");
setTimeout(() => console.log("세 번째 (4초후)"), 4000);
console.log("두 번째");
// =============콘솔 결과=============
// 첫 번째
// 두 번째
// 세 번째 (4초후)
JS 런타임이란 JS가 구동되는 환경을 뜻하며 크롬, 파이어폭스, NodeJS등이 있다. (크롬, NodeJS는 V8엔진을 사용한다.)
이벤트 루프
스택과 비동기작업
JS는 스택구조로 작업들을 처음부터 끝까지 한번에 처리한다.
function inner() {
return "완료";
}
function outer() {
return inner();
}
console.log(outer());
outer()를 실행하면 outer를 스택에 올리고 곧 이어 inner를 올린다. inner가 '완료'를 반환하면 스택에서 빠지고 그 다음 outer도 빠지면서 스택이 비게 된다.
이때 스택이 비게 되는 순간 이벤트 루프가 실행 가능한 비동기 작업들을 스택에 밀어넣는다.
만약 while문이 100s를 소요하면 100s동안 스택이 비지 않기 때문에 화면은 멈춘다. 이벤트 루프에게 차례가 돌아오지 않기 떄문이다. JS의 과한 로딩이 TBT(Total Blocking Time)에 영향을 주는 이유도 이 때문이다.
console.log("시작");
setTimeout(() => console.log("타이머 실행됨"), 0);
for (let i = 0; i < 3; i++) {
console.log("반복", i);
}
console.log("끝");
// =============콘솔 결과=============
// 시작
// 반복 0
// 반복 1
// 반복 2
// 끝
// 타이머 실행됨
위 코드에서 setTimeout에 0초를 걸어놔도 '타이머 실행됨'은 가장 나중에 실행된다. 이유는 setTimeout을 스택에 저장하지 않고 console.log와 반복문부터 stack에서 처리하기 때문이다.
즉 비동기 코드는 스택이 빌때까지 대기할 수 밖에 없고 그래서 setTimeout에 0초를 걸었지만 즉시 실행되지 않는 것이다.
따라서, setTimeout의 delay는 최소 지연 시간일뿐 정확한 타이밍 보장이 아니다.
도식화

- Call Stack에 setTimeout이 들어오면 Web API에 setTimeout을 위임하고
- n초후 setTimeout의 callback은 Task Queue에 들어가 실행 가능한 상태가 된다.
- Call Stack이 모두 완료되기만을 기다린다.
- 그 이후 Call Stack이 비었을때 Task Queue에서 대기하던 setTimeout의 callback을 처리한다.
이때 3~4 과정에서 n초가 지났는데도 Call Stack에 무언가 남아있으면 setTimeout은 Queue에서 계속 대기하게 되면서 타이밍을 놓칠 수 있다. (참고)
callback의 실제 실행 시점은 현재 실행 중인 작업의 종료 시점에 따라 달라질 수 있다.