[React] - useInterval ?

김하영·2023년 2월 13일
0

React

목록 보기
5/7
post-thumbnail

리액트에서 타이머 기능을 추가하면서 setInterval을 써야했는데 원하는대로 동작하지 않았다.
공부한 내용을 토대로 정리해보려고 한다.

1. 문제 상황 및 원인 분석

Case 1

function Time() {
	setInterval(() => {
    	setCurrentIndex((currentIndex) => currentIndex + 1);
    }, 1000)
}

javascript와 달리 React에서는 state가 변하면 App이 re-rendering 되기 때문에 setInterval()함수는 무한히 실행되게 된다.


Case 2

case1을 경험했다면 처음 렌더링 시에만 실행되도록 useEffect() 훅 내에 같이 쓰면 될 것이라 생각할 수 있다.

useEffect(() => {
	setInerval(() => {
    	setCurrentIndex((currentIndex) => currentIndex + 1);
    }, 1000);
    return () => clearInterval(id);
}, [])

하지만 uesEffect() 훅의 두 번째 인자로 빈 배열을 전달하면 처음 한 번만 실행되기 때문에 useEffect() 내부에서는 업데이트 된 currentIndex 값을 알 수 없고, 떄문에 console에는 초기값인 0만 찍히게 된다.


Case 3

case2의 문제로 인해 state가 변할 때마다 실행되도록 아래와 같이 작성할 수도 있다.

useEffect(() => {
	setInterval(() => {
    	console.log(currentIndex);
        setCurrentIndex((currentIndex) => currentIndex + 1);
    }, 1000);
    return () => clearInterval(id);
}, [currentIndex])

하지만 useEffect() 두 번째 인자로 전달하는 의존성 배열 내의 값을 useEffect() 함수 내에서 수정하고 있기 떄문에 무한 루프에 빠지게 된다.

그렇다면 어떻게 작성해야 이를 해결할 수 있을까?

바로 ✨useRef✨를 사용하여 ✨custom Interval✨ 훅을 만들면 된다.


2. 해결방안 - useInterval Hook

핵심은 interval을 전혀 변경하지 않고, 변경이 가능하면서 최근 interval callback을 가리키는 savedCallback 변수를 도입하는 것이다. 먼저 코드로 확인해 보자.

function useInterval(callback, delay) {
	const savedCallback = useRef();
    
    useEffect(() => {
    	savedCallback.current = callback;
    }, [callback]);
    
    useEffect(() => {
    	function tick() {
        	savedCallback.current();
        }
        if (delay !== null) {
        	let id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
}


function Time() {
	const [count, setCount] = useState(0);
    
    useInterval(() => {
    	setCount((count) => count + 1);
    }, 1000);
}

위의 코드를 보면 setInterval 대신 useInterval이라는 custom interval Hook을 만들어서 사용하고 있다.


3. useInterval 파헤치기

function useInterval(callback, delay) {
	const savedCallback = useRef();
    
    useEffect(() => {
    	savedCallback.current = callback;
    }, [callback]);
    
    useEffect(() => {
    	function tick() {
        	savedCallback.current();
        }
        if (delay !== null) {
        	let id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
}

1) useRef의 활용

useInterval hook을 보면 callback 함수와 delay를 전달받아서 useEffect 내부에서 callback 함수를 savedCallback.current에 저장하고 있다.

const savedCallback = useRef();

이 때, useInterval에서 useRef훅을 사용한 이유는 리렌더링을 방지하기 위해서이다.
useRef는 함수형 프로그래밍에서 사용하는 ref로 초기화된 ref 객체인 {current: null}을 반환하며 반환된 객체는 컴포넌트의 전 생애주기 동안 유지되어 useRef로 관리하는 값은 값이 변경되어도 컴포넌트가 리렌더링 되지 않는다.

🤔 만약 useState를 사용했다면 어떨까?

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

useInterval(() => {
	setCount((count) => count + 1);
}, 1000);

useState를 사용하여 데이터를 관리할 경우 값이 변경될 때 리렌더링이 일어나기 떄문에 useEffect() 내부에서 savedCallback 값이 변경될 때마다 리렌더링이 일어나게 되고 이 떄문에 두 번째 useEffect() 내부에서 savedCallback 값을 확인하면 계속해서 초기값만을 가져오게 될 것이다.

2) callback 함수를 저장하는 useEffect hooks

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

위의 코드를 보면 callback 데이터가 바뀔 때마다 useEffect()가 실행되어 savedCallback의 current 값이 새로운 callback 데이터로 업데이트 된다.

🤔 왜 useInterval()에 함수를 전달해서 내부에서 값을 업데이트 해주는 걸까?
이는 앞서 소개했던 문제상황의 case들을 생각해보면 이해할 수 있다.

  1. useEffect()는 의존성 배열을 전달하지 않으면 리렌더링 될때마다 실행된다.
  2. useEffect()의 두 번째 인자에 빈 배열을 전달할 경우 첫 렌더링 시에만 실행되어 훅 내부에서는 변화된 데이터 값을 얻을 수 없다.
  3. useEffect()는 특정 데이터가 변경될 때마다 실행되도록 할 수 있으나, useEffect() 내부에서 값이 업데이트 되는 데이터에 의존할 경우 무한 루프에 빠지게 된다.

이를 해결하고자 useInterval() 훅은 리렌더링 시마다 실행되어 업데이트 된 count 값을 가질 수 있게 만들고, count 값을 업데이트하는 함수를 useInterval 훅에 전달하여 내부에서 savedCallback.current에 저장하여 새로 업데이트 된 count 값을 useEffect 훅 내부에서도 얻을 수 있도록 한 것이다.

3) setInterval을 호출하는 useEffect hooks

첫 번째 useEffect 훅을 통해 callback 함수를 저장했으면 두 번째 useEffect에서는 setInterval 함수에 해당 callback 함수를 전달해 실행되도록 만들어야 한다.

useEffect(() => {
	function tick() {
    	savedCallack.current();
    }
    if (delay !== null) {
    	let id = setInterval(tick, delay);
        return () => clearInterval(id);
    }
}, [delay]);

위의 useEffect는 delay가 변경될 때마다 실행되며 delay가 null값이 아닐 경우에 setInterval 함수를 호출하여 callback 함수를 실행한다.

이렇게 작성하면 1. useEffect()가 무한히 실행되는 것을 방지하며 delay가 변경될 때에만 타이머를 재실행하게 되며 2. 첫 번째 useEffect()는 콜백함수가 변경될 때마다 업데이트하기 때문에 결국 두 번째 useEffect() 내의 useInterval() 함수는 재실행되지 않고도 새로 업데이트 된 콜백함수를 실행할 수 있다.

이 때 delay를 null check하는 Interval을 일시중지 할 수 있게 하기 위해서이며 useInterval에 null인 delay를 전달할 경우 더 이상 setInterval 함수가 실행되지 않게 된다.

return () => clearInterval(id); 부분은 clean-up 함수로 class component의 경우는 componentWillUnmount라는 라이프 사이클 메서드를 이용해 구현하며 function component의 경우는 useEffect()에 전달한 함수의 return 함수로 구현한다.
useEffect() 내에서 함수를 반환하면 컴포넌트가 unmount될 때 해당 함수가 실행되어 불필요한 동작을 제거하거나 메모리 누수 문제를 방지할 수 있어 사용한다.


참고자료

https://ye-yo.github.io/react/2022/01/21/setInterval%EC%82%AC%EC%9A%A9%EB%AC%B8%EC%A0%9C.html

profile
호기심 많은 프론트엔드 주니어 💡

0개의 댓글