JS requestAnimationFrame을 이용한 실시간 시계 만들기

최봉수·2022년 1월 26일
1

말이 많아 서론이 길 수도 있습니다. 내용만 보실 분은 바로 굵은 텍스트로

페이지에 실시간으로 시간 정보를 노출시켜줘야 할 일이 생겼습니다.

아무 생각 없이 new Date로 날짜 정보를 받아와서 가공 후 setInterval으로 1초 마다 실행 후 페이지를 떠날때 clearInterval을 해주려 했고, 실제로 구글링해서 나오는 대부분 실시간 시계들은 다 위와 같은 방법의 예제이기도 합니다.

화면에 보여져야 할 시간 정보는

  • 요일
  • 시간

이 정도라 setInterval을 1초 마다 하기에는 성능상 비효율적일거 같아서 1분으로 셋팅을 하고 코드를 작성했습니다..

화면엔 잘 나왔는데, 문제가 생겼습니다.
1분, 2분, 3분 지켜보면서 확인을 해보니 시간이 점점 밀리고 있습니다...

원인은 만약 유저가 5시 10분 30초에 페이지에 진입했다고 했을때 해당 진입 시점부터 1분 마다 setInterval이 진행돼서 시간이 밀렸던 것 이였습니다..

해결 방법을 고민하고 있을때 팀장님이 지나가다 얘기를 해주셨는데, 거기서 해답을 찾았습니다.
토씨 하나 안틀리고 기억하는 건 아니지만

  • 자바스크립트는 싱글 스레드라 초,분 마다 setInterval로 시계를 만들면 다른 작업이? 먹힐수?있어서 좋은 방법은 아닌거같다
  • requestAnimationFrame같은걸 써서 프레임으로 초를 대신해도 괜찮지 않냐

라고 말씀하셨던 거 같습니다..

그렇습니다. 머리에 전구가 켜졌습니다. 아, 왜 이 생각을 못했지?
바로 requestAnimationFrame을 사용하기로 하고 찾아봤습니다.

(구글에 requestAnimationFrame으로 만든 시계 관련 예제는 자료도 적고, 거의 스톱워치만 나오길래 이 포스팅을 하는 이유이기도 합니다. 근데 만들고 나니 간단한거라 자료가 없던건가 싶기도 합니다..)

구글링 도중 setInterval은 아니지만 setTimeout과 requestAnimationFrame으로 시계를 만들었을때 성능 차이를 보여주는 사이트도 발견하여 링크

1. 현재 시간 세팅

기존에 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을 활용한 시계 예제랑 크게 다르지 않습니다.

2. 실행

// 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에러가 발생하니 주의

3. 원하는 시점에 requestAnimationFrame 종료

// 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);
profile
돈이 좋아

0개의 댓글