article thumbnail

데이터를 초마다 자동으로 select 해서 출력하는 기능을 구현하게 되면서 setInterval()을 알게 되었다. 

하지만 axios로 넘겨주는 날짜 param 값이 계속 똑같은 날짜로만 넘어와 문제의 원인을 파악하는 데 애를 먹었다.

 

이 포스팅 setInterval()에 대해 공부하면서 알게된 사용 시의 유의점과 어떻게 setInterval()을 사용하여 원하는 기능을 구현할 수 있는지에 대한 기본적인 예시를 정리하려고 한다.

 

문제 파악

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

useEffect(() => {
	const intervalId = setInterval(() => {
    	setCount(count + 1);
        sampleFunc(count);
    }, 2000);
    
    return() => {
    	clearInterval(intervalId);
    }
}, []);

const sampleFunc = (count) => {
	console.log(count);
};

return(
	<>
    	<h1>{count}</h1>
    </>
)

해당 코드를 실행해보면 return, 즉 화면에 출력되는 count의 값은 1로 출력되고 sampleFunc()에 의한 console은 count 값이 계속 0으로 출력된다. 만약, 화면에서 숫자가 계속 증가되도록 출력하고 싶다면 setCount(count => count + 1);이라고 작성할 수는 있지만 console에서는 여전히 0으로 출력된다.

분명 같은 값을 출력하는데 왜 다르게 출력되는 것인지 이유를 알아보자.

 

 문제1 - setInterval의 시간 보장

React에서는 리렌더링이 일어나면 함수가 새로 실행된다.

보통의 subscription API들은 새로 함수가 실행되면, 이전의 subscription을 해제하고 새로운 subscription을 만드는데 setInterval()은 그러하지 못하기 때문에 clearInterval()을 사용해 직접 해제해야 한다. 즉, clearInterval()은 컴포넌트가 언마운트될 때 interval을 정리하고 함수가 호출되지 않도록 한다.

 

하지만, interval이 clearInterval에 의해 정리가 됨에도 count 값이 달라지는 이유는 setInterval()이 작성된 delay 시간을 100% 보장하지 못하기 때문이다.

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

 

문제2 - Closure 문제(🌟)

setInterval()의 시간 보장도 문제지만 count의 값이 초기화되는 문제는 closure 문제 때문이다.

useEffect는 처음 페이지가 렌더링될 때 count의 값을 캡쳐한다. 이때, setInterval()도 실행되는데, 캡쳐된 count의 값인 0이 계속 setInterval()에 의해 sampleFunc()로 전달된다.

Closure
어떤 내부 함수를 감싸는 외부 함수가 실행된 후 종료되었다 하더라도, 내부 함수에서 외부 함수의 값에 접근할 수 있는 현상

setInterval()에 있는 클로저는 내부에 존재하지 않는 count 변수를 찾아 항상 state 변수에 저장된 count = 0을 참조하지만, useEffect에 의해 재랜더링 되면서 count값이 변경되지 못해 계속 0+1을 실행한다.

즉, setInterval()은 종료되었지만 setInterval()의 내부 함수인 setCount가 실행될 때마다 외부 변수인 초기값인 0을 기억하고 계속 +1을 하기 때문에 값이 변경되지 않는다.

setCount(count => count + 1)을 실행했을 때는 1로 표시되는 것이 아니라 값이 계속 증가하는 이유는, closure가 처음 생성될 때, 참조하는 변수값을 Lexical Environment라고 하는 곳에 변수 값을 저장하고, 이를 사용하기 때문에 저장된 값에 접근하여 +1을 하기 때문이다.(callback 함수로 사용)

 

문제 해결(useInterval 사용)

문제를 해결하기 위해선 리렌더링이 발생하지 않게 하고, count의 값이 변경된 후 저장되어야 한다.

useRef를 사용한 useInterval은 해당 문제를 해결해준다.

useRef()의 current 속성은 해당 값이 변화해도 리렌더링이 발생하지 않고 페이지 자체가 새로고침되지 않는 이상 컴포넌트가 리렌더링 된다고 해도 값이 사라지지 않는다.

위의 코드를 useInterval을 사용해 변경하면 아래와 같이 수정할 수 있다.

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

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]);
}

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

const sampleFunc = (count) => {
	console.log(count);
};

return(
	<>
    	<h1>{count}</h1>
    </>
)

이제 count의 값이 정상적으로 변경되고 console에도 count값이 증가되어 출력된다.

(하지만 웹 페이지에서 출력되는 count 값과 console에 출력되는 count 값이 1씩 차이나는데 이는 위에서 설명한 closure에 의한 차이이다.)

 

useInterval의 동작 원리

useRef의 활용

    const savedCallback = useRef();

useRef() 훅은 리렌더링을 방지하는 데 사용된다.

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

즉, 페이지 자체가 새로고침 되지 않는 이상 useRef의 값은 계속 유지된다.

 

callback 함수를 저장하는 useEffect hooks

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

해당 useEffect는 useInterval()의 인자로 들어간 callback 함수가 변경될 때마다 ref 변수에 callback 함수를 저장한다.

위의 예시에서는 count 값을 업데이트하는 함수인 setCount를 useInterval에 전달하면서 내부의 savedCallback.current에 저장하여 업데이트 된 count 값을 useEffect 훅 내부에서 얻을 수 있도록 한다.

 

setInterval을 호출

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

두 번째 useEffect는 useInterval로 전달한 delay 시간에 맞춰 setInterval이 실행되도록 한다.

해당 useEffect는 무한히 실행되는 것이 아닌 delay가 변경될 때마다 실행되며, delay 값이 null이 아닌 경우 setInterval()에 해당 callback 함수를 전달해 실행되게 한다.

이미 tick() 내부의 값은 callback 함수가 변경될 때마다 업데이트 되고 있으므로 setInterval()을 재실행하지 않고도 업데이트 된 값을 불러올 수 있다.

 

번외

useState와 useRef를 함께 사용해 useInterval과 동일한 기능의 코드를 다르게 작성할 수 있다.

let [count, setCount] = useState(0);
const countRef = useRef(null);

countRef.current = () => {
	setCount(count+1);
};

useEffect(() => {
	let id = setInterval(() => {
    	countRef.current();
    }, 1000);
    
    return() => clearInterval(id);
}, []);

return(
	<h1>{count}</h1>
)

 

 


[ 참고 ]

https://mingule.tistory.com/65

https://velog.io/@yeyo0x0/React-React-Hooks에서-setInterval-사용-문제

https://velog.io/@dianestar/JavaScript-React-React에서-setInterval의-활용

https://velog.io/@sucream/setInterval을-쓰기-위한-여정

https://velog.io/@goldbear2022/클로저Closure가-되버린-내부함수가-일으키는-오류-feat.-setInterval

https://youtu.be/2tUdyY5uBSw?si=QZvInhMFdiusBgnD

https://codesandbox.io/s/hooks-setinterval-useref-forked-nn2scx?file=/src/index.js