timerclockevent-loop

초 단위 디지털 시계를 구현한다면 어떻게 하겠나요?

예상 시간
7
30초 답변

꼬리질문

조금 더 깊게 물어본다면

답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.

setInterval의 문제는?

부가 설명

여기서 말하는 문제는 “1초라는 시간이 실제로 달라진다”는 뜻이 아닙니다. 문제는 프로그램이 예약한 timer callback이 정확히 그 시각에 실행되지 않을 수 있다는 점입니다.

예를 들어 setInterval(fn, 1000)fn을 매번 정확히 1000ms마다 실행하겠다는 약속이 아닙니다. 브라우저나 런타임은 1000ms가 지난 뒤 실행 가능한 시점에 callback을 queue에 넣습니다. 그 순간 main thread가 긴 작업을 처리 중이면 callback은 그 작업이 끝난 뒤에야 실행됩니다.

이 상태에서 시계를 tick 횟수로 계산하면 오차가 쌓입니다.

let seconds = 0;
 
setInterval(() => {
  seconds += 1;
  render(seconds);
}, 1000);

위 코드는 callback이 조금씩 늦게 실행돼도 매번 1초만 지났다고 가정합니다. 1000ms마다 정확히 실행되면 문제가 없지만, 실제 실행 간격이 1010ms, 1020ms처럼 밀리면 표시 시간이 실제 시간보다 뒤처집니다.

그래서 timer는 시간을 계산하는 기준이 아니라 화면을 다시 그리는 trigger로 보는 편이 안전합니다.

function renderClock() {
  const now = new Date();
  render(now);
}
 
setInterval(renderClock, 1000);

이렇게 하면 callback이 조금 늦게 실행되더라도 그 시점의 실제 현재 시간을 다시 읽습니다. 표시가 잠깐 늦게 갱신될 수는 있지만, 시간이 누적해서 틀어지지는 않습니다.

더 정확히 맞추려면 다음 초 경계까지 남은 시간만큼 setTimeout을 다시 예약할 수 있습니다.

function tick() {
  const now = new Date();
  render(now);
 
  const delay = 1000 - now.getMilliseconds();
  setTimeout(tick, delay);
}
 
tick();

브라우저에서는 tab이 background로 가면 timer가 throttle될 수 있습니다. 그래서 화면에 다시 돌아왔을 때도 tick 횟수를 믿기보다 현재 시간을 다시 읽어 표시해야 합니다.

시계는 시간 계산의 기준과 화면 갱신 주기를 분리해서 생각하는 것이 중요합니다.

한 줄 정리

현재 시간을 주기적으로 읽어 화면에 표시하되, setInterval만 믿지 말고 실제 시각 기준으로 drift를 보정해야 합니다.