ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] React에서 setInterval 사용
    React 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을 일시 정지할 수 있다.

     

     

     

     

     

     

     

     

    반응형

    댓글

Designed by Tistory.