React

Inactive tab인 경우, 타이머의 setInterval 동작 문제 개선

presentKey 2023. 9. 12. 23:18

타이머

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>
  );
}