타이머 기능을 구현해야 한다면, 다들 setInterval
을 이용해 구현하는 방법을 떠올릴 것이다.
1초마다 반복적으로 time
변수에서 1씩 빼면 되기 때문이다.
하지만 setInterval
이외에도 타이머를 구현할 수 있는 방법은 다양하다.
그래서 한 번 다양한 타이머를 구현해보고 정리하고자 한다.
타이머에 대한 코드는 github에서 확인해볼 수 있다.
useEffect(() => {
const handleTimer = () => {
const now = Date.now();
const delay = now - startTimestamp.current;
// 이전 시간(startTimestamp)과 현재 시간(now)의 차이(delay)를 구하여 time값에서 얼마나 뺄지 구한다.
const delayedSec = Math.max(1, Math.floor(delay / 1000));
setTime((prev) => prev - delayedSec * 1000);
startTimestamp.current = now;
};
intervalId.current = setInterval(handleTimer, 1000);
return () => {
if (intervalId.current) clearInterval(intervalId.current);
};
}, []);
화면에 찍히는 delay값을 보면, 정확히 1초(=1000ms)마다 함수가 실행되지는 않는다. 대신 가능한 한 1000ms에 근접하게 실행된다. 가장 무난한 타이머를 만드는 방법이다.
++) 코드를 보면, 단순히 1초(=1000ms)씩 빼는 것이 아니라 delay된 시간을 계산해서 빼고 있다.
const delayedSec = Math.max(1, Math.floor(delay / 1000));
해당 이유는 다음과 같다.
자바스크립트에서, setTimeout
이나 setInterval
과 같은 타이머는 콜스택에 직접 쌓이는 것이 아니라 브라우저의 Web API에서 관리된다. 이후 지정된 시간이 끝나면 태스크 큐로 이동하고, 콜스택이 비어있을 때 이벤트 루프가 해당 콜백을 실행한다.
만약 콜스택에 실행 시간이 긴 작업이 있다면?
타이머에 지정된 시간(1000ms)이 지나도 콜백 함수는 즉시 실행되지 못한다. 콜스택에 아직 작업이 남아있기 때문이다.
따라서 1000ms가 지날 때마다 time값에서 1씩 빼는 것은, 타이머의 정확성을 떨어트린다.
타이머 지연 버튼을 클릭하면, 약 3초 정도 시간이 걸리는 작업이 실행된다. 따라서 콜백 함수에서 단순히 time 값에서 1만 빼도록 설계했다면, 실제 경과 시간과 타이머에 표시되는 시간에 차이가 발생한다.
따라서 이러한 오차를 해결하기 위해 delayedSec
를 별도로 계산하여 실제 경과 시간을 반영하도록 했다. 정확한 타이머를 만들어야 한다면, 이런 부분도 고려하는 것이 좋을 것이다.
useEffect(() => {
const handleTimer = () => {
const now = Date.now();
const delay = now - startTimestamp.current;
const delayedSec = Math.max(1, Math.floor(delay / MILLISECOND));
setTime((prev) => prev - delayedSec * 1000);
startTimestamp.current = now;
// 재호출
timeoutIdRef.current = setTimeout(handleTimer, 1000);
};
timeoutIdRef.current = setTimeout(handleTimer, 1000);
return () => {
if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
};
}, []);
연쇄적으로 setTimeout
을 호출한다.
setTimeout
을 이용하여 타이머를 만드는 방법은 추천하지 않는다. 왜냐하면 시간 지연 문제가 발생하기 때문이다. 시간 지연 문제에는 크게 두 가지 원인이 있다.
setTimeout
은 Web API에서 처리된 후 태스크 큐로 이동하며, 콜스택이 비어있을 때만 콜백 함수를 실행한다.
연쇄적으로 setTimeout
을 호출하는 경우 콜백 함수의 실행 시간만큼 다음 타이머 설정이 밀리게 된다.
결과적으로 매 실행마다 태스크 큐 대기 시간 + 콜백함수 실행 시간
만큼의 지연이 발생하므로, (1000 + α)
ms마다 타이머가 업데이트된다. 이러한 지연은 시간이 지날수록 누적되므로, 나중에는 실제 시간과 상당한 차이가 발생하게 된다.
이러한 누적 오차 때문에 setTimeout
으로 타이머를 구현하면 정확한 시간 측정이 어렵고, 타이머로서의 신뢰성이 떨어지게 된다. 따라서 정확한 타이머 구현을 위해서는 setTimeout보다는 setInterval
을 사용하자.
규리님~ 잘지내시죠~?
다음편 웹워커랑 리퀘스트애니메이션프레임도 기대할게여 ㅎㅎ