본문 바로가기

javascript/pure

[자바스크립트] setTimeout이 정확한 시간을 보장하지 않는 이유

 

setTimeout() - Web API | MDN

전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 실행하는 타이머를 설정합니다.

developer.mozilla.org

 자바스크립트는 특정 시간 이후, 또는 시간 간격으로 작업을 진행할 수 있도록 setTimeout, setInterval과 같은 함수를 대부분의 환경에서 지원한다. 이러한 함수들은 지연 시간 및 콜백을 입력받으며, 지연 시간 이후 콜백을 실행하는 동작 방식을 가지고 있다. 이때 콜백 함수는 정확히 지연 시간 이후에 실행되는 것이 아니라, 최소 지연시간을 대기한 후 실행되는 특성을 가지고 있다. 그렇다면 자바스크립트는 왜 정확한 지연 기능을 제공하지 않을까?

const start = Date.now();
console.log("before wait: ", start);

setTimeout(() => {
    const end = Date.now();
    console.log("after wait: ", end);
    console.log(`time gaps: ${end - start}ms`);
}, 100);

let i = 0;

while(i < 10000){
    console.log(i);
    i++;
}

위 코드는 setTimeout을 100ms으로 등록한 후 while문을 통해 0부터 9999까지 숫자를 출력한다. 혹자는 while문이 동작하다가 정확히 100ms의 시간이 지난 이후 콜백 함수가 실행되기를 기대할지도 모르겠다. 결과를 통해 살펴보자.

(좌) nodejs 환경, (우) 크롬 콘솔 환경

100ms라는 예상과는 달리 467ms, 301ms이라는 최소 3배 이상의 시간 차이가 발생했다. 대체 이유가 무엇일까?

자바스크립트의 동작 방식은 single threaded non blocking이라는 표현으로 묘사된다. 표현에서 드러나듯, 자바스크립트는 메인 스레드 하나를 기반으로 동작한다. 덕분에 자바스크립트 기반 프로그램을 구현할 때 사용자는 멀티 스레딩 환경에서 발생하는 온갖 복잡한 (데드락, 동기화 문제 등) 요소를 덜 신경 쓸 수 있게 된다.

 그러나 싱글 스레드 기반으로 동작한다는 의미는 곧 비동기 작업을 수행할 수 없다는 의미가 된다. 그런데, 우리는 웹 브라우저를 사용하면서 자바스크립트가 싱글 스레드 기반으로 동작한다는 사실을 인식하기는 쉽지 않다. 당연한게, 자바스크립트 언어 자체는 싱글 스레드 기반으로 동작하지만 자바스크립트 환경은 멀티 스레드 기반으로 동작하기 때문이다.

nodejs 공식 문서에서 제공하는 nodejs의 이벤트 루프 구조
mdn에서 표현한 이벤트 루프

 자바스크립트의 메인 스레드는 루프를 돌면서 들어오는 태스크를 처리한다. 루프의 구체적인 구조는 자바스크립트가 동작하고 있는 환경의 구현 사항에 따라 크게 다를 수 있지만, 공통적으로 I/O와 같은 긴 시간을 요구하는 작업 등을 비동기적으로 수행할 수 있도록 다른 API에게 맡긴다. API들은 할당된 작업을 수행한 후 콜백을 별도의 큐(마이크로 태스크 큐, 매크로 태스크 큐 등)에 더한다. 그동안 메인 스레드는 계속 루프를 돌면서 다른 작업을 수행하거나, 특정 이벤트의 발생을 주기적으로 검사한다. 이후 메인 스레드에 할당된 작업을 모두 수행하면 이벤트 루프는 태스크 큐에 쌓인 작업 목록을 메인 스레드로 가져와 실행한다.

 위에서 설명한 것처럼 자바스크립트 동작 상 setTimeout의 콜백은 태스크 큐에 들어가는 다른 콜백이나 프로미스, 심지어 메인 스레드에서 블록킹을 유발하는 작업에 의해서도 지연될 수 있다. 따라서 지연 시간은 정확한 종료시간이 아니다.