카운트다운 방식의 온라인 타이머를 찾아보니 마음에 드는 것이 없었다. 단순히 숫자만 표시하지 말고 카운트다운의 긴박감이 있으면 좋겠는데 말이지. 타이머 프로그램이 어려운 건 아니니 그래서 한번 만들어보기로 했다. 그리고 프로그램 공부하는 사람들을 위해 여기 설명도 올리고. 최종 결과물은 여기를 보도록 한다.

카운트다운 타이머 v1
카운트다운 타이머 v1

요건

카운트다운의 긴박감을 포함해 내가 만들 타이머의 요건을 생각해봤다.

  1. 카운트다운의 느낌을 잘 전달하는 방식은 시간 단위를 레고 블록(?)처럼 쌓아 불을 켜놓고 하나씩 끄는 방식이 좋을 것 같다. 디지털 타이머처럼 숫자판도 보여주고 거기에 블록 무더기를 더 보여주는 것이다.
  2. 시간 단위는 일, 시, 분, 초, 100분의 1초까지 보여주도록 하자. 단, 100분의 1초를 보여주는 건 특히 긴박감을 보여주는 데 좋을 것 같긴 한데 사용자가 설정하는 건 의미가 없겠고 일, 시, 분, 초만 설정할 수 있도록 하자.
  3. 사용자의 타이머 완료 시간을 기억하여 나중에 다시 방문하더라도 유지되게 하자.
  4. 타이머 시간을 기억하는 김에 하나가 아니라 둘 이상도 기억할 수 있으면 어떨까? 그러려면 각 타이머에 명칭도 붙일 수 있어야겠고.

만들기 전에 생각했던 위 요건 중 결과적으로는 3, 4번은 다음으로 미루게 됐다. 일단 1, 2번이 큰 줄거리이므로 계속 진행해보자.

고려 사항

그럼 어떻게 만들어볼까? 생각할 것들이 몇 가지 있겠지.

화면 프로토타입

다음은 화면 프로토타입이다. 디자인하고는 먼 나지만 jQuery UI를 활용해 볼썽 사납지는 않게 만들어졌다. 일, 시, 분, 초, 100분의 1초 숫자판과 눈금 블록, 시작/정지 단추 등이 들어가 있다. 기본적인 사용 방법은 일, 시, 분, 초에 대해 수치를 설정한 후 타이머를 개시하는 것이다. 개시한 후에는 시작/정지 단추가 "정지"로 바뀌어서 정지할 수 있게 하고 시간을 변경할 수 없게 한다. HTML 코딩 내용은 최종 결과물을 참고하도록 한다.

타이머 프로토타입
타이머 프로토타입

기본 기능 구현

수치 설정 방법이 숫자판 슬라이더를 사용하는 것과 숫자판 아래 눈금을 누르는 방법 두가지를 다 고려하려고 하므로 어느 쪽을 설정하든 다른 쪽에도 반영돼야 한다. 여기서 UI 갱신 함수라고 만들 수 있을 것이며 데이터가 바뀌면 그에 관련된 UI는 전부 갱신하는 "뷰"가 된다. 각 UI에서 발생하는 이벤트에 이 함수를 붙여야 한다.

function setTimerDigit($slider, value) {
$slider.slider('value', value).prev().text(value < 10 ? '0' + value : value);
var $marks = $slider.parent().parent().find('.marks div');
var max = $slider.slider('option', 'max');
$marks.slice(0, value).css('background-color', 'yellow'); // 설정 값까지의 눈금은 노란색
if (value < max)
$marks.slice(value - max).css('background-color', 'transparent'); // 그 외의 눈금은 색 없음
}

사용자가 설정한 시간 값에서 완료 일시를 구해야 한다. 그래서 완료 일시와 현재 시간을 계속해서 비교해서 UI를 갱신해야 한다. 하루는 24 * 60 * 60 = 86400초고 JavaScript의 Date 객체는 밀리초(1000분의 1초) 단위로 값을 처리한다. 다음은 타이머를 시작할 때의 소스며 콜백에 타이머 갱신 함수도 포함돼 있다.

