말이 많아 서론이 길 수도 있습니다. 내용만 보실 분은 바로 굵은 텍스트로
페이지에 실시간으로 시간 정보를 노출시켜줘야 할 일이 생겼습니다.
아무 생각 없이 new Date로 날짜 정보를 받아와서 가공 후 setInterval으로 1초 마다 실행 후 페이지를 떠날때 clearInterval을 해주려 했고, 실제로 구글링해서 나오는 대부분 실시간 시계들은 다 위와 같은 방법의 예제이기도 합니다.
화면에 보여져야 할 시간 정보는
이 정도라 setInterval을 1초 마다 하기에는 성능상 비효율적일거 같아서 1분으로 셋팅을 하고 코드를 작성했습니다..
화면엔 잘 나왔는데, 문제가 생겼습니다.
1분, 2분, 3분 지켜보면서 확인을 해보니 시간이 점점 밀리고 있습니다...
원인은 만약 유저가 5시 10분 30초에 페이지에 진입했다고 했을때 해당 진입 시점부터 1분 마다 setInterval이 진행돼서 시간이 밀렸던 것 이였습니다..
해결 방법을 고민하고 있을때 팀장님이 지나가다 얘기를 해주셨는데, 거기서 해답을 찾았습니다.
토씨 하나 안틀리고 기억하는 건 아니지만
라고 말씀하셨던 거 같습니다..
그렇습니다. 머리에 전구가 켜졌습니다. 아, 왜 이 생각을 못했지?
바로 requestAnimationFrame을 사용하기로 하고 찾아봤습니다.
(구글에 requestAnimationFrame으로 만든 시계 관련 예제는 자료도 적고, 거의 스톱워치만 나오길래 이 포스팅을 하는 이유이기도 합니다. 근데 만들고 나니 간단한거라 자료가 없던건가 싶기도 합니다..)
구글링 도중 setInterval은 아니지만 setTimeout과 requestAnimationFrame으로 시계를 만들었을때 성능 차이를 보여주는 사이트도 발견하여 링크
기존에 new Date로 각 값을 return해주던 함수를 쓰던게 있어서 두개로 쪼겠습니다.
굳이 안 쪼개셔도 상관 없습니다.
// current date info
function dateInfo(staticDate = new Date()) {
const currentDate = staticDate;
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const day = currentDate.getDay();
const date = currentDate.getDate();
const hour = currentDate.getHours();
const min = currentDate.getMinutes();
return { currentDate, year, month, day, date, hour, min };
}
// set real time
function setRealTime() {
let { currentDate, year, date, hour, min } = dateInfo();
const dateArr = currentDate.toString().split(' ');
const monthStr = dateArr[1];
const dayStr = dateArr[0];
const ampm = hour >= 12 ? 'PM' : 'AM';
hour = modifyTimeUnit(hour);
min = modifyTimeUnit(min);
return { ampm, hour, min, dayStr, monthStr, date, year };
}
dateInfo 함수에서 return해준 month, day를 그대로 안쓰고 new Date()를 문자열로 바꿔서 사용한 이유는
new Date().getMonth나 .getDay는 문자열이 아닌 넘버로 반환되는데 문자열로 돔에 출력을 해줘야 해서입니다.
ampm 변수는 반환 된 .getHour의 값으로(0~23) 오전/오후를 각각 시간에 맞게 할당 해줍니다.
그리고 나서
// modify time unit
function modifyTimeUnit(time) {
if (time < 10) return '0' + time;
return time;
}
위 함수를 통해서 0~9시, 0~9분을 04시, 07분 등으로 변환 해줍니다.
여기까지는 setInterval, setTimeout을 활용한 시계 예제랑 크게 다르지 않습니다.
// requestAnimationFrame Variable
let realTimeFrame;
mdn 문서를 읽어보시면
대부분의 최신 브라우저에서는 성능과 배터리 수명 향상을 위해 requestAnimationFrame() 호출은 백그라운드 탭이나 hidden iframe에서 실행이 중단됩니다.
라고 적혀있지만, 혹시 페이지를 떠나기 전에 requestAnimationFrame을 종료 시켜야 할 상황이 있을수도 있기에 clearInterval을 하는 방식과 동일하게 requestAnimationFrame을 할당 할 변수를 만들어 줬습니다.
이제 실행을 시켜보겠습니다.
// start real time
function startRealTime() {
let { ampm, hour, min, dayStr, monthStr, date, year } = setRealTime();
realTimeFrame = requestAnimationFrame(startRealTime);
// test
document.querySelector('.real_time').innerText = `${ampm} ${hour}:${min} ${dayStr}, ${monthStr} ${date}, ${year}`;
console.log(`${ampm} ${hour}:${min} ${dayStr}, ${monthStr} ${date}, ${year}`);
}
startRealTime();
// 실행 결과
// PM 18:16 Wed, Jan 26, 2022
// ...
startRealTime 함수 안에서 1번에 있던 setRealTime함수를 실행하여 리턴값을 destructuring으로 각 키값이 담긴 변수로 만들어주고
requestAnimationFrame(startRealTime)을 통하여 재귀적으로 실행되게 합니다.
그 후 함수 밖에서 startRealTime함수를 한번 더 실행해 줌으로써 마무리 합니다.
콘솔을 보시면 프레임 단위(초당 60회 정도)로 계속 콘솔이 찍힐 것 이고, 분이 바뀌는 시점엔 .real_time 내부 텍스트도 같이 분이 바뀌는 것을 확인 할 수 있습니다.
requestAnimationFrame(startRealTime) 여기서 requestAnimationFrame(startRealTime()) 이렇게 바로 실행 해버리시면 maximum call stack에러가 발생하니 주의
// stop real time
function stopRealTime() {
cancelAnimationFrame(realTimeFrame);
}
함수를 하나 만든 다음.
위에서 말씀드린 requestAnimationFrame을 할당해논 변수(realTimeFrame)를 cancelAnimationFrame안에 넣어줍니다.
이제 특정 시점에 stopRealTime()만 호출해주시면 requestAnimationFrame은 종료가 됩니다.
테스트를 해보시려면 위 2번의 startRealTime() 이후에
setTimeout(stopRealTime, 3000)
해보시면 3초 후 콘솔에 찍히던 로그가 멈추는 것을 확인할 수 있습니다.
// requestAnimationFrame Variable
let realTimeFrame;
// current date info
function dateInfo(staticDate = new Date()) {
const currentDate = staticDate;
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const day = currentDate.getDay();
const date = currentDate.getDate();
const hour = currentDate.getHours();
const min = currentDate.getMinutes();
return { currentDate, year, month, day, date, hour, min };
}
// set real time
function setRealTime() {
let { currentDate, year, date, hour, min } = dateInfo();
const dateArr = currentDate.toString().split(' ');
const monthStr = dateArr[1];
const dayStr = dateArr[0];
const ampm = hour >= 12 ? 'PM' : 'AM';
hour = modifyTimeUnit(hour);
min = modifyTimeUnit(min);
return { ampm, hour, min, dayStr, monthStr, date, year };
}
// modify time unit
function modifyTimeUnit(time) {
if (time < 10) return '0' + time;
return time;
}
// start real time
function startRealTime() {
let { ampm, hour, min, dayStr, monthStr, date, year } = setRealTime();
realTimeFrame = requestAnimationFrame(startRealTime);
// test
document.querySelector('.real_time').innerText = `${ampm} ${hour}:${min} ${dayStr}, ${monthStr} ${date}, ${year}`;
console.log(`${ampm} ${hour}:${min} ${dayStr}, ${monthStr} ${date}, ${year}`);
}
// stop real time
function stopRealTime() {
cancelAnimationFrame(realTimeFrame);
}
startRealTime();
// stopRealTime(); or setTimeout(stopRealTime, 3000);