우리의 타이머, 절대 멈추지 않는다!

sumi-0011·2024년 2월 27일
1

⏰ 10MM 개발기

목록 보기
5/5
post-thumbnail

앱과 웹 환경에서 타이머 기능을 구현하면서 겪은 이야기인데, 함께 들어보시겠어요? 😊

우리의 타이머, 절대 멈추지 않는다!

저희 팀에서 새로운 요구사항을 받았어요.

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

처음엔 '아, 간단하겠네~' 했는데, 생각보다 고민할 게 많더라고요.

백엔드 vs 프론트엔드, 누가 책임질까?

먼저 백엔드 팀과 이야기를 나눴어요.
"시간을 서버에 저장할까요, 아니면 클라이언트에서 처리할까요?" 고민 끝에 프론트엔드에서 처리하기로 했죠.
API 콜을 줄이고 싶었거든요. 대신 타이머가 끝날 때만 서버에 시간을 보내기로 했어요.

프론트엔드의 고민: 어떻게 저장할까?

자, 이제 프론트엔드에서 어떻게 시간을 저장할지 고민이 시작됐어요.
팀원들과 브레인스토밍을 했는데, 세 가지 아이디어가 나왔어요:

  1. 1초마다 저장하자! (정확하긴 한데, 좀 과하지 않을까?)
  2. 10초마다 저장하자! (1초보단 나은 것 같은데...)
  3. 페이지 나갈 때만 저장하자! (오, 이거 좋은데?)

처음엔 1초마다 저장하려고 했어요. 근데 생각해보니 UI가 버벅거릴 것 같더라고요. 10초도 괜찮아 보였지만, 뭔가 아쉬웠어요.

그러다 페이지를 나갈 때 저장하는 방법을 알게되었어요.
"이거다!" 싶었죠. 근데 또 걱정이 되더라고요. "앱이 강제로 종료되면 어쩌지?" 하는 생각이 들었거든요.

Page Visibility API, 우리의 구원자!

고민 끝에 Page Visibility API를 알게 됐어요.
이 API를 사용하면 사용자가 페이지를 보고 있는지, 아닌지 알 수 있더라고요. 이걸 이용해서 페이지를 벗어날 때 시간을 저장하기로 했어요.

하지만 혹시 모를 상황에 대비해서, 10초마다 저장하는 방법도 함께 사용하기로 했어요. 안전빵으로요! 😉

코드로 구현하기

두 가지 방법을 모두 사용하기 위해 Custom Hook을 만들었어요. 코드를 자세히 살펴볼까요?

useRecordMidTime(time, missionId); // 10초마다 저장
useUnloadAction(time, missionId); // 페이지 나갈 때 저장

useRecordMidTime - 주기적으로 저장하는 Hook

이 Hook은 10초마다 현재 시간을 localStorage에 저장해요.

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

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

여기서 useInterval은 React에서 setInterval을 안전하게 사용할 수 있게 해주는 Custom Hook이에요. 10초(10000ms)마다 onSaveTime 함수를 실행해서 현재 시간을 저장하죠.

useUnloadAction - 페이지 이탈 시 저장하는 Hook

이 Hook은 페이지 visibility가 변경될 때 현재 시간을 저장해요.

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

useUnloadActionuseVisibilityState라는 또 다른 Custom Hook을 사용해요. 이 Hook은 Page Visibility API를 이용해서 페이지가 숨겨질 때(hidden 상태가 될 때) onAction 함수를 실행해요.

useCallback을 사용한 이유는 불필요한 리렌더링을 방지하기 위해서예요. time이 변경될 때만 새로운 함수를 만들도록 했죠.

팀원의 리뷰, 새로운 깨달음

코드를 작성하고 PR을 올렸는데, 팀원에게 정말 유익한 리뷰를 받았어요.

PR 리뷰 이미지

이 리뷰를 통해 useVisibilityState Hook의 의존성 배열에 대해 다시 생각해보게 됐어요. onVisibilityChangeuseEffect의 의존성 배열에 추가하고, time prop이 바뀔 때마다 onVisibilityChange 함수가 새로 생성되도록 수정했죠.

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

  useEffect(() => {
    document.addEventListener('visibilitychange', onVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', onVisibilityChange);
    };
  }, [onVisibilityChange]); // onVisibilityChange를 의존성 배열에 추가
}

이렇게 수정하면 time이 변경될 때마다 정확하게 최신 시간이 저장되면서도, 불필요한 리렌더링은 방지할 수 있어요.

마무리하며

이번 경험을 통해 정말 많은 것을 배웠어요. Custom Hook 작성 방법, React의 성능 최적화 기법, 그리고 무엇보다 팀 협업의 중요성을 몸소 느꼈죠.

코드를 작성할 때는 단순히 '작동하는' 코드를 만드는 것을 넘어서, 다른 개발자들의 관점에서도 이해하기 쉽고 유지보수가 용이한 코드를 작성해야 한다는 걸 깨달았어요. 팀원의 리뷰 덕분에 제 코드가 한 단계 더 발전할 수 있었죠.

이런 작은 경험들이 모여 우리를 더 나은 개발자로 만들어가는 것 같아요.
앞으로도 이런 도전적인 과제들을 마주하며 성장해 나가고 싶습니다.
여러분도 개발하면서 겪은 재미있는 경험이 있다면 언제든 공유해주세요! 함께 배우고 성장하는 게 개발의 묘미 아니겠어요? 😊🚀

profile
안녕하세요 😚

0개의 댓글