useInterval 사용해서 타이머 만들기

sunkyu_hong·2022년 3월 29일
1

setInterval 과 예기치못한 Closure

웹 앱 내에서 사용할 타이머를 개발중이였는데 setInterval 을 사용하는 부분에서 문제가 발생했다..!

import { useEffect, useState } from "react";
import styled from "styled-components";

const TimerBox = styled.div``;

const Controls = styled.div``;

const Timer = ({ delay }) => {
  const [isPlay, setIsPlay] = useState(false);
  const [minute, setMinute] = useState(1);
  const [second, setSecond] = useState(5);
  const [timerInterval, setTimerInterval] = useState(0);

  const tick = () => {
    console.log(minute, second);
    if (second > 0) {
      setSecond((sec) => sec - 1);
    }

    if (second === 0) {
      if (minute === 0) {
        setIsPlay(false);
      } else {
        setMinute((min) => min - 1);
        setSecond(59);
      }
    }
  };

  useEffect(() => {
    console.log(minute, second);

    if (second === 0) {
      clearInterval(timerInterval);
    }
  }, [minute, second]);

  useEffect(() => {
    if (isPlay) {
      setTimerInterval(setInterval(tick, 1000));
    }
  }, [isPlay]);

  const handleResetClick = () => {
    setSecond(delay);
  };

  const handlePrevClick = () => {};

  const handlePauseClick = () => {
    setIsPlay(false);
    clearInterval(timerInterval);
  };

  const handlePlayClick = () => {
    setIsPlay(true);
  };

  const handleNextClick = () => {
    console.log(minute, second);
  };
  return (
    <>
      <TimerBox>
        {minute < 10 ? `0${minute}` : minute}:
        {second < 10 ? `0${second}` : second}
      </TimerBox>
      <Controls>
        <ul>
          <li>
            <button>이전</button>
            <button onClick={handleResetClick}>초기화</button>
          </li>
          <li className="on">
            {!isPlay ? (
              <button onClick={handlePlayClick}>재생</button>
            ) : (
              <button onClick={handlePauseClick}>일시정지</button>
            )}
          </li>
          <li>
            <button onClick={handleNextClick}>앞으로</button>
          </li>
        </ul>
      </Controls>
    </>
  );
};

export default Timer;

재생 버튼을 누르면 setInterval 이 실행되는 위와같은 형식의 코드였는데

setInterval 실행시에 내부에 클로저가 발생해서 second 와 minute 값이 0으로 고정되는 문제가 발생했다 ...

해결하기

비슷한 문제를 겪는 사람들이 많았는데

그 중에 한 블로그 글을 참고했다.

useInterval 사용하기

useInterval 은 setInterval 의 문제점을 개선하고자 Dan 형님이 만드신 커스텀 훅인데

import React, { useState, useEffect, useRef } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

위와 같은 구성으로 되어있고 동작방식은 setInterval 과 같으나 다른점은

Interval 동작시에 인자가 동적 이라는 점이다..!

내가 딱 필요로 했던 코드(반복 동작하는데 인자(시간)가 동적인)였기때문에
바로 적용해보았다

import { useEffect, useRef, useState } from "react";
import styled from "styled-components";

function useInterval(callback, delay) {
  const savedCallback = useRef(); // 최근에 들어온 callback을 저장할 ref를 하나 만든다.

  useEffect(() => {
    savedCallback.current = callback; // callback이 바뀔 때마다 ref를 업데이트 해준다.
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current(); // tick이 실행되면 callback 함수를 실행시킨다.
    }
    if (delay !== null) {
      // 만약 delay가 null이 아니라면
      let id = setInterval(tick, delay); // delay에 맞추어 interval을 새로 실행시킨다.
      return () => clearInterval(id); // unmount될 때 clearInterval을 해준다.
    }
  }, [delay]); // delay가 바뀔 때마다 새로 실행된다.
}

const TimerBox = styled.div``;

const Controls = styled.div``;

const Timer = ({ delay }) => {
  const [isPlay, setIsPlay] = useState(false);
  const [minute, setMinute] = useState(0);
  const [second, setSecond] = useState(0);
  const [timerInterval, setTimerInterval] = useState(0);

  const tick = () => {
    if (second > 0) {
      setSecond((sec) => sec - 1);
    }

    if (second === 0) {
      if (minute === 0) {
        setIsPlay(false);
      } else {
        setMinute((min) => min - 1);
        setSecond(59);
      }
    }
  };

  const getTime = () => {
    setMinute(parseInt(delay / 60));
    setSecond(parseInt(delay % 60));
  };

  const customInterval = useInterval(
    () => {
      tick();
    },
    isPlay ? 1000 : null
  );

  useEffect(() => {
    getTime();
  }, []);

  useEffect(() => {
    getTime();
  }, [delay]);

  useEffect(() => {
    if (second === 0) {
      clearInterval(timerInterval);
    }
  }, [minute, second]);

  useEffect(() => {
    if (isPlay) {
      setTimerInterval(customInterval);
    }
  }, [isPlay]);

  const handleResetClick = () => {
    getTime();
  };

  const handlePrevClick = () => {};

  const handlePauseClick = () => {
    setIsPlay(false);
    clearInterval(timerInterval);
  };

  const handlePlayClick = () => {
    setIsPlay(true);
  };

  const handleNextClick = () => {
    console.log(minute, second);
  };
  return (
    <>
      <TimerBox>
        {minute < 10 ? `0${minute}` : minute}:
        {second < 10 ? `0${second}` : second}
      </TimerBox>
      <Controls>
        <ul>
          <li>
            <button>이전</button>
            <button onClick={handleResetClick}>초기화</button>
          </li>
          <li className="on">
            {!isPlay ? (
              <button onClick={handlePlayClick}>재생</button>
            ) : (
              <button onClick={handlePauseClick}>일시정지</button>
            )}
          </li>
          <li>
            <button onClick={handleNextClick}>앞으로</button>
          </li>
        </ul>
      </Controls>
    </>
  );
};

export default Timer;

이렇게 타이머를 완성했다

profile
깨지고 부서지는

0개의 댓글