react에서 1초씩 카운트 하면서 화면에 출력해주는 함수가 있다고 하자. 그럼 코드를 요렇게 작성할 것이다.
function Counter() {
const [count, setCount] = useState(0);
setInterval(() => {
setCount(count + 1);
}, 1000)
return (
<>
<h1>{count}</h1>
</>
);
}
그럼 잘 작동할까? 놀랍게도(?) 잘 작동하지 않는다.
왜냐면 리랜더링할때마다 setInterval이 새로 호출이 된다. 그럼 setCount를 여러번 만들어내는 것이랑 같은효과가 나타나게 된다.
즉 count가 점점 쌓여간다는 것이다.
그래서 아래와 같이 바꾸어 준다.
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
useEffect는 디폴트값으로 매번 랜더링하게 되어있다.(두번째 인자가 없을때) 그럼, 랜더링될때 setInterval id가 만들어지고 리랜더링되기 전에 return문에 의해 setInterval이 클리어된다.
그리고 2번째 랜더링때 다시 setInterval이 만들어지고 클리어되고를 반복한다. 결국 잘작동하지만 매번 setInterval이 만들어지고 지워지고를 반복하기 때문에 비효율적이다.(만약 return으로 클리어해주지 않는다면 이전과 같은 이슈가 발생할 것이다.)
그래서 빈 array를 두번째 인자로 넘겨줌으로써 setInterval을 딱 한번만 실행한다.
const [count,setCount] = useState(0)
useEffect(() => {
let id = setInterval(() => {
console.log('interval')
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}.[]);
여기서 끝나면 재미없다. 역시나 문제가 발생할것이다.(끊임없는 디버깅 ㅋㅋㅋㅋㅋ 잼따..) 바로 카운트가 1에서 멈춘다는것. 이유는 useEffect가 콜백함수를 업데이트하지 않았다. 그래서 count는 일명 closure에 의해서 갇혀있는것이다. 즉 default count인 0에서 멈춰있고 이전에 업데이트한 count를 참고하지 않고 있다.
그래서 setCount(prevCount => prevCount + 1)
을 해주면 된다. 그럼 useState가 내부적으로 이전 count를 알아내게 된다. 근데 요것을 좀더 압축해서 간단하게 사용할 수 있다.
✨✨useInterval
✨✨을 만들 시간이다.
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
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]);
}
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000)
return (
<>
<h1>{count}</h1>
</>
);
}
아주 깔끔할 뿐더러 잘 동작한다. 그리고 delay에 따라서 setInterval을 새로 설정해주므로 dynamic한 카운트 앱을 만들 수 도 있다.
function Counter() {
const [count, setCount] = useState(0);
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(
() => {
// Your custom logic here
setCount(count + 1);
},
isRunning ? delay : null
);
function handleDelayChange(e) {
setDelay(Number(e.target.value));
}
function handleIsRunningChange(e) {
setIsRunning(e.target.checked);
}
return (
<>
<h1>{count}</h1>
<input
type="checkbox"
checked={isRunning}
onChange={handleIsRunningChange}
/>{" "}
Running
<br />
<input value={delay} onChange={handleDelayChange} />
</>
);
}
요렇게 하면 delay시간을 실시간으로 반영할 수 있고, 카운트를 잠시 중단할 수 도 있다.
카운트 앱을 만들면서 리액트와 클로저에대해 좀 더 친해질 수 있는 매우 좋은 시간이었다. well-spent 2 hours.
출처:
1. https://www.youtube.com/watch?v=2tUdyY5uBSw&t=531s
2. https://overreacted.io/making-setinterval-declarative-with-react-hooks/