앱이 갑자기 꺼진다면?!

sumi-0011·2024년 2월 27일
1

⏰ 10MM 개발기

목록 보기
5/5
post-thumbnail

요구사항
앱 환경을 고려해 화면을 이탈하거나, 백그라운드에서도 타이머의 시간이 없어지면 안 됩니다

예상치 못하게 앱/웹이 종료되는 경우에 타이머의 시간이 없어지지 않게 하기 위해, 시간을 서버 혹은 클라이언트에 저장해야 했습니다.
그렇다면 그 시간을 어떻게 주기적으로 저장할지가 관건이었습니다.

시간 저장은 프론트에서 알아서 처리할게!

백엔드와 이야기하며,
백엔드 리소스와 API 콜을 줄이기 위해 시간 저장은 프론트에서 처리하기로 하였고,
끝내기 버튼을 눌렀을 때 API를 호출해 타이머의 시간을 백엔드에 저장하기로 하였습니다.

그러면 프론트에서 시간을 어떻게 저장할까?

시간을 어떻게 저장할까? 하는 논의에 세 가지 정도의 의견이 나왔는데요

  • 1초마다 주기적으로 스토리지에 저장하자
  • 1초는 너무 리소스가 크다. 굳이 (?) 10초 정도로 하자
  • 웹 페이지를 이탈하는 이벤트를 잡을 수 있을 것 같다.

1초마다 주기적으로 스토리지에 저장

web storage는 두 가지 종류가 있는데, session storage와 local storage가 있습니다.
시간 저장의 경우 세션이 종료되어도 유지되어야 했으므로 storage 중에서도 local storage를 사용하여야 했습니다.

1 초마다 주기적으로 storage에 저장하면 1초 간격의 정확한 시간을 저장할 수 있다는 장점이 있었습니다.
하지만, web storage 두 종류 모두 메인 스레드에서 동작해 storage에서 값을 읽고, 쓰는 동안 UI가 멈추는데, 이를 1초마다 주기적으로 하는 것은 리소스가 크다고 생각되었습니다.

2. 10초마다 주기적으로 스토리지에 저장

위에서 말했던 것과 같이 1초마다 주기적으로 스토리지에 접근하는 것은 리소스가 크다고 생각하였습니다.
저희 서비스는 초 단위로 정확하게 저장되어야 하는 기능은 아니므로, 10초에 한 번씩 스토리지에 저장하는 방법을 생각해 보았습니다.

3. 웹 페이지를 이탈하는 이벤트를 잡아, 해당 시점의 시간을 저장한다. ✅

주기적인 storage 접근이 없으므로,
웹 페이지를 이탈하는 이벤트를 잡을 수만 있다면 해당 방식이 제일 리소스가 적은 방법이라고 생각하였습니다.

"사용자가 자발적으로 화면을 이탈하는 것이 아니라, 강제로 종료되는 경우에도 이를 감지할 수 있을까?"가 걱정되었지만, 감지만 할 수 있다면 이보다 좋은 방법을 없을 것이라 생각하였고, 3번째 방식을 사용하기로 결정하였습니다.

Page Visibility API를 이용하자!

웹 페이지를 이탈하는 것을 감지하는 여러 방식 중
사용자가 웹 페이지를 보고 있는지를 알 수 있는 Page Visibility API를 선택하였습니다.

브라우저에서 다른 탭을 보고있거나, 앱이 백그라운드에서 동작하게 되면 visibilityChange 이벤트가 실행이 되고, document.hidden의 값을 통해 현재 페이지를 보고있는 것인지 감지할 수 있었습니다.

해당 방식으로 웹 페이지 이탈을 측정하는 것이 가능한지 검증이 되지 않았고, 만약 이벤트를 감지 못한다면 서비스에 큰 영향이 갈 수 있는 문제가 있었기 때문에,

최종적으로, 로컬스토리지에 주기적으로 저장 + 화면 이탈 감지 두 방식을 모두 사용하기로 결정하였습니다.

두 방식은 모두 Custom Hook을 만들어 사용하였습니다.

  useRecordMidTime(time, missionId); // 주기적인 시간 저장
  useUnloadAction(time, missionId); // 화면 이탈 감지

useRecordMidTime - 로컬스토리지에 주기적으로 저장

export function useRecordMidTime(time: number) {
  const onSaveTime = () => {
    localStorage.setItem(STORAGE_KEY.STOPWATCH.TIME_2, String(time));
  };

  useInterval(() => {
    onSaveTime();
  }, 10000);
}

10000ms -> 10s 에 한번씩 local storage에 현재 time을 저장합니다.

useUnloadAction - 화면 이탈 감지

export function useUnloadAction(time: number) {
  const onSaveTime = useCallback(() => {
    localStorage.setItem(STORAGE_KEY.STOPWATCH.TIME, String(time));
  }, [time]);

  useVisibilityState(onSaveTime);
}

function useVisibilityState(onAction: VoidFunction) {
  const onVisibilityChange = useCallback(() => {
    if (document.visibilityState === 'hidden') {
      onAction();
    }
  }, [onAction]);

  useEffect(() => {
    document.addEventListener('visibilitychange', onVisibilityChange);

    // 컴포넌트가 언마운트될 때 이벤트 리스너를 제거합니다.
    return () => {
      document.removeEventListener('visibilitychange', onVisibilityChange);
    };
  }, [onVisibilityChange]); // 빈 의존성 배열을 전달하여 이 훅이 컴포넌트가 마운트되거나 언마운트될 때만 실행되도록 합니다.
}

페이지의 visibility 상태가 hidden으로 변경되었을 때 local storage에 현재 time을 저장합니다.

이때 주의할 것은!
useVisibilityState 에서 onVisibilityChange을 빼주지 않는다면, 익명함수가 되어 이벤트가 제대로 제거되지 않는 문제가 있을 수 있었습니ㅏㄷ.

익명함수 관련 레퍼런스 ->

그래서 이벤트 콜백 함수 onVisibilityChange를 따로 정의 후, 페이지 마운트 언마운트 시점에 onVisibilityChange 이벤트리스너를 등록해주었습니다.

이 과정에서 팀원에게 아래와 같은 리뷰를 받게 되었습니다.
리뷰를 받고 나서야, 다른 사람에게 제가 짠 코드가 이해가 안 될 수도 있다는 것을 고려하지 않았다는 것을 알게되었습니다 😓


PR 링크

그래서 좀 더 자세하게 이야기한다면,

리뷰에서 이야기한 것 처럼 onVisibilityChange를 useEffect의 의존성 배열에 추가하고,
props time이 바뀌면 onVisibilityChange 함수가 바뀌고 (useCallback) onVisibilityChange 함수가 바뀌면 useEffect를 실행해 이벤트 리스너를 다시 등록하도록 수정하였습니다.

만약, 의존성 배열에 onViliityChage를 그냥 추가하면, 관련없는 부분들이 렌더링이 될 때에도 매번 함수를 다시 만들고, useEffect가 실행되는 문제가 있어 time 이 바뀔 때만 함수를 다시 만들 수 있도록 useCallback으로 감싸야했습니다.

reference

beforeunload

profile
안녕하세요 😚

0개의 댓글