아주아주아주 게으르게 진행하고 있는 프로젝트가 있다.
workout 루틴을 기록하고 친구들과 공유할 수 있도록 하는 mobile application인데, 어쩌다 보니 동기부여가 팍 죽어버린.. 아쉬운 녀석이다.
이게 원래는 동아리원들끼리 같이하는 팀 프로젝트지만,, 다들 FE BE 환경에 익숙치 않아 그 부분에 대한 공부를 선행해야하는 상황이라 지금은 그냥 나 혼자 FE 부분만 깨작깨작 만들어보는 중이다.
(FE는 react native - expo, BE는 node - express - mysql - sequelize로 개발할 예정이었음.)
사실 혼자서 하더라도 빡세게 끝내버렸으면 지금쯤 FE는 다 끝냈고 BE 부분도 얼추 진행해 놓았을 텐데, 나의 게으름이 끝이 없었고 아직도 코드는 프론트 단에서 어물쩍거리고 있다 ㅜㅜ
대충 이런식의 milli-second 단위로 표시되는 타이머를 만들고자 했다.
용도는 세트 간 휴식시간 체크를 위해서.
시간 추가 버튼을 통해서 10초나 30초 추가할 수 있고, 피그마에는 빼먹었지만 시작 버튼과 초기화 버튼도 들어간다.
npm에 타이머 기능을 구현한 패키지도 있었지만 업데이트 한지 3년이나 지났기도 했고, 이 정도는 React와 약간의 vanilla JS로 금방 만들겠다 싶어서 직접 구현하기로 한다.
맨 처음 생각해낸 방식은
useState로 남은 시간을 저장하는 timer
state를 관리한다.
시작 버튼의 onPress 이벤트로 setInterval()
을 지정한다.
setInterval()
의 첫번째 parameter는 timer
의 값을 0.01씩 줄여주는 함수일시정지 버튼을 누르거나, timer
의 값이 0 이하로 떨어질 경우 clearInterval()
을 실행하여 돌아가고있는 interval을 정지.
대충 방식은 맞았지만 clearInterval()
이 말썽이었다.
// ...
const StartWorkout = () => {
// States
const [timer, setTimer] = useState(0);
const [minute, setMinute] = useState("");
const [second, setSecond] = useState("");
const [milliSecond, setMilliSecond] = useState("");
const [toggleTimer, setToggleTimer] = useState(false);
const [toggleBtnName, setToggleBtnName] = useState("시작");
const [isTimerRunning, setIsTimerRunning] = useState(false);
let timerInterval;
// Hooks
useEffect(() => {
minuteCalculator();
if (timer <= 0) {
setToggleTimer(false);
setIsTimerRunning(false);
}
console.log(timer);
}, [timer])
useEffect(() => {
if (toggleTimer) {
timerInterval = setInterval(timeDecrement, 10);
setIntervalId(timerInterval);
setIsTimerRunning(true);
} else if (!toggleTimer || timer < 0) {
clearInterval(timerInterval);
}
}, [toggleTimer])
useEffect(() => {
if (!isTimerRunning) {
setToggleBtnName('시작');
} else if (isTimerRunning && !toggleTimer) {
setToggleBtnName('다시시작');
} else if (isTimerRunning && toggleTimer) {
setToggleBtnName('일시정지');
}
}, [isTimerRunning, toggleTimer])
// Event Handlers
const addTime = (time) => {
setTimer((prev) => prev + time)
}
const minuteCalculator = () => {
let tempMinute = parseInt(timer / 60).toString();
let tempSecond = timer % 60
let tempSecondre = Math.round((tempSecond / 1)).toString();
let tempMilliSecond = Math.round(((tempSecond % 1) * 100)).toString()
setMinute(tempMinute);
setSecond(tempSecondre);
setMilliSecond(tempMilliSecond);
}
const toggleTimerFunc = () => {
if (toggleTimer) {
setToggleTimer(false)
} else if (!toggleTimer && timer > 0){
setToggleTimer(true)
}
}
const timeDecrement = () => {
setTimer((prev) => prev - 0.01);
}
const clearTime = () => {
setTimer(0);
}
// ... return
구글링으로 setInterval()
이용법을 확인했을 때, 미리 setInterval()
을 담을 변수를 선언한 후 특정 조건을 만족했을 때 clearInterval()
의 parameter로 그 변수를 건네주면 interval이 중단되어야 했다.
아마 react에서 변수를 관리할 때의 문제가 그 이유인 것 같다.
console.log로 확인해보았을 때, timerInterval
에는 해당 setInterval()
메소드의 id 값이 number type이 저장되어있었다.
물론 이는 useEffect Hook 안의 console에서만 확인할 수 있었고, Hook 바깥에서 console.log로 timerInterval
을 출력하면 여전히 undefined 상태였다.
이 것도 scope 문제라 부르는게 맞는지 모르겠지만, 아무튼 이 react에서의 scope 문제는 useState로 id state를 관리해줌으로서 해결할 수 있엇다.
useState
로 intervalId
state를 관리해줌으로서 scope 문제를 해결해준다.
setInterval()
메소드를 할당한 변수를 intervalId
에 업데이트 시켜주고, 특정 조건 달성 시 clearInterval()
의 parameter로 intervalId
를 건네주면 정상적으로 interval이 중단된다.
// ...
const StartWorkout = () => {
// States
const [timer, setTimer] = useState(0);
const [minute, setMinute] = useState("");
const [second, setSecond] = useState("");
const [milliSecond, setMilliSecond] = useState("");
const [toggleTimer, setToggleTimer] = useState(false);
const [toggleBtnName, setToggleBtnName] = useState("시작");
const [isTimerRunning, setIsTimerRunning] = useState(false);
const [intervalId, setIntervalId] = useState(0)
let timerInterval;
// Hooks
useEffect(() => {
minuteCalculator();
if (timer <= 0) {
setToggleTimer(false);
setIsTimerRunning(false);
}
console.log(timer);
}, [timer])
useEffect(() => {
if (toggleTimer) {
const timerInterval = setInterval(timeDecrement, 10);
setIntervalId(timerInterval);
setIsTimerRunning(true);
} else if (!toggleTimer || timer < 0) {
clearInterval(intervalId);
}
}, [toggleTimer])
useEffect(() => {
if (!isTimerRunning) {
setToggleBtnName('시작');
} else if (isTimerRunning && !toggleTimer) {
setToggleBtnName('다시시작');
} else if (isTimerRunning && toggleTimer) {
setToggleBtnName('일시정지');
}
}, [isTimerRunning, toggleTimer])
// Event Handlers
const addTime = (time) => {
setTimer((prev) => prev + time)
}
const minuteCalculator = () => {
let tempMinute = parseInt(timer / 60).toString();
let tempSecond = timer % 60
let tempSecondre = Math.round((tempSecond / 1)).toString();
let tempMilliSecond = Math.round(((tempSecond % 1) * 100)).toString()
setMinute(tempMinute);
setSecond(tempSecondre);
setMilliSecond(tempMilliSecond);
}
const toggleTimerFunc = () => {
if (toggleTimer) {
setToggleTimer(false)
} else if (!toggleTimer && timer > 0){
setToggleTimer(true)
}
}
const timeDecrement = () => {
setTimer((prev) => prev - 0.01);
}
const clearTime = () => {
setTimer(0);
}
// ... return
setInterval()
을 1초 (1000 ms) 단위로 실행시켰을 때는 아무 문제 없이 실행이 되었으나 10 ms 단위로 설정하자 자꾸 시간이 조금씩 밀리는 일이 생기더라.
JavaScript의 event loop가 Single Thread이기 때문에, setInterval()
과 같은 내장된 timer 함수들의 event들이 항상 제 시간에 맞춰서 발생할 수는 없다.
10ms 로 interval을 설정했다면 최소한 10ms에 한 번은 이벤트가 발생하는거지, event loop의 대기열에 따라 그 interval이 10ms가 될 수도 있고 20ms가 될 수도 있는 것이다.
따라서 setInterval()
으로 시간을 직접 세주는 것은 오차가 불가피한 방식이므로 다른 방법을 모색해야 했다.
setInterval()
을 통해 시간을 직접 세기보다는 해당 interval 텀으로 Date.now()
를 통해 현재 system time을 가져와서 시간의 차이를 구하는게 정확한 방식이라고 한다.
Date.now()
로 system time을 가져오는 일이 조금 밀릴 수는 있지만, 여전히 가져오는 건 정확한 system time 이기 때문이다.
일단 근데 timer 문제로 좀 시달렸기 때문에 Date.now()
를 이용해 개선하는 건 다음 시간에 해보려고 한다...
딱히 없고, 문제 해결을 도와준 준호형에게 감사링
게을러서 미안해