웹 앱 내에서 사용할 타이머를 개발중이였는데 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 은 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;
이렇게 타이머를 완성했다