-
[React] React에서 setInterval 사용React 2022. 7. 22. 18:08
Making setInterval Declarative with React Hooks
How I learned to stop worrying and love refs.
overreacted.io
번역 / 리액트 훅스 컴포넌트에서 setInterval 사용 시의 문제점
Dan abramov의 https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 번역입니다.All copyrights to Dan Abramovtranslated by Jake seoTHE
velog.io
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을 일시 정지할 수 있다.
반응형'React' 카테고리의 다른 글
[React] 객체 key에 접근 (0) 2022.09.21 [React] 코드 분할과 React.lazy (0) 2022.08.11 [React] 제어/비제어 컴포넌트 (0) 2022.07.10 [React] Intersection Observer API : 무한 스크롤 구현 (0) 2022.07.03 [React] 조건부 렌더링 시 "falsy" 값 렌더링 주의 (0) 2022.06.28