// times는 일, 시, 분, 초의 설정 값을 배열로 넣어둔 것
endTime = new Date().getTime() + (times[0] * 86400 + times[1] * 3600 + times[2] * 60 + times[3]) * 1000;

var timeoutFunc = function() {
var timeLeft = endTime - new Date().getTime();
...

timeLeft = Math.floor(timeLeft / 1000);
var newTimes = [Math.floor(timeLeft / 86400),
Math.floor(timeLeft % 86400 / 3600),
Math.floor(timeLeft % 3600 / 60),
timeLeft % 60
];

// 직전 시간 배열과 현재 시간 배열을 비교하여 다른 값은 갱신
for (var i = 0; i < 4; ++i) {
if (newTimes[i] != times[i]) {
setTimerDigit($timers.eq(i).next(), newTimes[i]);
}
}
times = newTimes;

hTimer = setTimeout(timeoutFunc, 1);
};
timeoutFunc();

처음엔 setInterval을 사용하기도 했었으나 브라우저나 PC에 따라 성능이 다르므로 일괄적으로 일정 주기로 갱신을 하도록 하는 것은 UI 갱신이 처리를 못 따라갈 수 있는(그에 따라 버벅거리는 게 보이는) 위험이 있다. 각 환경에 맞게 하려면 setTimer를 계속해서 호출해주는 게 맞는 방법이다.

UI 상세화

핵심 기능은 처리했으나 실제 구현하면서 다음과 같은 것들을 고려하여 UI를 상세화해야 했다.

그런데 추가 정보를 표시하면서 원점부터 다시 고려해야할 중대한 요건을 하나 발견하게 되었다.

고정 목표 일시 설정 기능

원래 타이머의 목적은 시간 기간(duration)만 생각해서 예를 들어 라면 타이머로 사용한다든지 작업 시간 타이머로 사용하려는 것이었는데 타이머 정보로 "목표 일시"라는 걸 보여주려고 하면서 새로운 요건이 등장하게 됐다. 타이머의 용도를 기간만 설정하는 것이라면 "목표 일시"는 상대적인 값이 되는데 어느 특정 일시를 먼저 전제로 할 수도 있어야겠다는 것이다. 이것은 날짜가 들어갔기 때문에 특히 필요하다. 크리스마스까지 남은 날 수를 카운트다운하려는데 처음부터 그 날 수를 알기는 어렵지 않겠는가? 컴퓨터가 해줘야 하는 것이다. 그래서 고정된 목표 일시를 설정할 수 있는 기능도 추가하게 됐다.

목표 일시를 어디까지 설정할 수 있을까를 생각해봤는데 크리스마스 카운트다운을 하려면 1년 정도는 돼야겠다는 생각이 들었다. 그리고 어찌하다보니 숫자판 아래 눈금도 그렇게 1년 정도로 만들게 됐다. 자릿수가 많으니 눈금을 작게 했는데 어떻게 하다보니 눈금을 한 줄에 12개씩 표시해야했고 그게 100분의 1초와 줄 수를 맞추니 372일이 나온 것이다. 개발하다보면 이런 의도하지 않은 결과도 생긴다. 좀더 장기의 날짜를 선택하려면 어떻게 할지가 그래서 미제로 남는다.

프로젝트를 하면서도 공기 후반에 이런 새로운 요건이 발생하는데 내가 혼자 하는 경우도 역시 예외는 아니다. 사람이 처음부터 모든 걸 생각해낼 수는 없기 때문이다. 아무튼 그래서 목표 일시를 누르면 달력과 시간 팝업을 열어 설정하는 기능을 추가하게 됐다.

기타

이상으로 온라인 타이머 제작 후기를 마친다. 다시 한번 결과물은 여기를 확인하고 버그가 있으면 가차 없이 알려주시길!