본문 바로가기

Lab Notes

setTimeout의 정확성

settimeout 함수를 아래처럼 실행하면 지정한 시간 이후에 콜백함수를 실행합니다.

setTimeout(function() { console.log("3초가 지났습니다."); }, 3000);
setTimeout(function() { console.log("5초가 지났습니다."); }, 5000);
setTimeout(function() { console.log("7초가 지났습니다."); }, 7000);

 

만약, 동일한 시간으로 설정한 settimeout 함수 여러개를 호출하게 된다면 지정한 시간에 맞춰 모든 콜백 함수가 지정한 시간에 맞게 실행이 될까요?

// 현재 시간을 기준으로 로그를 찍기 위한 초기 시간
const startTime = Date.now();

// 1초 후에 실행되는 여러 setTimeout 함수
setTimeout(() => {
    console.log(`Callback 1 executed at ${Date.now() - startTime}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 2 executed at ${Date.now() - startTime}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 3 executed at ${Date.now() - startTime}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 4 executed at ${Date.now() - startTime}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 5 executed at ${Date.now() - startTime}ms`);
}, 1000);

 

위 코드는 5개의 setTimeout을 모두 1초(1000밀리초) 후에 실행하도록 설정합니다. 각 콜백은 실행될 때 현재 시간에서 시작 시간을 빼서 실행 시간을 밀리초 단위로 출력합니다. 이를 통해 각 콜백이 실제로 얼마나 정확한 시간에 실행되는지 확인할 수 있습니다.

 

테스트 결과,

"Callback 1 executed at 1002ms"
"Callback 2 executed at 1002ms"
"Callback 3 executed at 1002ms"
"Callback 4 executed at 1003ms"
"Callback 5 executed at 1003ms"

모든 콜백 함수는 예상했던 대로 대략 1초 후에 실행되었지만, 완벽하게 정확한 1000밀리초가 아니라 1002밀리초와 1003밀리초에 실행된 것을 확인할 수 있습니다. 이러한 차이가 나는 이유는 무엇일까요?

 

다양한 이유가 있겠지만 그 중에도 이벤트 루프와 비동기 처리 방식도 관계도 있을 것 같습니다. setTimeout 함수는 지정된 딜레이 후에 콜백 함수를 비동기적으로 실행하도록 예약합니다. 이 지정된 시간은 콜백 함수가 실행되기 시작하는 최소 지연 시간을 의미하며, 반드시 그 시간에 실행될 것임을 보장하지 않습니다. 콜백 함수의 실행은 지정된 딜레이 이후 가능한 가장 빠른 시점에 이루어지지만, 이벤트 루프가 콜 스택을 확인하고 현재 실행 중인 다른 작업들이 모두 완료된 후에야 실행될 수 있습니다.

In My Opinion!!
콜 스택에 태스크가 비어있는 상황이라고 해도 콜백이 실행되기까지는 이벤트 루프가 태스크 큐를 체크하는 시점과 콜 스택으로의 이동에 소요되는 시간도 고려한다면 오차는 발생할 수 밖에 없을 것 같아요

 

그렇다면 어떻게 해야 정확 또는 가깝게 동일한 콜백함수를 실행할 수 있을까요?

 

1. 단일 setTimout

// 현재 시간을 기준으로 로그를 찍기 위한 초기 시간
const startTime = Date.now();

// 동일 시간에 실행되도록 설정된 여러 setTimeout
const delay = 1000; // 지연 시간 1초 (1000밀리초)
const numberOfTimeouts = 5; // setTimeout의 개수

for (let i = 1; i <= numberOfTimeouts; i++) {
    setTimeout(() => {
        console.log(`Callback ${i} executed at ${Date.now() - startTime}ms`);
    }, delay);
}

 

테스트 결과,

"Callback 1 executed at 1002ms"
"Callback 2 executed at 1002ms"
"Callback 3 executed at 1002ms"
"Callback 4 executed at 1002ms"
"Callback 5 executed at 1002ms"

