디지털 시계는 현재 시간을 읽고 일정 주기로 화면을 갱신하는 구조입니다. 가장 단순하게는 1초마다 timer를 실행해 현재 시간을 읽고 화면을 갱신할 수 있습니다.
다만 setInterval(1000)이나 sleep(1)은 콜백을 정확히 1초 뒤에 실행한다는 보장이 아닙니다. 실행 가능한 가장 이른 시점을 예약하는 것에 가깝고, main thread가 바쁘거나 event loop가 밀리면 콜백 실행이 늦어질 수 있습니다.
그래서 매 tick마다 “이전 값에 1초를 더한 시간”이 아니라 시스템의 현재 시간을 다시 읽어 표시하는 편이 좋습니다. 더 정확히 하려면 다음 초 경계까지 남은 시간만큼 timeout을 걸어 drift를 줄일 수 있습니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
setInterval의 문제는?
setInterval은 callback을 정확한 시각에 실행한다고 보장하지 않습니다. main thread가 긴 작업을 처리 중이거나 event loop에 앞선 작업이 쌓여 있으면 callback 실행이 늦어질 수 있습니다.
이 지연이 반복되면 tick이 정확히 1초 간격으로 실행되지 않고, tick 횟수로 계산한 시간에 누적 오차가 생깁니다.
오차를 줄이는 방법은?
매번 Date.now 같은 실제 현재 시간을 기준으로 표시하고, 다음 초 경계에 맞춰 setTimeout을 다시 예약할 수 있습니다.
requestAnimationFrame을 쓰면 어떤 장점이 있나요?
requestAnimationFrame은 브라우저의 repaint 주기(보통 60fps)에 맞춰 callback을 호출합니다. 화면 갱신과 렌더링이 동기화되어 불필요한 중간 paint를 줄일 수 있고, tab이 background로 가면 자동으로 멈춰 배터리를 아낄 수 있습니다. 다만 초 단위 시계라면 굳이 60fps로 실행할 이유가 없어 setInterval이나 setTimeout 방식이 더 적합할 수 있습니다.
Background tab에서 timer는 어떻게 달라지나요?
브라우저는 background tab의 timer를 throttle할 수 있습니다. 그래서 화면에 다시 돌아왔을 때 현재 시간을 기준으로 표시를 갱신해야 합니다.
이벤트 루프가 setInterval 정확도에 영향을 주는 이유는?
JavaScript는 single-threaded이고, timer callback은 call stack이 비어야 실행됩니다. setInterval이 예약한 시각에 call stack에 다른 작업이 있으면, 그 작업이 끝날 때까지 callback이 대기하게 됩니다. 이 때문에 timer 간격이 일정하게 보장되지 않습니다.
시간 표시와 tick 저장을 분리해야 하는 이유는?
tick 횟수로 시간을 계산하면 지연이 누적됩니다. 표시할 시간은 실제 현재 시각에서 계산하고, timer는 갱신 trigger로만 쓰는 편이 안전합니다.
시스템 시간(Date.now)과 performance.now()의 차이는?
Date.now()는 Unix epoch 기준의 절대 시각을 밀리초 단위로 반환합니다. performance.now()는 페이지 로드 시점 기준의 상대 시간을 마이크로초 정밀도로 반환하며, 시스템 시간 변경에 영향을 받지 않습니다. 시계 표시처럼 실제 시각이 필요할 때는 Date.now(), 경과 시간 측정이나 성능 측정에는 performance.now()가 적합합니다.
시계 앱에서 탭 전환 후 시간이 튀는 문제를 어떻게 해결하나요?
background에서 throttle된 timer는 foreground 복귀 시 밀린 callback을 한꺼번에 실행해 화면이 순식간에 여러 번 갱신될 수 있습니다. visibilitychange 이벤트를 감지해 tab이 다시 visible 상태가 되면 현재 시각을 즉시 읽어 화면을 갱신하고, timer를 재설정하면 이 문제를 줄일 수 있습니다.
부가 설명
여기서 말하는 문제는 “1초라는 시간이 실제로 달라진다”는 뜻이 아닙니다. 문제는 프로그램이 예약한 timer callback이 정확히 그 시각에 실행되지 않을 수 있다는 점입니다.
예를 들어 setInterval(fn, 1000)은 fn을 매번 정확히 1000ms마다 실행하겠다는 약속이 아닙니다. 브라우저나 런타임은 1000ms가 지난 뒤 실행 가능한 시점에 callback을 queue에 넣습니다. 그 순간 main thread가 긴 작업을 처리 중이면 callback은 그 작업이 끝난 뒤에야 실행됩니다.