데이터를 초마다 자동으로 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
'공부 기록 > React' 카테고리의 다른 글
[React] '실무 중심! FE 입문자를 위한 React'를 수강하며 (0) | 2025.02.27 |
---|---|
[React] useState와 useEffect 사용하기 (0) | 2023.07.11 |
[React] useNavigate를 사용해서 뒤로가기 (0) | 2023.07.04 |
[React] Router을 사용해서 URL parameter에 따라 화면 다르게 보이기 (0) | 2023.07.03 |
[React] Error :: Cannot read property 'map' of undefined (0) | 2023.06.30 |