[React] 타이머 막대(Time Progress Bar) 만들기

사요·2022년 12월 19일
0

UNIJAM

목록 보기
3/3

목표

: 다음과 같이 60초 시간을 카운트해주는 타이머 막대를 만들 것이다. 노란색은 현재 잔여시간(초)을 의미하고, 흰색은 경과된 시간을 의미한다.

시간 막대

시간을 막대형태로 렌더링 해주는 것은 다음 라이브러리를 이용하기로 했다.

import ProgressBar from "@ramonak/react-progress-bar";

setInterval

: 해당 타이머 기능을 구현하기 위해서는 '일정 주기(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>
  );
}

[결과]
https://velog.io/@yeyo0x0/React-React-Hooks%EC%97%90%EC%84%9C-setInterval-%EC%82%AC%EC%9A%A9-%EB%AC%B8%EC%A0%9C

실행해보니 1초가 지날때마다, 막대를 잘 줄여주고 있었다.
그러나...

  • 시간이 바뀔때마다 Timer 컴포넌트의 리렌더링이 일어났다.
  • 0초가 되어도 Timer가 멈추지 않았다.

원인을 파악하기 위해 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);
  }, []);

setInterval을 이용한 호출 스케줄링

https://ko.javascript.info/settimeout-setinterval

가비지 컬렉션과 setInterval·setTimeout

setInterval이나 setTimeout에 함수를 넘기면, 함수에 대한 내부 참조가 새롭게 만들어지고 이 참조 정보는 스케줄러에 저장됩니다. 따라서 해당 함수를 참조하는 것이 없어도 setInterval과 setTimeout에 넘긴 함수는 가비지 컬렉션의 대상이 되지 않습니다.

// 스케줄러가 함수를 호출할 때까지 함수는 메모리에 유지됩니다.
setTimeout(function() {...}, 100);
setInterval의 경우는, clearInterval이 호출되기 전까지 함수에 대한 참조가 메모리에 유지됩니다.

그런데 이런 동작 방식에는 부작용이 하나 있습니다. 외부 렉시컬 환경을 참조하는 함수가 있다고 가정해 봅시다. 이 함수가 메모리에 남아있는 동안엔 외부 변수 역시 메모리에 남아있기 마련입니다. 그런데 이렇게 되면 실제 함수가 차지했어야 하는 공간보다 더 많은 메모리 공간이 사용됩니다. 이런 부작용을 방지하고 싶다면 스케줄링할 필요가 없어진 함수는 아무리 작더라도 취소하도록 합시다.

setInterval

https://developer.mozilla.org/en-US/docs/Web/API/setInterval

setInterval(f)도 처음 몇 번은 함수 f를 지연 없이 실행하지만, 나중엔 지연 간격을 4밀리초 이상으로 늘려버립니다.

useEffect CleanUp

요약

  • 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가 과부하 상태인 경우
브라우저 탭이 백그라운드 모드인 경우
노트북이 배터리에 의존해서 구동 중인 경우

배운점

Ref

https://iborymagic.tistory.com/96

https://mingule.tistory.com/65

profile
하루하루 나아가는 새싹 개발자 🌱

0개의 댓글