react에서 setInterval 사용 시 플래그 설정을 왜 useRef로 적용해야할까?

박재현·2022년 9월 15일
0

FE 톺아보기

목록 보기
9/10
post-thumbnail

🤔문제 정의

3, 2, 1, 0 카운트 다운을 진행하는 컴포넌트를 동작시키기 위해 setInterval을 사용해 3에서 1초마다 countDown을 동작하고, count가 0이 되었을 때, clearInterval을 사용해 setInterval을 중지하는 로직을 구현 중이었다.

clearInterval 동작 여부를 구분하기 위해 isIntervalStop이라는 식별자를 flag로 사용하였다.

일반 변수로 플래그 선언

const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
let isIntervalStop = false;

const buttonClickHandler = () => {
  setIsStartCountDown(true);
};

useEffect(() => {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() => {
      console.log("isIntervalStop:", isIntervalStop);
      if (!isIntervalStop) {
        setCount((prev) => prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown, isIntervalStop]);

useEffect(() => {
  if (count === 0) {
    isIntervalStop = true;
  }
}, [count]);

count가 0이 되었을 때, isIntervalStop을 true로 변경하지만, isIntervalStop은 state가 아니므로 isIntervalStop의 변경을 useEffect에서 감지하지 못한다. 따라서 clearInterval로직이 실행되지 않는다.

useState로 플래그 선언

useEffect에서 flag의 변경을 감지해서 clearInterval로직을 동작할 수 있도록 isIntervalStopstate로 선언하였다.

const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const [isIntervalStop, setIsIntervalStop] = useState(false);

const buttonClickHandler = () => {
  setIsStartCountDown(true);
};

useEffect(() => {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() => {
      console.log("isIntervalStop:", isIntervalStop);
      if (!isIntervalStop) {
        setCount((prev) => prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown, isIntervalStop]);

useEffect(() => {
  if (count === 0) {
    setIsIntervalStop(true);
  }
}, [count]);

setIsIntervalStop(true)가 실행되면, useEffect에서 isIntervalStop의 변경을 감지하고, clearInterval로직을 실행한다.

하지만 isIntervalStop에 대한 console을 출력하면 true로 변경 되었다가 이내 다시 false로 출력되는 것을 볼 수 있다. 결과적으로 countDown은 멈추지 않고 계속 실행된다.

이를 확실히 알기 위해서는 react에서 state가 변경될 때, 어떤 동작을 수행하는지 알아볼 필요가 있다.

🤩state 변경 시 리렌더링

react 컴포넌트의 경우 부모 컴포넌트가 리렌더링될 때, props값이 변경될 때, state가 변경될 때 리렌더링된다.

isIntervalStop을 state로 둘 경우 state를 변경하는 순간 useEffect 내부의 countDownInterval로직이 다시 선언되고 실행된다. 그리고 기존에 실행 중이던 setInterval의 콜백함수는 종료되지 않고, 계속 실행된다.

이 때, 기존에 실행 중이던 setInterval의 콜백함수는 리렌더링되어 새로 생성된 isIntervalStop이 아닌, 리렌더링되기 전, 종료되기 전의 컴포넌트의 isIntervalStop을 참조하고 있다.

기존 실행 중이던 콜백함수의 실행컨텍스트의 렉시컬 환경은 리렌더링 전의 컴포넌트 함수 스코프의 렉시컬 환경을 참조하고 있고, 이 때의 컴포넌트 함수를 외부함수, 그리고 외부함수의 isIntervalStop이라는 자유변수를 참조하고 있는 콜백함수는 클로저가 된다.

즉, isIntervalStop state를 true로 변경하여도 클로저인 콜백함수는 false로 종료되어진 isIntervalStop 자유변수를 사용하기 때문에 종료되지 않고 계속 실행된다. 그리고 리렌더링 이후 새로 선언된 isIntervalStop에 true가 할당되어, 리렌더링 이후 만들어진 countDownInterval로직은 한번도 실행되지 않고 clearInterval로 종료된다.

🏆useRef로 플래그 선언

useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당한다. current 속성의 값을 변경하여도, 리렌더링되지 않는다.

useRef로 isIntervalStop을 선언하게 되면, isIntervalStop.current를 true로 변경했을 때, 컴포넌트 함수가 리렌더링되지 않고, 기존에 setInterval의 콜백함수도 true로 변경된 isIntervalStop.current를 참조하므로, 문제없이 카운트다운이 종료된다.

const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const isIntervalStop = useRef(false);

const buttonClickHandler = () => {
  setIsStartCountDown(true);
};

useEffect(() => {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() => {
      console.log("isIntervalStop:", isIntervalStop);
      if (!isIntervalStop.current) {
        setCount((prev) => prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown]);

useEffect(() => {
  if (count === 0) {
    isIntervalStop.current = true;
  }
}, [count]);

🏊‍♂️useRef에 setInterval 할당하기

플래그를 통해 clearInterval를 처리하는 로직이 한 useEffect에 몰려있어 복잡하다. useRef에 setInterval를 할당하여 setInterval를 시작하는 로직과, clearInterval를 통해 inteval을 종료하는 로직을 다른 useEffect로 분리하였다.

const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const countDownInterval = useRef<NodeJS.Timer | null>(null);

const buttonClickHandler = () => {
  setIsStartCountDown(true);
};

useEffect(() => {
  if (isStartCountDown === true) {
    countDownInterval.current = setInterval(() => {
      setCount((prev) => prev - 1);
    }, 1000);
  }
}, [isStartCountDown]);

useEffect(() => {
  if (count === 0) {
    clearInterval(countDownInterval.current as NodeJS.Timer);
    countDownInterval.current = null;
    setIsStartCountDown(false);
  }
}, [count]);

참조

React Hooks: useRef 사용법

profile
공동의 성장을 추구하는 개발자

0개의 댓글