React

[React] React에서 setInterval 사용

hid1 2022. 7. 22. 18:08
 

Making setInterval Declarative with React Hooks

How I learned to stop worrying and love refs.

overreacted.io

 

번역 / 리액트 훅스 컴포넌트에서 setInterval 사용 시의 문제점

Dan abramov의 https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 번역입니다.All copyrights to Dan Abramovtranslated by Jake seoTHE

velog.io

 

react 프로젝트 진행 중 timer를 구현하기 위해 setInterval을 사용해야 했다.

setInterval 사용 시 react에서 일어날 렌더링에 관한 이슈에 대해 궁금하여 검색하였고

위에 두 글을 참고하여 작성하였다.

 

 

🤔 useEffect로 구현한 setInterval  문제점


Case1

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  });

  return <h1>{count}</h1>;
}

useEffect를 이용하여 setInterval을 구현한 코드이다.

이 코드를 실행했을 때 보기엔 잘 동작해 보인다.

 

setInterval(() => {
  ReactDOM.render(<Counter />, rootElement);
}, 100);

하지만 위와 같이 더 작은 interval로 렌더링하면 작동이 안된다.

 

리액트는 컴포넌트가 렌더링 된 이후에 useEffect를 실행한다.

effect를 clean-up 하는 시점은 컴포넌트가 unmount 될 때이다.

우리가 너무 많이 re-rendering하고 effect를 재적용하면 타이밍이 어긋나 interval은 동작할 기회를 얻지 못한다.

 

위의 코드에 console.log를 작성하여 확인해 본 결과

 

 

Case2

Case1에서의 문제는 effect의 재실행보다 너무 빨리 clear 되었다는 점이다.

밑의 코드는 effect를 다시 재실행시키지 않는 방법이다.

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

 

하지만 이 코드의 문제는 useEffect가 count를 0으로 잡는다.

따라서 setInterval의 클로저가 항상 첫 렌더의 count인 0을 참조하게 되고 항상 count + 1은 1이 출력하게 된다.

 

해결방법?

setCount(count + 1)을 setCount(c => c + 1)과 같이 "update" 폼과 함께 사용하는 것이다.

이렇게 변경하면 변수가 항상 새로운 상태를 읽어들일 수 있게 된다.

하지만 새로운 props를 읽을 때는 다른 방법이 필요하다.

 

 

 

💡 Ref을 통한 해결


문제점

  • 첫 렌더에서 callback1을 가진 setInterval(callback1, delay)를 수행할 것이다.
  • 다음 렌더에서 새로운 props와 state를 거쳐서 만들어지는 callback2가 있다.
  • 하지만 시간을 재설정하지 않고서는 callback을 대체할 수 없다!

 

 

만일 interval을 전혀 변경하지 않고,

대신 변경 가능한 최근의 interval callback을 가리키는 savedCallback 변수를 도입한다면?

 

 

const savedCallback = useRef();

 function callback() {
    setCount(count + 1);
  }

  useEffect(() => {
    savedCallback.current = callback;
  });
  
   useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);

매 렌더링 시 useRef()를 통해 생성된 savedCallback.current에 실행될 callback을 등록한다.

callback은 최신의 props, state 등을 읽게 된다.

 

setInterval에서 trick 콜백 함수를 실행하게 된다면 최신의 callback을 호출할 수 있게 된다.

 

 

 

✨ useInterval hook


import React, { useState, useEffect, useRef } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

위의 코드를 바탕으로 만들어진 useInterval hook이다.

delay가 변할 때 타이머가 재 시작된다.

 

const [delay, setDelay] = useState(1000);
  const [isRunning, setIsRunning] = useState(true);

  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

null을 delay에 전달하는 것으로 interval을 일시 정지할 수 있다.

 

 

 

 

 

 

 

 

반응형