리액트에서 setInterval의 동작문제와 해결법

rlorxl·2022년 11월 14일
0

React

목록 보기
2/10

연습 프로젝트에서 페이지 전환 시 인증을 거치도록 하기 위해 인증요청 시간동안 보여줄 로딩 스피너를 구현하려 했다.

로딩이미지 배열을 만들고 setInterval을 사용해 0.1초마다 setCount로 배열의 인덱스를 변경해주는 방식으로 count가 배열의 길이를 넘어가면 0으로 set해주도록 하였다.

const Loading = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    let timer = setInterval(() => {
      if (count >= 15) setCount(0);
      setCount((prev) => prev + 1);
    }, 100);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return (
    <Wrapper>
      <div>
        <LoadingImage src={loadingImages[count]} />
        <p>Loading...</p>
      </div>
    </Wrapper>
  );
};

그런데 setInterval안의 조건문이 예상대로 동작하지 않는 문제가 있었고 검색해보니 리액트에서 자바스크립트처럼 똑같이 setInterval을 사용했을 때 동작이 정확하지 않다는 글들이 많았다.

결론부터 말하면 커스텀훅을 사용해서 clearInterval이 제대로 동작하도록 만들어야 했다.

기존상태로 구현했을 때의 문제는 이렇다.


문제1

useEffect의 의존성을 빈 배열로 설정했으므로 처음 한 번만 실행되기 때문에 setInterval에서 바뀐 state의 값을 알 수 없고, 처음 실행된 동작만 실행시켜 카운트가 무한히 올라간다.

👉 state가 바뀌면 React는 리렌더링을 하게 되는데, setInterval은 렌더와 관계없이 계속 살아남아있는다. React는 리렌더링을 하면서 이전의 render된 내용들을 다 잊고 새로 그리게 되는데, setInterval은 그렇지 않다. Timer를 새로 설정하지 않는 이상 계속 이전의 내용(props나 state)들을 기억하고 있다.


문제2

setInterval은 함수를 실행하는 시간조차 delay에 포함시키기 때문에, 만약 함수를 실행하는 시간이 delay 시간보다 길다면 타이머가 제대로 작동하지 않는다.

👉 결과적으로 setState에 의해 리렌더링은 계속 일어나면서 clearInterval은 제대로 실행이 되지 않는 문제가 발생한다.


useInterval()

제대로 동작하게 하기 위해서 useInterval이라는 커스텀 훅에 콜백함수를 넘겨 해결해야 한다.

코드는 다음과 같다.

import { useEffect, useRef } from 'react';

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

  useEffect(() => {
    savedCallback.current = callback; // ref를 이용하여 리렌더링 시에도 값이 유지되도록 한다.
  }, [callback]);

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

    if (delay !== null) {
      let timerId = setInterval(tick, delay);
      return () => clearInterval(timerId);
    }
  }, [delay]); 
};

export default useInterval;

useInterval의 인자로 콜백함수를 받아 이를 savedCallback이라는 ref에 할당한다.
첫번째 useEffect를 보면 callback이 변할 때마다 .current의 값이 변한다.

🤔 이때 useRef를 사용하는것이 조금 생소했는데 useRef는 .current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref 객체를 반환하며 변경가능한 값을 저장할 수 있고 useState와 다르게 생명주기에 의존하지 않아 값이 바뀌어도 리렌더링이 일어나지 않기 때문에 함수를 저장해 놓는 용도로 사용하기에 적합하다는 장점이 있다.

두번째 useEffect를 보면 tick이라는 함수가 저장된 콜백을 실행하는데 의존성으로는 delay가 지정되어있다.
(내부의 조건문은 delay가 null일 때를 고려해 null일 때는 실행되지 않도록 하는 옵션이다.)

그리고 setInterval에 저장된 콜백함수를 지정하고 실행시키는데 clean-up함수로 clearInterval을 반환하고 있어 컴포넌트가 unmount될 때 실행되도록 한다.

👉👉
이렇게 하면 두번째 useEffect에는 delay가 의존성으로 있어 재실행되지 않지만 첫 번째 useEffect의 의존성으로 있는 callback의 변경이 있을 때 savedCallback.current가 업데이트 되기 때문에 결국 두 번째 useEffect() 내의 setInterval() 함수는 재실행되지 않고도 새로 업데이트 된 콜백함수를 실행할 수 있다.



✏️✏️

실제로 useInterval을 사용했더니 원하던대로 함수가 동작하는 것을 볼 수 있었다.
생각보다 리액트에서 setInterval의 동작과 리렌더링 문제, ref의 속성, clean-up함수를 모두 제대로 알고 있어야 해결할 수 있었기 때문에 흥미로웠고 특히 ref를 이런 식으로도 쓸 수 있다는 것을 알게 되어서 useRef에 대해서도 다시 공부해봐야 될 것 같다.

0개의 댓글