이메일 인증이나 비밀번호 찾기 같은 기능에서 사용할 타이머를 만들었다. 특정 시간 내에 동작해야 하는 기능을 구현하기 위해 타이머 제어 코드가 필요했고, 이를 정리해둔다.
useTimer 훅 만들기 전체 코드
import { useEffect, useRef, useState } from "react";
export default function App() {
const { isRunning, minute, second, start, stop, reset, restart, setTimer } =
useTimer({ initialTime: 120 }); // 초기 2분 (120초) 설정
const [customTime, setCustomTime] = useState(120); // 사용자 지정 시간
return (
<div className="App">
<h1>타이머</h1>
{/* 기본 타이머 표시 */}
<h2>
{String(minute).padStart(2, "0")}:{String(second).padStart(2, "0")}
</h2>
{/* 타이머 컨트롤 버튼 */}
<button onClick={start} disabled={isRunning}>
시작
</button>
<button onClick={stop} disabled={!isRunning}>
정지
</button>
<button onClick={reset}>리셋</button>
<button onClick={restart}>재시작</button>
{/* 사용자 지정 시간 입력 */}
<div>
<input
type="number"
value={customTime}
onChange={(e) => setCustomTime(Number(e.target.value))}
/>
<button
onClick={() => {
setTimer(customTime);
}}
>
시간 설정
</button>
</div>
</div>
);
}
function useTimer({ initialTime }) {
const initTime = useRef(initialTime); // 초기 타이머 값 저장
const targetTime = useRef(0); // 타이머 종료 목표 시간 저장
const stopTime = useRef(0); // 타이머 정지 시 남은 시간 저장
const requestId = useRef(null); // `requestAnimationFrame` ID 저장
const [remainingTime, setRemainingTime] = useState(initialTime);
const [isRunning, setIsRunning] = useState(false);
/**
* 타이머 업데이트 함수
* - 목표 시간과 현재 시간을 비교하여 남은 시간을 계산
* - 시간이 0 이하가 되면 타이머를 자동으로 정지
*/
function updateTimer() {
const newTime = Math.max((targetTime.current - Date.now()) / 1000, 0);
setRemainingTime(newTime);
if (newTime <= 0) {
stop();
} else {
requestId.current = requestAnimationFrame(updateTimer); // 다음 프레임에서 실행
}
}
/**
* 타이머 시작
* - 최초 실행 시 `initTime` 기준으로 목표 시간 설정
* - 정지 후 재시작할 경우 `stopTime`을 기준으로 목표 시간 설정
*/
function start() {
if (targetTime.current === 0) {
targetTime.current = Date.now() + initTime.current * 1000;
} else {
targetTime.current = Date.now() + stopTime.current * 1000;
}
setIsRunning(true);
requestId.current = requestAnimationFrame(updateTimer);
}
/**
* 타이머 정지
* - 현재 남은 시간을 저장하고 `requestAnimationFrame`을 취소하여 중지
*/
function stop() {
stopTime.current = Math.max((targetTime.current - Date.now()) / 1000, 0);
setIsRunning(false);
if (requestId.current) {
cancelAnimationFrame(requestId.current);
requestId.current = null;
}
}
/**
* 타이머 초기화
* - 초기 시간으로 리셋하고 실행 중이면 정지
*/
function reset() {
stop();
targetTime.current = 0;
stopTime.current = 0;
setRemainingTime(initTime.current);
}
/* 타이머 재시작 (초기 시간으로 다시 시작) */
function restart() {
reset();
start();
}
/**
* 새로운 시간 설정
* - 새로운 초기 시간으로 변경하고 리셋
*/
function setTimer(newTime) {
initTime.current = newTime;
setRemainingTime(newTime);
targetTime.current = 0; // 목표 시간 초기화
stopTime.current = 0; // 정지된 시간 초기화
}
/**
* - 언마운트 시 `requestAnimationFrame`을 정리하여 메모리 누수 방지
*/
useEffect(() => {
return () => {
if (requestId.current) cancelAnimationFrame(requestId.current);
};
}, []);
return {
isRunning,
minute: Math.floor(remainingTime / 60),
second: Math.floor(remainingTime % 60),
start,
stop,
reset,
restart,
setTimer,
};
}
사실 처음에는 RequestAnimationFrame이 아니라 setInterval을 통해서 일정 시간마다 업데이트를 했었는데, 0초에서 interval 시간 만큼 늦을때가 있어서 방식을 변경한 것이다.