useInterval과 useRequestAnimationFrame

babypig·2023년 5월 29일
3

React.js

목록 보기
7/8
post-thumbnail

📝 시작전 tmi

3월부터 본격적으로 시작하게된 next.js 프로젝트가 종료되었다.
리액트의 경험도, 타입스크립트의 경험도 없었지만 3월에 next.js 프로젝트에 들어가게되어
2월동안 급하게 리액트 강의와 타입스크립트 강의 및 디자인 패턴에 관한 공부를하였다,
프로젝트를 진행하면서 바쁜거도 바쁜거지만 퇴근 후 컴포넌트 학습과, 오늘의집을 클론코딩하기로
마음먹고 nest.js를 학습하여 현재도 클론 코딩중에 있다,,,api와 프론트 단 모두 구현하려니 생각보다
더디긴 하지만 그래도 정해진 일정이 있는건 아니니 조금 더 코드에 대하여 고민하면서 학습할수있는게
현재로서는 너무 만족스럽다.

🤔 이 글을 쓰게된 계기

클론코딩을 진행중 회원가입에 타이머 인증을 구현하다가 오늘의집에 타이머가 브라우저에서 다른일을 하면,
시간이 제대로 동작하지않는 문제점을 발견했다.
찾아본 결과 타이머는 정확하지 않으며, 1초마다 실행되길 원했지만 실제로는 그보다 훨씬 긴 간격으로 실행되며, 1초라고 지시한건 “1초마다 실행해”라는 뜻이 아니고, “1초동안은 실행하지 마“라는 뜻이다.
실제로 실행되는 주기는 1.8초일수도 있고 2.5초일수도,,그러니 여러번 반복하면 3분이 아니고 6분일지 9분일지 알 수 없다는걸 알게되었다.

🖥️ 코드 살펴보기

import { useEffect, useLayoutEffect, useRef } from 'react';

const useInterval = (callback: () => void, delay: number | null) => {
  const savedCallback = useRef(callback);
  useLayoutEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (!delay && delay !== 0) return;

    const id = setInterval(() => savedCallback.current(), delay);

    return () => clearInterval(id);
  }, [delay]);
};

export default useInterval;

useInterval 같은 경우에는 검색해보면 많은 내용이 나와있다.

요약 하자면,

  1. useRef 훅을 사용하여 savedCallback이라는 변수를 생성 후 callback 함수를 참조하는 역할
  2. useLayoutEffect 훅을 사용하여 callback 함수가 변경될 때마다 savedCallback.current 값을 업데이트하고 최신의 callback 함수를 유지
  3. useEffect 훅을 사용하여 delay 값이 변경될 때마다 타이머를 설정하고 해제, delay 값이 null이거나 0이면 타이머를 설정하지 않기
    • setInterval 함수를 사용하여 savedCallback.current 함수를 주어진 delay 간격으로 반복 실행하고 이 함수는 타이머 식별자 id를 반환함.
    • 반환된 id를 사용하여 clearInterval 함수를 호출하여 타이머를 해제함.

하지만 내가 의도한건 시간보장이 된다고 생각했지만, 여전히 여러가지 테스트를 거쳤을때 완벽하게
시간이 보장되지 않았다, 그래서 requesAnimationFrame을 이용하면 어떨까 생각해보았다.

import { useEffect, useRef } from 'react';

const useRequestTimer = (callback: (deltaTime: number) => void) => {
  const requestRef = useRef<number | null>(null);
  const previousTimeRef = useRef<number | undefined>();

  const animate = (time: number) => {
    if (previousTimeRef.current != undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime);
    }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => {
      if (requestRef.current) {
        cancelAnimationFrame(requestRef.current);
      }
    };
  }, []);
};

export default useRequestTimer;

요약 설명

  1. useRef 훅을 사용하여 requestRef와 previousTimeRef를 선언합니다. 이를 통해 변수를 유지하고, 값이 변경되어도 리렌더링을 하지않는다.
  2. animate 함수는 requestAnimationFrame을 사용하여 애니메이션 프레임마다 호출하고 time 인자는 현재 시간을 나타내며, previousTimeRef를 사용하여 이전 시간과의 차이인 deltaTime을 계산한다.
  3. animate 함수 내에서 callback 함수를 호출하여 deltaTime을 전달
  4. 초기화 단계에서는 requestAnimationFrame(animate)을 호출하여 애니메이션을 시작하고 정리 단계에서는 cancelAnimationFrame을 사용하여 애니메이션을 중지한다.
  5. 의존성 배열 []을 전달하여 useEffect가 초기화 시에만 실행되게 한다.

2개의 가장 큰 차이점은 useRequestTimer 훅은 requestAnimationFrame을 사용하여 애니메이션 프레임마다 호출되는 방식으로 타이머를 구현하고 useInterval 훅은 setInterval을 사용하여 주어진 시간 간격마다 호출되는 방식으로 타이머를 구현한다.

즉 , requestAnimationFrame은 브라우저의 리플로우와 리페인트 주기에 맞춰 호출되므로, 브라우저의 최적화된 프레임 주기(초당 60프레임)에 따라 작업이 실행되고 setInterval은 브라우저의 일반적인 타이머 메커니즘에 의존한다.

📝 정리

codesandbox를 통하여 useRequestTimer를 사용한 타이머와 useInterval을 사용한 타이머를 비교해보았을때 useRequestTimer가 더 정확한 타이머를 구현하는거 처럼 보였다. 하지만 requestAnimation은
디스플레이 장치의 주사율을 따라가며, 이는 높은 주사율을 가진 컴퓨터에서는 더 정확한 타이머를 구현할 수 있으며, 주사율이 낮을수록 정확성이 떨어질 수 있다는 문제점이 있다. 다른 성능의 컴퓨터에서 테스트를 하지는 못하여 실제로 어떻게 다르게 작동할지는 직접 눈으로 확인하지는 못햇다.
결론은 2개 다 내가 만족할만한 결과를 이루지는 못했다. 다음 대안은 date객체의 getTime()메서드를 이용해야 비교적 정확한 카운트가 가능하다니 useInterval과 조합하여 새로운 hooks을 만들고 테스트 해볼 예정이다.

profile
babypig

0개의 댓글