: 다음과 같이 60초 시간을 카운트해주는 타이머 막대를 만들 것이다. 노란색은 현재 잔여시간(초)을 의미하고, 흰색은 경과된 시간을 의미한다.
시간을 막대형태로 렌더링 해주는 것은 다음 라이브러리를 이용하기로 했다.
import ProgressBar from "@ramonak/react-progress-bar";
: 해당 타이머 기능을 구현하기 위해서는 '일정 주기(1초)마다 특정 작업이 일어나야한다.따라서 자연스럽게 setInterval
함수를 활용하는 것을 떠올리게 되었다.
그리고 다음과 같은 코드를 구현하게 되었다.
export default function TimeBar() {
console.log("render");
const [sec, setSec] = useState(5);
useEffect(() => {
const interval_id = setInterval(() => {
setSec((sec) => sec - 1);
if (sec == 0) {
clearInterval(interval_id);
}
}, 1000);
}, []);
return (
<Row>
{/*<Clock />*/}
<SvgIcon src={ClockSrc} size="100px" />
<SizedBox width={"50px"} />
<ProgressBar
completed={String(sec)}
bgColor="var(--yellow)"
width="500px"
height="40px"
/>
{sec}
</Row>
);
}
실행해보니 1초가 지날때마다, 막대를 잘 줄여주고 있었다.
그러나...
원인을 파악하기 위해 setInterval의 callback 함수 내부에 현재 sec
를 출력하는 문장을 추가하였다.
useEffect(() => {
const interval_id = setInterval(() => {
setSec((sec) => sec - 1);
console.log(sec); //이문장 추가
if (sec == 0) {
clearInterval(interval_id);
console.log("중지!!");
}
}, 1000);
}, []);
[결과]
그러나 놀라운 결과가 있었다. sec이 줄어들지 않고 계속 5로 유지되더라 하는 것이다. useEffect내부에서 찍는 sec는 계속 5인데, 어떻게 return 문 안에서는 5->4->3->... 으로 줄어드는 것처럼 보일 수 있었던 것일까?
console.log(sec);
useEffect(() => {
console.log("useEffect");
const interval_id = setInterval(() => {
setSec((sec) => sec - 1);
if (sec == 0) {
clearInterval(interval_id);
console.log("중지!!");
}
}, 1000);
}, []);
[결과]
: 제대로 줄어들었다.
그 이유는
const [sec, setSec] = useState(5);
useEffect(() => {
const interval_id = setInterval(() => {
setSec((sec) => sec - 1);
}, 1000);
setTimeout(() => {
clearInterval(interval_id);
alert("정지");
}, 5000);
}, []);
https://ko.javascript.info/settimeout-setinterval
setInterval이나 setTimeout에 함수를 넘기면, 함수에 대한 내부 참조가 새롭게 만들어지고 이 참조 정보는 스케줄러에 저장됩니다. 따라서 해당 함수를 참조하는 것이 없어도 setInterval과 setTimeout에 넘긴 함수는 가비지 컬렉션의 대상이 되지 않습니다.
// 스케줄러가 함수를 호출할 때까지 함수는 메모리에 유지됩니다.
setTimeout(function() {...}, 100);
setInterval의 경우는, clearInterval이 호출되기 전까지 함수에 대한 참조가 메모리에 유지됩니다.
그런데 이런 동작 방식에는 부작용이 하나 있습니다. 외부 렉시컬 환경을 참조하는 함수가 있다고 가정해 봅시다. 이 함수가 메모리에 남아있는 동안엔 외부 변수 역시 메모리에 남아있기 마련입니다. 그런데 이렇게 되면 실제 함수가 차지했어야 하는 공간보다 더 많은 메모리 공간이 사용됩니다. 이런 부작용을 방지하고 싶다면 스케줄링할 필요가 없어진 함수는 아무리 작더라도 취소하도록 합시다.
https://developer.mozilla.org/en-US/docs/Web/API/setInterval
setInterval(f)도 처음 몇 번은 함수 f를 지연 없이 실행하지만, 나중엔 지연 간격을 4밀리초 이상으로 늘려버립니다.
setInterval(func, delay, ...args)과 setTimeout(func, delay, ...args)은 delay밀리초 후에 func을 규칙적으로, 또는 한번 실행하도록 해줍니다.
setInterval·setTimeout을 호출하고 반환받은 값을 clearInterval·clearTimeout에 넘겨주면 스케줄링을 취소할 수 있습니다.
중첩 setTimeout을 사용하면 setInterval을 사용한 것보다 유연하게 코드를 작성할 수 있습니다. 여기에 더하여 지연 간격 보장이라는 장점도 있습니다.
대기 시간이 0인 setTimeout(setTimeout(func, 0) 혹은 setTimeout(func))을 사용하면 ‘현재 스크립트의 실행이 완료된 후 가능한 한 빠르게’ 원하는 함수를 호출할 수 있습니다.
지연 없이 중첩 setTimeout을 5회 이상 호출하거나 지연 없는 setInterval에서 호출이 5회 이상 이뤄지면, 4밀리초 이상의 지연 간격이 강제로 더해집니다. 이는 브라우저에만 적용되는 사항이며, 하위 호환성을 위해 유지되고 있습니다.
스케줄링 메서드를 사용할 땐 명시한 지연 간격이 보장되지 않을 수도 있다는 점에 유의해야 합니다.
아래와 같은 상황에서 브라우저 내 타이머가 느려지면 지연 간격이 보장되지 않습니다.
CPU가 과부하 상태인 경우
브라우저 탭이 백그라운드 모드인 경우
노트북이 배터리에 의존해서 구동 중인 경우