WEB/💡 Javascript

[JavaScript] 비교적 정확한 타이머 만들기

무딘붓 2024. 8. 15. 11:13

 

자바스크립트로 정확한 타이머를 만드는 것은 어렵습니다. 어려운 이유는 무엇인지, 그리고 어떻게 비교적 정확한 타이머를 만들 수 있는지 알아보겠습니다.

 

🤔 1. setInterval()과 setTimeout()이 정확하지 못한 이유

 

JavaScript에서 타이머 기능을 구현할 때 자주 사용하는 `setInterval()`과 `setTimeout()` 함수는 브라우저 환경에서 일정 시간 간격으로 코드를 실행할 수 있게 해 줍니다.

`setTimeout()` 함수를 이용해 1초 간격으로 실행되는 타이머를 만들어 보겠습니다.

 

const INTERVAL = 1000; // 1초 간격
let startTime = Date.now();

function timerFunction() {
    const currentTime = Date.now();
    const drift = currentTime - startTime; // 경과 시간 계산
    console.log(`경과 시간: ${drift}ms`);
    setTimeout(timerFunction, INTERVAL);
}

setTimeout(timerFunction, INTERVAL);

 

 

 

경과 시간을 계산해 보면, 1000ms 간격으로 실행되지 않는 것을 볼 수 있습니다. 그리고 오차가 점점 누적되는 것도 확인할 수 있습니다. 왜 이런 오차가 나타날까요?

JavaScript는 단일 스레드에서 동작하는 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 이 작업들을 효율적으로 관리하기 위해 이벤트 루프(Event Loop)라는 메커니즘을 사용합니다. `setInterval()`과 `setTimeout()`으로 설정된 콜백 함수도 이벤트 루프가 관리합니다.

이벤트 루프의 동작 과정을 시각화하면 아래와 같습니다.

 

Event Loop 동작 과정 ( 출처 : https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif )

 

이해를 돕기 위해 아래 코드의 수행 과정을 살펴보겠습니다.

function timerFunction() {
    console.log('hello');
    setTimeout(timerFunction, 1000);
}

setTimeout(timerFunction, 1000);
  1. `setTimeout()` 함수가 call stack에 추가됩니다.
  2. `setTimeout()`이 호출되어, 콜백 함수인 `timerFunction()`이 Web API에 넘겨집니다.
  3. `setTimeout()`은 스택에서 빠져나오고, 값을 반환합니다.
  4. Web API에서 1000ms 동안 타이머가 실행된 후, 콜백 함수 `timerFunction()`이 이벤트 큐에 추가됩니다.
  5. call stack이 비어 있으면, 이벤트 루프가 `timerFunction()`을 call stack에 추가합니다.
  6. `timerFunction()`이 호출되어 'hello'가 출력되고, `timerFunction()`은 call stack에서 제거됩니다.

 


여기서 1000ms는 `setTimeout`이 설정된 시간 후에 콜백 함수가 Web API에서 이벤트 큐로 이동하는 간격을 의미합니다. 그러나 이벤트 큐에 들어간 콜백 함수가 call stack으로 이동하여 실행되는 시점은 JavaScript 엔진이 다른 작업을 처리하고 있는지에 따라 달라질 수 있습니다.

따라서, `setTimeout`으로 1000ms마다 콜백을 실행하도록 설정했다고 해도, 실제로 정확히 1000ms 후에 실행된다는 보장은 없습니다.

 

 


🕒 2. 시간 차이를 측정해서 setTimeout 오차 보정하기

 

그렇다면 `setTimeout()` 의 오차를 어떻게 줄일 수 있을까요?

여기서 소개할 방법은 시간 차이를 측정해서 보정하는 방법입니다. 이 방법은 예상 실행 시간과 실제 실행 시간의 차이를 이용합니다. 동작 방식은 다음과 같습니다.

 

  1. 다음 콜백 함수가 호출될 예상 시간 (`expectedTime`)을 계산합니다.
    (`expectedTime = 현재 시간 + INTERVAL`)
  2. 타이머 콜백 함수가 실행되면, 현재 시간과 예상 시간의 차이 (=오차)를 계산합니다.
  3. 타이머 실행 간격 (`INTERVAL`)에서 오차를 뺍니다.

예를 들어, 1000ms 간격으로 동작하는 타이머가 20ms 늦게 실행되었다면, 다음 타이머는 (1000-20=)980ms 후에 실행되도록 조정해 오차를 줄이는 방법입니다.

이 방법을 JavaScript로 구현한 코드는 다음과 같습니다.

 

const INTERVAL = 1000; // 1초 간격
let startTime = Date.now();
let expectedTime = Date.now() + INTERVAL; // 다음 실행 예상 시간

function timerFunction() {
    console.log(`경과 시간: ${Date.now() - startTime}ms`);    
    
	  const drift = Date.now() - expectedTime; // 오차 (현재 시간과 예상 시간 사이의 차이) 계산
	  expectedTime += INTERVAL; // 예상 시간을 1초 후로 계산
	  
	  // 다음 호출 시간을 오차를 이용하여 보정
	  setTimeout(timerFunction, Math.max(0, INTERVAL - drift));
}

