setInterval을 이용하여 타이머를 구현했었는데, 타이머를 등록하고 브라우저의 다른 탭으로 이동하면 시간이 제대로 맞지 않는 문제가 있었습니다.
https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
그 이유는 브라우저가 백그라운드 탭의 부하를 줄이기 위해 setInterval이 지연 실행되고 있었습니다.
▶ 문제 해결
이전에는 단순히 setInterval을 1초 간격으로 실행해서 타이머가 늦어지는 문제가 발생했습니다.
시작 시점과 현재 시점의 경과된 시간을 Date.now()를 이용하여, 백그라운드 탭에서 활성 탭으로 돌아왔을 때 타이머 시간이 제대로 계산되도록 했습니다.
Date.now() 경과 시간 참고
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
export default function Timer({ timer }: Props) {
const { handleRemoveTimer } = useTimerList();
const time = useRef(parseInt(timer.time, 10) * 60); // timer.time(5, 25 등 분 시간), 25분 * 60 = 1500초
const intervalId = useRef<number | undefined>(undefined);
const [clock, setClock] = useState({
min: time.current / 60, // 분 계산
sec: time.current % 60, // 초 계산
});
const handleStartTimer = useCallback(() => {
const startedAt = Date.now(); // 시작 시점
intervalId.current = window.setInterval(() => {
const elapsedTime = Math.floor((Date.now() - startedAt) / 1000); // 경과 시간 계산
setClock({
min: (time.current - elapsedTime) / 60,
sec: (time.current - elapsedTime) % 60,
});
if (time.current - elapsedTime < 0) { // 타이머 시간 종료
clearInterval(intervalId.current);
time.current = 0;
setClock({ min: 0, sec: 0 });
}
}, 1000);
}, []);
const handleResetTimer = useCallback(() => { // 타이머 재시작
clearInterval(intervalId.current);
time.current = parseInt(timer.time, 10) * 60;
handleStartTimer();
}, [handleStartTimer, timer.time]);
useEffect(() => {
handleStartTimer(); // 타이머 시작
return () => clearInterval(intervalId.current); // Timer unmount
}, [handleStartTimer]);
return (
<li
className={`${styles.timer} ${time.current <= 0 && styles['is-alarm']}`}
>
<div className={styles.info}>
<div className={styles['image-wrap']}>
<Image
className={styles.image}
src={`/images/monsters/${timer.monsterName}.png`}
alt={`타이머 ${timer.monsterName} 이미지`}
width={36}
height={36}
/>
</div>
<span className={styles.time}>
{`${Math.floor(clock.min).toString().padStart(2, '0')} :
${clock.sec.toString().padStart(2, '0')}`}
</span>
</div>
<div className={styles.buttons}>
<button
className={styles.reset}
type='button'
title='시간 초기화'
onClick={handleResetTimer}
>
<TimerResetIcon />
</button>
<button
className={styles.close}
type='button'
title='타이머 삭제'
onClick={() => handleRemoveTimer(timer.monsterName)}
>
<RemoveIcon />
</button>
</div>
</li>
);
}