본문 바로가기

JS Ecosystem

(component) React 타이머 시작, 일시정지/재개, 초기화

자바스크립트 내장함수 setInterval과 clearInterval 그리고 react 의 useState와 useRef로 Timer 를 구현해보았습니다.

전체 코드 보기 http://bit.ly/3Q7to69

요구사항

1. seconds를 입력할 때 60초를 넘겨서 입력하면 초과한 초를 제외한 나머지는 분(minutes)으로 합산된 상태로 타이머가 작동되어야 합니다.

2. 일시정지(PAUSE)와 재개(RESUME) 동작 기능이 있습니다.

배경지식

1. setInterval과 clearInterval 활용방법

특정함수를 1초 간격(1000m/s, milliseconds, 밀리세컨즈)으로 반복해서 실행시킬 때 seInterval 함수를 이용합니다.

setInterval 함수는 'interval ID'를 리턴합니다. clearInterval 함수 사용하기 위해 이 리턴값을 변수 timer에 담아 줍니다.

const timer = setInterval(()=> {
    특정함수();
  }, 1000)

 

특정함수의 반복적인 실행을 종료하고 싶다면 clearInterval의 매개변수로 setInterval의 리턴값을 넣어준다. 리턴값은 timer에 담겨있습니다.

clearInterval(timer);

구현방법

우선 수도 코드로 구현방법을 기획했습니다.

1. 입력받은 값으로 total seconds 값을 구한다.(A)

2. 해당값을 1초마다 감소시키는 setInterval을 구현한다.(B)

3. B의 리턴값을 useRef에 담는다.(C)

4. 일시정지/재개버튼에 따라 clearInterval 처리해주고 useRef를 null처리 해준다.(D)

5. D는 반복적으로 사용될 수 있기에 변수로 캐싱해준다.

 

1) 선언된 useState와 useRef

1
2
3
4
5
6
7
8
9
10
11
12
  const [initialMin, setInitialMin] = useState(0);
  const [initialSec, setInitialSec] = useState(0);
 
  const [minutes, setMinutes] = useState(0);
  const [seconds, setSeconds] = useState(0);
 
  const [count, setCount] = useState(0);
 
  const [isStarting, setIsStarting] = useState(false);
  const [isPause, setIsPause] = useState(false);
 
  const intervalRef = useRef(null);
cs

 

initialMin, intialSec : 처음 입력받은 "분"과 "초"값 상태관리

minutes, seconds : 화면 하단에 00 : 00 형식으로 표시되는 타임 디스플레이 상태관리

count : total sec 값 관리. setInterval 함수로 1단위 감소되는 값.  useEffect의 의존성 배열의 요소

isStarting : START 버튼 클릭할 경우 분과 초값을 입력한 상태인지 입력값 없이 누른 것인지를 구분하는 요소

isPause : PAUSE/RESUME 버튼 클릭할 경우 PAUSE인지 RESUME인지를 구분하는 요소

 

2) JSX

 return (
    <Container>
      <TimeText>
        <label>
          <input
            type="number"
            value={initialMin}
            onChange={(e) => setInitialMin(e.target.value)}
          />
          <div className="text">minutes</div>
        </label>
        <label>
          <input
            type="number"
            value={initialSec}
            onChange={(e) => setInitialSec(e.target.value)}
          />
          <div className="text">seconds</div>
        </label>
      </TimeText>
      <TimeButton>
        <button type="button" onClick={start}>
          START
        </button>
        <button type="button" onClick={pauseAndResume}>
          PAUSE/RESUME
        </button>
        <button type="button" onClick={reset}>
          RESET
        </button>
      </TimeButton>
      <TimeDisplay>
        {minutes >= 10 ? minutes : `0${minutes}`} :{' '}
        {seconds >= 10 ? seconds : `0${seconds}`}
      </TimeDisplay>
    </Container>
  );
}

 

3) 입력 받은 minutes과 seconds을 합산하여 total sec을 구현합니다. totalSec은 setCount로 변경됩니다.