setTimeout(timerFunction, INTERVAL);

 

`drift`에 오차를 저장하고, 다음 타이머 실행 시간을 `Math.max(0, INTERVAL - drift)`로 보정합니다. 만약 오차가 INTERVAL보다 커지면, 0ms 후에 바로 다음 타이머가 작동하게 만들어 빠르게 오차를 줄여나가도록 만듭니다.

동작 결과는 다음과 같습니다.

 

 

비록 정확히 1000ms 간격으로 동작하지는 않지만, 오차를 크게 줄일 수 있습니다.

 

오차를 계속해서 보정하기 때문에, 반복 작업에서 안정적으로 사용할 수 있다는 장점도 있습니다.

 

 


⚙️ 3. Web Worker를 사용해서 백그라운드에서도 끊기지 않는 타이머 만들기

 

시간 차이를 측정해 오차를 보정하더라도 여전히 문제가 남아 있습니다.

 

바로 다른 탭으로 이동하거나 브라우저가 백그라운드에서 동작할 때, 타이머가 느려진다는 것입니다. 아래 동작 예시를 확인해 보세요.

 

다른 탭으로 이동하면 느려지는 타이머

 

이러한 문제를 해결하기 위해 Web Worker를 사용할 수 있습니다. Web Worker는 JavaScript의 메인 스레드와 독립적으로 실행되며, 브라우저의 UI 업데이트나 다른 메인 스레드 작업에 영향을 받지 않고 백그라운드에서 작업을 처리할 수 있는 기능을 제공합니다.

 

Web Worker를 사용하여 타이머를 구현하면, 메인 스레드의 상태와 관계없이 일정한 간격으로 타이머를 실행할 수 있습니다. 이제 Web Worker를 활용한 타이머 예제를 살펴보겠습니다.

 

 

1. 먼저, Web Worker 파일을 생성합니다.

// worker.js

self.onmessage = function (e) {
  const INTERVAL = e.data.interval; // 메인 스레드에서 전달받은 인터벌 값
  let expectedTime = Date.now() + INTERVAL;

  function timerFunction() {
    const now = Date.now();
    const drift = now - expectedTime;
    expectedTime += INTERVAL;

    self.postMessage({ drift, time: now });
    setTimeout(timerFunction, Math.max(0, INTERVAL - drift));
  }

  setTimeout(timerFunction, INTERVAL);
};

 

이 파일에서는 Web Worker가 수행할 작업(타이머)을 정의합니다.

 

`self.onmessage` 이벤트 핸들러는 Web Worker가 메인 스레드로부터 메시지를 받을 때 호출됩니다. 메인 스레드에서 간격 값을 보내주면, 그 간격에 맞춰 타이머를 일정하게 동작시킵니다. 그리고 각 간격마다 `postMessage` 메서드를 사용해 메인 스레드로 시간 정보를 전송합니다.

 

 

2. 이제 메인 스레드에서 Web Worker를 호출해 타이머를 제어해 보겠습니다.

const worker = new Worker('worker.js');

worker.postMessage({ interval: 1000 }); // 1초 간격으로 타이머 실행

worker.onmessage = function (e) {
  const { drift, time } = e.data;
  console.log(`경과 시간: ${time - performance.timing.navigationStart}ms, 오차: ${drift}ms`);
};

 

`new Worker('worker.js')`를 사용해 새로운 Web Worker를 생성하면, `worker.js` 파일이 Web Worker로 실행됩니다.

메인 스레드에서 Web Worker로 메시지를 보내려면 `worker.postMessage()`를 사용합니다. Web Worker에서 메인 스레드로 메시지를 받을 때는 `worker.onmessage` 이벤트 핸들러가 호출되며, 이 핸들러는 Web Worker에서 보낸 메시지를 처리해 경과 시간과 오차를 출력합니다.

실행 예시는 다음과 같습니다.

 

다른 탭으로 이동해도 오차가 늘어나지 않는다.

 

 


📚 4. 참고자료

 

https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript

 

How to create an accurate timer in javascript?

I need to create a simple but accurate timer. This is my code: var seconds = 0; setInterval(function() { timer.innerHTML = seconds++; }, 1000); After exactly 3600 seconds, it prints about 3500 s...

stackoverflow.com

https://abhi9bakshi.medium.com/why-javascript-timer-is-unreliable-and-how-can-you-fix-it-9ff5e6d34ee0

 

Why Javascript timer is unreliable, and how can you fix it

If you are a Javascript developer, at some point in your career, you must have used setTimeout or setInterval. They are extremely handy if…

abhi9bakshi.medium.com

https://medium.com/pixo-co/%EC%9B%B9%EC%97%90%EC%84%9C-%EC%A0%95%ED%99%95%ED%95%9C-%ED%83%80%EC%9D%B4%EB%A8%B8%EB%A5%BC-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%80-f134e3766301

 

웹에서 정확한 타이머를 만드는 방법은?

How to make accurate timer in Web

medium.com

https://velog.io/@apparatus1/timer

 

[Javascript] 타이머를 구현하는 방법

Javascript로 Timer를 구현할 때 사용할 수 있는 방법에 대해 알아봅니다.

velog.io