[React] React에서 setInterval 사용
react 프로젝트 진행 중 timer를 구현하기 위해 setInterval을 사용해야 했다.
setInterval 사용 시 react에서 일어날 렌더링에 관한 이슈에 대해 궁금하여 검색하였고
위에 두 글을 참고하여 작성하였다.
🤔 useEffect로 구현한 setInterval 문제점
Case1
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
return <h1>{count}</h1>;
}
useEffect를 이용하여 setInterval을 구현한 코드이다.
이 코드를 실행했을 때 보기엔 잘 동작해 보인다.
setInterval(() => {
ReactDOM.render(<Counter />, rootElement);
}, 100);
하지만 위와 같이 더 작은 interval로 렌더링하면 작동이 안된다.
리액트는 컴포넌트가 렌더링 된 이후에 useEffect를 실행한다.
effect를 clean-up 하는 시점은 컴포넌트가 unmount 될 때이다.
우리가 너무 많이 re-rendering하고 effect를 재적용하면 타이밍이 어긋나 interval은 동작할 기회를 얻지 못한다.
위의 코드에 console.log를 작성하여 확인해 본 결과
Case2
Case1에서의 문제는 effect의 재실행보다 너무 빨리 clear 되었다는 점이다.
밑의 코드는 effect를 다시 재실행시키지 않는 방법이다.
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
하지만 이 코드의 문제는 useEffect가 count를 0으로 잡는다.
따라서 setInterval의 클로저가 항상 첫 렌더의 count인 0을 참조하게 되고 항상 count + 1은 1이 출력하게 된다.
해결방법?
setCount(count + 1)을 setCount(c => c + 1)과 같이 "update" 폼과 함께 사용하는 것이다.
이렇게 변경하면 변수가 항상 새로운 상태를 읽어들일 수 있게 된다.
하지만 새로운 props를 읽을 때는 다른 방법이 필요하다.
💡 Ref을 통한 해결
문제점
- 첫 렌더에서 callback1을 가진 setInterval(callback1, delay)를 수행할 것이다.
- 다음 렌더에서 새로운 props와 state를 거쳐서 만들어지는 callback2가 있다.
- 하지만 시간을 재설정하지 않고서는 callback을 대체할 수 없다!
만일 interval을 전혀 변경하지 않고,
대신 변경 가능한 최근의 interval callback을 가리키는 savedCallback 변수를 도입한다면?
const savedCallback = useRef();
function callback() {
setCount(count + 1);
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
매 렌더링 시 useRef()를 통해 생성된 savedCallback.current에 실행될 callback을 등록한다.
callback은 최신의 props, state 등을 읽게 된다.
setInterval에서 trick 콜백 함수를 실행하게 된다면 최신의 callback을 호출할 수 있게 된다.
✨ useInterval hook
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
위의 코드를 바탕으로 만들어진 useInterval hook이다.
delay가 변할 때 타이머가 재 시작된다.
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
null을 delay에 전달하는 것으로 interval을 일시 정지할 수 있다.