초기값이 입력된 input 영역은 0으로 초기화됩니다.

  const calTotalSec = () => {
    const totalSec = Number(initialMin) * 60 + Number(initialSec);
    setCount(totalSec);
    setInitialMin(0);
    setInitialSec(0);
  };

 

4) 전역변수 count를 가지고 "00:00" 형식으로 표시합니다.

  const timeFormat = () => {
    if (count < 0) {
      initInterval();
      setIsStarting(false);
    } else {
      const min = Math.floor(count / 60);
      const sec = count % 60;
      setMinutes(min);
      setSeconds(sec);
    }
  };

 

5) setInterval의 callback 함수로 사용할 countdown 함수입니다.

  const countdown = () => {
    setCount((c) => c - 1);
  };

 

6) START 버튼 클릭시 3), 4)번 함수가 실행되며 intervalRef.current에 setInterval의 결과값을 담습니다.

변수 isStarting 을 가지고 PAUSE인지 RESUME인지를 구분합니다.

  const start = () => {
    if (!isStarting) {
      calTotalSec();
      timeFormat();
      setIsPause(false);
      intervalRef.current = setInterval(countdown, 1000);
      setIsStarting(true);
    }
  };

 

7) START 혹은 RESET 버튼 클릭시에 작동할 setInterval을 초기화해주는 함수입니다. 반복적으로 사용되어서 함수로 묶었습니다.

  const initInterval = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

 

8) PAUSE / RESUME 버튼 코드입니다.

전역변수 count (total seconds)의 값이 0일 경우에는 setInterval과 setIsPause를 초기화 해줍니다.

  const pauseAndResume = () => {
    if (count === 0) {
      initInterval();
      setIsPause(false);
    }

    if (count !== 0) {
      if (!isPause) {
        initInterval();
        setIsPause(true);
      } else {
        intervalRef.current = setInterval(countdown, 1000);
        setIsPause(false);
      }
    }
  };

 

9) useEffect hook입니다. count가 변경될때마다 timeFormat 함수가 실행되어 "00:00"에서 분과 초가 바뀝니다.

  useEffect(timeFormat, [count]);
  const timeFormat = () => {
    if (count < 0) {
      initInterval();
      setIsStarting(false);
    } else {
      const min = Math.floor(count / 60);
      const sec = count % 60;
      setMinutes(min);
      setSeconds(sec);
    }
  };

 

이슈사항

1. setInterval 리턴값을 useRef에 담는 것

setInterval로 반복적으로 실행되는 특정함수를 중지시키기 위해 setInterval의 리턴값을 변수에 저장해서 그 변수를 clearInterval의 인자로 전달해야 합니다.

 

React이기 때문에 useState로 상태를 관리하거나 const나 let 키워드로 변수를 선언하고 할당해주는 방법이 있습니다. useState으로 관리할 경우 상태값 변화에 따른 렌더링이 발생할 수 있으며 const나 let은 React라서 사용하지 않았습니다.

 

useRef로 관리하는 변수는 관리하는 값이 변한다고 해서 리렌더링이 발생하지 않습니다. (참고자료 https://bit.ly/3ibQkEL)

렌더링 최적화를 위해 useRef에 setInterval 리턴값을 담았습니다.

 intervalRef.current = null;

위 코드와 같이 null을 할당해도 렌더링은 발생하지 않아서 최적화에 좋습니다.

 

2. setInterval 함수와 timeFormat 함수의 분리

처음에는 setInterval 함수와 timeFormat 함수를 start 버튼이 누를 때 한꺼번에 작동하게 만들었습니다.

그런데 count 상태가 변하면 이에 따라 타이머 값도 변경되기 때문에  timeFormat 함수가 재실행되어야 합니다.

setInterval 함수는 한번 실행되고 나면 clearInterval로 종료되기 전까지는 계속 실행되기 때문에

useEffect로 timeFormat 함수만 실행되게 처리하였습니다.