지난 포스팅에서 자바스크립트로 OX 퀴즈를 만들며 Timer API의 기초를 다뤄보았습니다. 많은 분이 타이머 기능에 관심을 가져주셨는데요. 오늘은 그 연장선상에서 조금 더 정밀한 ‘자바스크립트 스톱워치(Stopwatch)’를 만들어보려 합니다.
단순히 1초씩 줄어드는 타이머와 달리, 스톱워치는 0.01초(밀리초) 단위까지 실시간으로 화면에 뿌려줘야 합니다. 이 과정에서 우리는 자바스크립트의 비동기 처리와 메모리 관리, 그리고 더 나아가 사용자 경험(UX)을 위한 버튼 상태 제어까지 깊이 있게 학습할 수 있습니다.
“requestAnimationFrame과 setInterval 중 무엇을 사용해야 하는가?”와 상황에 맞게 start, stop, reset 버튼을 활성화하고 비활성화하는 방법, 숫자를 00 : 00 형식으로 포맷팅하는 방법까지 추가로 알아볼 생각입니다.

1. 자바스크립트 스톱워치 로직 설계
스톱워치를 구현할 때는 크게 세 가지 상태를 정의해야 합니다.
- Running (실행 중): 시간이 계속 흐르며 화면이 0.01초 단위로 갱신됩니다.
- Stopped (정지): 흐르던 시간이 멈추고, 그 시점의 기록이 화면에 고정됩니다.
- Reset (초기화): 모든 기록을 00:00:00으로 되돌립니다.
여기서 가장 중요한 포인트는 “정지했다가 다시 시작했을 때, 이전 기록에 이어서 시간이 흘러야 한다”는 점입니다. 이를 위해 우리는 ‘시작 시간’과 ‘누적 시간’이라는 두 개의 변수를 전략적으로 활용할 것입니다.
2. 자바스크립트 스톱워치 코드
① 밀리초 단위의 정밀도 확보
일반적인 setInterval은 시스템 부하에 따라 약간의 오차가 발생할 수 있습니다. 그래서 이번에도 Date.now()를 활용해 절대적인 시간 차이를 계산합니다.
function updateTime() {
const now = Date.now();
elapsedTime = now - startTime + savedTime;
display.textContent = formatTime(elapsedTime);
}
여기서 savedTime은 일시정지를 눌렀을 때까지의 시간을 저장하는 변수입니다. 다시 시작할 때 이 값을 더해주어야만 시간이 끊기지 않고 자연스럽게 이어집니다.
② 숫자 포맷팅: padStart()의 마법
시간이 1일 때 01로 보이게 하는 것은 가독성 측면에서 매우 중요합니다. 과거에는 조건문을 복잡하게 썼지만, 최신 자바스크립트에서는 padStart(2, ‘0’) 함수 하나로 아주 깔끔하게 처리할 수 있습니다. 이 부분은 코딩 테스트에서도 자주 활용되니 꼭 기억해 두세요!
function timeToString(time) {
let diffInMin = time / (1000 * 60);
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let diffInMs = (diffInSec - ss) * 100;
let ms = Math.floor(diffInMs);
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString().padStart(2, "0");
return `${formattedMM}:${formattedSS}.${formattedMS}`;
}
3. 버튼 비활성화
초보자와 숙련자의 차이는 ‘예외 처리’에서 옵니다. 스톱워치가 이미 실행 중인데 ‘Start’ 버튼을 또 누르면 어떻게 될까요? 타이머가 중첩되어 시간이 제멋대로 흐르게 됩니다.
이를 방지하기 위해 button.disabled = true; 속성을 활용하여 각각의 상황에 맞는 버튼만 활성화되도록 제어했습니다.
function showButton(state) {
if (state === "RUNNING") {
startBtn.disabled = true;
stopBtn.disabled = false;
} else if (state === "STOPPED") {
startBtn.disabled = false;
stopBtn.disabled = true;
} else {
startBtn.disabled = false;
stopBtn.disabled = true;
}
}
4. requestAnimationFrame vs setInterval
자바스크립트의 requestAnimationFrame과 setInterval 중 무엇을 써야 할까요? 보통 0.01초 단위의 정밀한 그래픽 갱신이 필요하다면 브라우저의 주사율에 맞춘 requestAnimationFrame이 유리합니다. 하지만 학습용 스톱워치에서는 setInterval로도 충분히 훌륭한 결과물을 낼 수 있습니다. 다만, 브라우저 탭이 백그라운드로 이동할 때 타이머가 느려지는 현상을 방지하려면 앞서 언급한 Date.now() 기반의 시간 계산 방식이 필수적입니다.
5. 실행 영상

6. 더하기
① 랩 타임(Lap Time) 기능 추가
스톱워치에서 가장 유용한 기능 중 하나는 특정 시점의 기록을 남기는 ‘랩 타임’입니다.
버튼을 누를 때마다 현재 시간을 리스트 형태로 표시해주는 기능을 추가하면 조금 더 완성도 있는 프로젝트를 만들 수 있을 것입니다.
② 로컬 스토리지(LocalStorage)로 데이터 보존
현재 코드는 페이지를 새로고침하면 측정 중이던 기록이 모두 사라집니다.
localStorage에 저장하고, 다시 접속했을 때 그 기록을 불러오는 기능을 추가해 보세요.
7. 참고
오늘 우리는 자바스크립트를 활용해 단순히 숫자가 올라가는 타이머를 넘어, Date.now()를 이용한 정밀한 시간 측정 원리와 UX를 고려한 버튼 제어까지 함께 살펴보았습니다.
자바스크립트 스톱워치 프로젝트는 자바스크립트의 비동기 처리(Asynchronous)와 이벤트 루프(Event Loop)를 이해하는 첫 단추와 같습니다. 브라우저가 화면을 그리는 찰나의 순간에도 우리가 짠 코드가 어떻게 시간을 계산하고 화면에 반영하는지 배울 수 있는 좋은 예제입니다.
코딩 공부를 하다 보면 가끔 “이런 간단한 도구를 만드는 게 정말 도움이 될까?”라는 의문이 들 때가 있습니다. 하지만 오늘 우리가 구현한 ‘숫자 포맷팅’이나 ‘상태 제어’ 로직은 나중에 거대한 웹 애플리케이션을 만들 때도 변함없이 사용되는 핵심 기술입니다.
다음 포스팅에서는 이어서 ‘데이터를 기억하는 법’에 대해 다뤄보려 합니다. 우리가 만든 스톱워치나 할 일 목록(To-do List)이 새로고침 후에도 유지되려면 브라우저 저장소를 어떻게 활용해야 하는지, 실무에서 필수적인 Web Storage API를 주제로 찾아오겠습니다.