[React] React에서 setInterval 함수 사용하기
이 글의 원문은 아래와 같습니다. 원문의 내용이 길고 영어로 되어있어 정리하여 한글로 옮겼습니다.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Making setInterval Declarative with React Hooks
How I learned to stop worrying and love refs.
overreacted.io
● 컴포넌트 렌더링과 setInterval( ) 함수
리액트를 보통 Redux와 함께 사용합니다. 리액트는 렌더링이 일어날 때 마다 effect hook의 내용을 새로 실행(적용)시킵니다. 이와 같은 방식은 렌더링이 새로 일어날 때 마다 새로운 API(예, 이벤트 리스너)를 코드에 다시 적용(구독, subscription)시키기 때문에 좋은 접근법이라고 할 수 있습니다. 하지만 이와 같은 방법은 setInterval( ) 함수와 약간의 충돌 가능성을 내포합니다.
만약 setInterval( ) 함수의 주기(period)가 짧다면 setInterval( ) 함수가 인자로 주어진 콜백 함수를 호출하는 주기보다 렌더링 주기가 더 짧아질 수 있습니다. 이 경우 setInterval( ) 함수는 프로그래머가 원하는대로 동작하지 않는 것으로 보이게 됩니다.
그러면 이제, 컴포넌트의 상태 변화에 대해 매 번 새로운 setInterval( )이 설정되도록 하지 않고 useEffect 훅과 useEffect 훅이 동작하는 조건인 종속 배열(dependancy array)을 이용해서 컴포넌트가 페이지에 마운트될 때 한 차례만 setInterval( ) 함수를 실행하게끔 해 봅시다.
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
useEffect 훅의 종속 배열에 빈 배열 []을 전달하면 해당 effect 훅은 컴포넌트가 페이지에 마운트되고 언마운트 될 때에만 실행될 것입니다. 하지만 이렇게 해도 1초(1000ms)에 1씩 증가하리라 기대했던 count는 1에 고정되어 증가하지 않는 것 처럼 보입니다.
네, 증가하지 않는 것 처럼 보이는 것입니다. 실제로는 증가하는 것인데 매 주기마다 0→1을 반복하기 때문에 증가하지 않는 것처럼 보이는 것입니다. 그렇다면 그 이유는 무엇일까요? 그 이유는 바로 자바스크립트의 closure 개념에 있습니다.
자바스크립트는 함수나 클래스와 같은 객체를 상태와 상태를 변경시키는 절차의 집합으로 봅니다. 이와 같은 개념이 자바스크립트의 closure 개념입니다. 따라서 코드의 실행 시점에서 함수나 클래스를 할당(apply 또는 subscript) 받으면 그 상태까지 함께 가져오게 됩니다. 따라서 위의 예에서 setInterval( ) 함수는 콜백 함수인 () => {setCount(count + 1);}의 상태를 useEffect 훅이 그것을 가져왔을 때 상태인 count 값이 0인 것으로 보고 이를 1만큼 증가시키게 됩니다. 결국 setInterval( ) 함수가 콜백 함수를 매 번 실행하더라도 count 상태 값은 0에서 시작하여 1증가한 값인 1이 되고 이를 계속 반복하게 됩니다.
그렇다면 이와 같은 0→1 상태의 반복을 어떻게 피할 수 있을까요? 답을 먼저 살펴보자면 setInterval( ) 함수가 실행되고 페이지 렌더링이 일어날 때 마다 setInterval( ) 함수가 호출하는 콜백 함수도 함께 업데이트 해 주는 것입니다.
function Counter() {
const [count, setCount] = useState(0);
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);
}, []);
return <h1>{count}</h1>;
}
위 코드의 첫 번째 useEffect 훅을 보면 렌더링이 일어날 때 마다 ref 훅을 이용해서 함수 객체 자체를 update해 주고 있는 것을 알 수 있습니다. 즉, setInterval( ) 함수는 이 savedCallback 레퍼런스를 이용해서 매 번 count 상태 값이 update된 함수를 호출합니다.
참고로 리액트의 ref 훅(useRef 훅)은 mutable update를 도와주는 객체라고 할 수 있습니다. useRef 훅은 컴포넌트 렌더링들 사이에 보관이 필요한 객체나 변수들을 새로운 변수나 객체를 생성한 후 그 레퍼런스(=포인터)를 ref훅의 current 속성의 값에 저장합니다. 값이 변하면 그 reference(변수나 객체의 주소 값)도 변화시키는 mutable update 방식을 사용하는 데서 ref라는 이름이 유래하였다고 할 수 있습니다.
위 코드를 좀 더 이식성 좋게(내용의 원문에서는 '선언적인declarative 방식'이라고 표현하였습니다) 아래와 같이 정리해 볼 수 있습니다.
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
위와 같이 Counter 컴포넌트는 선언적인 방법으로(즉, 추상화된 함수나 메서드를 호출하는 접근법으로) setInterval( ) 함수의 기능을 사용할 수 있습니다.
■