맨틀 이야기

Page Visibility API 본문

카테고리 없음

Page Visibility API

jbilee 2024. 5. 7. 15:22

Image by Moshe Harosh from Pixabay

 

setInterval 함수를 사용해 카운트다운 타이머를 구현한 내 웹앱을 모바일에서 실행했을 때 치명적인 오류를 발견했다.

 

타이머를 실행한 상태에서 모바일 기기의 화면을 끄면, 화면이 꺼진 상태로 있는 동안에는 setInterval 함수도 멈춰버리는 것이다.

 

당연히 화면이 꺼진 상태에서도 setInterval 함수로 인해 카운트다운이 계속 진행되고 있을 거라고 생각했는데, 실제로는 타이머의 시간이 흐르지 않고 멈춘 상태였다가 모바일 기기의 화면이 다시 켜지면 그때 이어서 동작하도록 돼있었다.

 

찾아보니 모바일 기기에서는 배터리 수명 보존과 퍼포먼스 향상을 위해 setInterval, setTimeout과 같은 시간과 관련된 함수들을 스로틀링한다고 한다. 화면이 꺼져있는 동안에는 동작을 일시정지 시켰다가, 화면이 다시 켜졌을 때 이어서 동작하도록 한다는 것이다.

 

타이머 앱을 만드는 경우 화면이 꺼진 상태에서도 흐르는 시간은 계속 트랙킹해야 하기 때문에, 위와 같은 문제에 대한 대안책을 알아보다가 Page Visibility API에 대해 알게 됐다.

 

Page Visibility API란?

Page Visibility API는 현재 사용자가 보고 있는 페이지가 active한 상태인지 아닌지를 파악할 수 있도록 도와주는 웹 API다.

 

데스크톱일 경우 브라우저의 다른 탭으로 이동했는지, 모바일일 경우 다른 탭으로 이동했거나 기기의 화면이 꺼졌는지 여부를 확인할 수 있다. API가 동작하고 있는 페이지의 focus 상태가 바뀔 때마다 "visibilityChange" 이벤트가 발동하고, 난 리스너의 콜백 함수로 해당 탭을 사용자가 보고 있는지 아닌지에 따라 대응하는 함수를 호출하면 된다.

window.addEventListener("visibilitychange", () => {
  if (isPaused.current && pauseTime.current) resumeTimer();
  else pauseTimer();
});

 

타이머 앱에서는 visibility가 바뀔 때 타이머를 일시정지시키거나 재가동하도록 구현했다.

 

리액트 앱이기 때문에 타이머의 일시정지 상태를 알리는 boolean 값을 useRef 상태에 저장해 사용했고, 또 모바일 환경일 때만 필요한 기능이라 모바일 환경일지 식별하는 코드도 추가했다.

 

모바일 화면이 꺼졌을 때의 시간을 따로 저장한 다음 유저가 모바일 기기를 다시 켰을 때의 시간에서 차감하는 계산식까지 추가하니 아래 결과물이 나왔다:

useEffect(() => {
    if (!isMobile()) return;

    const pauseTimer = () => {
      if (!currentInterval.current) return;
      pauseTime.current = Date.now();
      clearInterval(currentInterval.current);
      isPaused.current = true;
    };

    const resumeTimer = () => {
      const resumeTime = Date.now();
      timeLeft.current -= Math.round((resumeTime - pauseTime.current!) / 1000);
      updateTime(); // 남은 시간을 즉시 UI에 그리도록 함
      if (timeLeft.current <= 0) {
        isPaused.current = false;
        return changeMode();
      }
      isPaused.current = false;
      startTimer();
    };

    window.addEventListener("visibilitychange", () => {
      if (isPaused.current && pauseTime.current) resumeTimer();
      else pauseTimer();
    });
  }, []);

 

 

참고