[JavaScript/React] React에서 setInterval의 활용

정다은·2022년 10월 20일
1
post-thumbnail

[참고] setInterval() - Web APIs | MDN
[참고 #1] Dan Abramov, Making setInterval Declarative with React Hooks
[참고 #2] 번역 / 리액트 훅스 컴포넌트에서 setInterval 사용 시의 문제점
[참고 #3] React에서 setInterval 사용하기

📌 JavaScript의 setInterval() 함수

  • 고정된 시간 지연(delay)마다 반복적으로 함수를 호출하기 위해 활용
  • 인터벌을 고유하게 식별하는 interval ID를 반환하며 이를 clearInterval() 함수를 통해 제거할 수 있음
  • setInterval() 함수의 Parameters
    • func: delay 밀리초마다 실행될 콜백 함수
    • delay (optional): 함수를 실행시킬 간격을 나타내는 밀리초로 옵셔널 값으로 지정되지 않을 경우 기본값은 0
    • arg0, ..., argN (optional): 옵셔널 값으로 콜백함수의 인자로 넘어가는 인자들
// 3초(=3000밀리초) 간격으로 callbackFunc을 실행
// callbackFunc에 첫 번째 인자로 "first", 두 번째 인자로 "second" 값을 넘겨줌
const intervalID = setInterval(callbackFunc, 3000, "first", "second");

function callbackFunc(a, b) {
  console.log(a); // first
  console.log(b); // second
}

clearInterval(intervalID); // clearInterval() 실행 시 반복 호출이 멈춤

🚨 React에서 setInterval() 함수를 사용할 때 발생하는 문제

  • 리액트와 자바스크립트의 근본적인 차이에서 발생한 문제
    • 리액트 컴포넌트의 state와 props계속해서 변화할 수 있는 값이며, 해당 값들이 변화할 경우 리액트 컴포넌트는 새로 변경된 값을 가지고 리렌더링을 발생시킨다
    • 자바스크립트의 setInterval() 함수는 한 번 설정되면 clearInterval() 함수를 호출하지 않는 이상 변화되지 않으며 초기의 값을 계속해서 기억하고 참조한다

💡 React의 useEffectJavaScript의 setInterval를 활용하여 3초마다 count라는 상태 값이 1씩 증가하는 코드를 작성하려는 시도를 해보자

  • #1 의존성 배열 ❌ (매 렌더링마다 실행)

    • 0, 1, 2, 3, ... 과 같이 count 값이 증가하며 제대로 동작하는 듯 보임
    • clearInterval()setInterval()의 호출 타이밍이 어긋날 수 있는 문제 존재
    • 타이머 간격이 작아지거나 너무 많은 리렌더링이 일어날 경우 setInterval()이 제대로 동작할 기회를 얻지 못할 수 있음
    import { useState, useEffect } from "react";
    
    const App = () => {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const timer = setInterval(() => {
          setCount(count + 1);
        }, 3000);
    
        return () => clearInterval(timer);
      });
    
      return <span>{count}</span>;
    }
    
    export default App;
  • #2 빈 의존성 배열 ⭕ (최초 마운트 시에만 실행)

    • count 값이 0, 1, 2, 3, ... 과 같이 증가하는 것이 아니라, 0, 1에서 멈추는 현상 나타남

    • useEffect가 (첫 렌더에서 count의 초기값을 잡아버리고) 재실행되지 않으면 setInterval()에 있는 클로저는 항상 첫 렌더의 count 값(0)를 참조하며 count+1은 항상 1이 되는 것

      import { useState, useEffect } from "react";
      
      const App = () => {
        const [count, setCount] = useState(0);
      
        useEffect(() => {
          const timer = setInterval(() => {
              setCount(count + 1);
          }, 3000);
      
          return () => clearInterval(timer);
        }, []);
      
        return <span>{count}</span>;
      }
      
      export default App;

✅ Solution #1 useState의 updater function 형식

  • setCount(count + 1)setCount(count => count + 1)과 같이 수정
  • setState 내부에 콜백 함수를 사용하는 방식으로 변수가 항상 새로운 상태를 읽어들일 수 있음
  • 비동기로 동작하는 setState의 변경이 순서대로 일어나게 하기 위한 방법
  • state가 변화하는 문제 해결 ⭕ props가 변화하는 문제 해결 ❌
const App = () => {
  const [count, setCount] = useState(0);

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

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

✅ Solution #2 useReducer()

  • 리듀서 내부에서 현재 state와 새로운 props 두 가지 모두에 대한 접근 권한을 갖도록 하기
  • dispatch 함수 자체는 불변이라 어떠한 클로저로부터도 데이터를 주입시킬 수 있음
  • 한계점은 아직 side-effect를 emit 할 수 없다는 것 (?)
  • state가 변화하는 문제 해결 ⭕ props가 변화하는 문제 해결 ⭕
const App = () => {
  const [count, dispatch] = useReducer((state, action) => {
    if (action === "increment") {
      return state + 1;
    }
  }, 0);

  useEffect(() => {
    let timer = setInterval(() => {
      dispatch("increment");
    }, 1000);
    
    return () => clearInterval(id);
  }, []);

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

✅ Solution #3 useInterval()

  • useRef()current 속성을 가진 객체를 반환하는데, current 속성의 값은 다음과 같은 특징을 가짐
    • 해당 값이 변화해도 리렌더링이 발생 ❌
    • 리액트 컴포넌트가 리렌더링 될 때 해당 값이 유실 ❌
  • setInterval()에 들어가는 callback 함수를 이 current에 저장하고 해당 값을 바꿔주면 계속해서 변화하는 (count가 변하여 리렌더링이 발생할 시 새로운 state와 props를 거쳐서 만들어지는) 콜백 함수를 반영할 수 있음
  • 빈 의존성 배열로 인하여 인터벌이 리셋되지는 않지만 useRef()를 활용하여 저장해둔 savedCallback 덕분에 항상 최근 렌더링 후 세팅한 콜백 함수를 읽어올 수 있음
  • state가 변화하는 문제 해결 ⭕ props가 변화하는 문제 해결 ⭕
// useInterval.js 
import { useEffect, useRef } from "react";

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

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

    useEffect(() => {
      	// cf. delay 인자에 null 값을 전달할 경우 타이머를 멈출 수 있음
        if (delay === null) return;
        
        const timer = setInterval(() => savedCallback.current(), delay);
        return () => clearInterval(timer);
    }, [delay]);
}

export default useInterval;
// App.js
import useInterval from "./useInterval";

const App = () => {
  const [count, setCount] = useState(0);
  
  useInterval(() => {
    setCount(count + 1);
  }, 3000);
  
  return <span>{count}</span>;
}
profile
벨로그에는 주로 알고리즘 문제를 풀고 기록합니다 ✍

0개의 댓글