대략 1초에 실행되었지만 모든 콜백 함수가 동일한 시간에 호출된 것을 확인할 수 있습니다.

 

2. Web worker

브라우저 환경에서 Web Workers를 사용하면 메인 스레드와는 독립적인 백그라운드 스레드에서 스크립트를 실행할 수 있습니다. 이 방법은 복잡한 계산이나 시간이 많이 걸리는 작업을 메인 스레드에서 분리하여 실행할 수 있지만, DOM 업데이트는 할 수 없으며 메인 스레드와의 통신은 메시지 전달을 통해 이루어집니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Web Worker Timeout Test</title>
</head>
<body>
    <script>
        if (window.Worker) {
            const workerCount = 5; // 5개의 워커 생성
            const delay = 1000; // 모든 워커에 대한 지연 시간은 1000밀리초

            for (let i = 1; i <= workerCount; i++) {
                const worker = new Worker('worker.js');

                worker.postMessage({ id: i, delay: delay });

                worker.onmessage = function(event) {
                    console.log(`Worker ${event.data.id} executed at ${event.data.time}ms`);
                };

                worker.onerror = function(error) {
                    console.error(`Worker Error: ${error.message}`);
                };
            }
        } else {
            console.log("Your browser doesn't support web workers.");
        }
    </script>
</body>
</html>
// worker.js
self.onmessage = function(event) {
    const { id, delay } = event.data;
    setTimeout(() => {
        const executionTime = performance.now(); // Web Workers에서는 Date.now() 대신 performance.now() 사용 가능
        self.postMessage({ id: id, time: executionTime });
    }, delay);
};

 

테스트 결과,

"Worker 1 executed at 1007.4000000059605ms"
"Worker 2 executed at 1006.4000000059605ms"
"Worker 4 executed at 1010.5999999940395ms"
"Worker 3 executed at 1010.5ms"
"Worker 5 executed at 1009.5ms"

동일하게 지연되는 걸 확인 할 수 있습니다.

 

3. performance.now()

높은 정밀도가 요구되는 타이밍 작업을 위해서는 performance.now()와 같은 더 정밀한 타이머를 활용할 수 있습니다. performance.now()는 페이지 로드 이후의 시간을 밀리초 단위로 제공하며, Date.now()보다 높은 정밀도를 제공합니다.

// 현재 시간을 기준으로 로그를 찍기 위한 초기 시간
const startTime = performance.now();

// 1초 후에 실행되는 여러 setTimeout 함수
setTimeout(() => {
    console.log(`Callback 1 executed at ${(performance.now() - startTime).toFixed(2)}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 2 executed at ${(performance.now() - startTime).toFixed(2)}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 3 executed at ${(performance.now() - startTime).toFixed(2)}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 4 executed at ${(performance.now() - startTime).toFixed(2)}ms`);
}, 1000);

setTimeout(() => {
    console.log(`Callback 5 executed at ${(performance.now() - startTime).toFixed(2)}ms`);
}, 1000);

 

테스트 결과,

"Callback 1 executed at 1000.50ms"
"Callback 2 executed at 1000.70ms"
"Callback 3 executed at 1000.80ms"
"Callback 4 executed at 1000.90ms"
"Callback 5 executed at 1001.00ms"

Date.now() 보다 낮은 오차로 콜백 함수가 호출된 것을 확인할 수 있습니다.

 

결국, setTimeout은 다양한 외부 요인에 의해 그 정밀도에 한계가 있습니다. setTimeout의 사용이 불가피한 상황에서는 가능한 한 오차를 최소화하는 방법을 찾아보는 것도 하나의 방법일 것 같습니다.

 

'Lab Notes' 카테고리의 다른 글

promise.all & promise.allsettled 이해하기  (0) 2024.04.09
BigInt vs Number  (0) 2023.11.10
HTML vs XML  (0) 2023.11.09