플립 시계 만들기 with JS (2020)

Wonkook Lee·2021년 9월 4일
66

Toy Projects

목록 보기
4/4


플립 시계 만들기


아날로그를 닮고 싶은 디지털 🕰

숫자판이 틱,틱 소리를 내며 가벼이 넘어간다. 지금 이 순간 흐르는 시간이 내는 소리다. 우리를 둘러싼 수 많은 장치들이 아날로그에서 디지털로 바뀌어 버린것도 이젠 오래 전이 되어버렸다. 시대가 변하고 우리는 새로운 세상에 완전히 적응해버린 듯 해도 문득 아날로그만의 독특한 감성의 결이 그리워진다.

스플릿 플랩 디스플레이(Split-Flap Display)라고 불리는 우리에게 익숙한 이 기계적 표현 방식은 실용성과는 거리가 있지만 디지털 인터페이스 시대에도 즐겨 사용되는 UI 중 하나이다.

스플릿 플랩 디스플레이로 표현된 시계는 어떻게 만들 수 있을까? 호기심이 나를 이끌었었다. 한 해 전 취미 삼아 가볍게 배웠던 자바스크립트로 만든 나의 작고 보잘 것 없는 장난감을 소개한다.

지금 보면 바보같은 코드이기 때문에 혹여라도 참고하지 않으셨으면 좋겠다.
이 글은 '제가 이런걸 만들었습니다 여러분!' 보단 '이런걸 만들기도 했었지요.'에 가깝다




매커니즘 ⚙️

메커니즘은 매우 간단하다. 구동 축이 동일한 방향으로, 매 초마다 입력을 받아서 설정된 각도만큼 회전하며, 궤도에 연결된 숫자판(Flap)이 하나씩 뒤집어지면서 다음 숫자를 보여준다.
각 숫자판은 반으로 쪼개져 있으며, 뒷면엔 다음 숫자의 절반이 표시되어 있다.

외부에서 보여지는 물리적 동작만 화면으로 표현하기 위해 아래와 같이 규칙을 정했다.

규칙

  1. 각 플랩은 0부터 9까지 구성되어있으며 오름차순으로 표시된다.
  2. 컴포넌트 별 최대 표시값 : Hour = 24, Min = 60, Sec = 60이며 최대 값을 넘기면 상위 파트의 숫자가 증가한다. (플립 UI작동)
  3. 최대값을 초과하면 다시 0으로 리셋된다.
  4. 제일 작은 표시 단위인 오른쪽 Sec 플랩에만 Date 객체의 값을 전달하고, setInterval 함수로 구동한다.



구성 🎛

크게 시, 분, 초로 나누고 Upper-Left / Lower-Left / Upper-Right / Lower-Right로 플랩을 나눈다.

보일지 모르겠지만 DOM node Assigning을 보면 각 플랩마다 요소가 한 개 더 있는것을 볼 수 있다.

숫자판이 내려오면서 Upper Plate는 다음 숫자가 보이고, Lower Plate는 플립 애니메이션이 끝날때 까지 현재 숫자를 유지해야 하는 엇박자를 해결해야 했다. 그래서 매 초마다 보여져야 할 숫자를 표시하는 Backplate와 플립 애니메이션 효과를 위한 Foreplate로 레이어를 나눴다.




구현 🎶


Image vs Number

각 숫자를 반으로 나눈 이미지들을 준비해서 매 초마다 img 태그 source의 주소값을 데이터셋을 참조하여 바꿔줄까 고민했지만, 결정적으로 이미지는 CSS 애니메이션 중 RotateX 효과에 적합하지 않았던 것으로 기억한다.
이미지 대신 Number를 그대로 표현하기로 했으며, 간결한 레터 레이아웃을 위해 Helvetica로 자간을 맞춰가며 적용했다.


Logic

지금 보면 setInterval에 욱여 넣은 무지막지한 코드 블럭이지만..
설명하자면 Date 객체로부터 시간 단위별 값을 가져오는 것으로 시작한다. 한 자리 단위일 때 앞에 무조건 0이 붙어야 해서 10시 이전에는 강제로 0을 붙였다. 그땐 24시간제로 값을 가져올 수 있는 걸 몰랐었다.
1초마다 시간을 가져오다가 타이밍이 어긋나는 경우가 있어서 Interval은 500ms 간격으로 설정했다.

let h0, h1, m0, m1, s0, s1, hDigit, minDigit, secDigit;

const realTime = setInterval(() => {
  const getTime = new Date();

  // HOUR
  const hour = getTime.getHours();
  hDigit = hour.toString();
  h0 = hDigit.charAt(0);
  h1 = hDigit.charAt(1);
  if (hDigit < 10) {
    h0 = 0;
    h1 = hDigit;
  }

  // MIN
  const min = getTime.getMinutes();
  minDigit = min.toString();
  m0 = minDigit.charAt(0);
  m1 = minDigit.charAt(1);
  if (minDigit < 10) {
    m0 = 0;
    m1 = minDigit;
  }

  // SEC
  const sec = getTime.getSeconds();
  secDigit = sec.toString();
  s0 = secDigit.charAt(0);
  s1 = secDigit.charAt(1);
  if (secDigit < 10) {
    s0 = 0;
    s1 = secDigit;
  }
}, 500);

아래는 숫자가 바뀔때마다 플랩 애니메이션을 적용하고 해제하기 위한 클래스 토글 함수다.

// FLIP UI
const flipUI = (ele1, ele2) => {
  ele1.classList.remove('active');
  ele2.classList.remove('active');
  void ele1.offsetWidth;
  void ele2.offsetWidth;
  ele1.classList.add('active');
  ele2.classList.add('active');
}

setInterval의 향연..
각 단위별 로직이 조금씩 다르지만 부끄러워서 Sec 로직만 적었다. 각 숫자 단위가 어떻게 달라지는지에 따라 표현을 조건별로 분기하여 나름 애썼던 모습이 보인다.

// SEC FLIPPER
const countSec = setInterval(() => {
  if (s1 != 0) {
    flipUI(upFlipSecR, loFlipSecR);
  } else {
    flipUI(upFlipSecL, loFlipSecL);
    flipUI(upFlipSecR, loFlipSecR);
  }
  if (s1 % 10 === 0) {
    flipUI(upFlipSecL, loFlipSecL);
  }
  upSecL.textContent = s0;
  loFlipSecL.textContent = s0;
  upSecR.textContent = s1;
  loFlipSecR.textContent = s1;
  delaySec(s0, s1);
}, 1000)

const delaySec = function (c, d) {
  setTimeout(() => {
    upFlipSecL.textContent = c;
    loSecL.textContent = c;
    upFlipSecR.textContent = d;
    loSecR.textContent = d;
  }, 800);
}



테스트 및 최종 적용

자연스럽게 앞 자리 단위가 오르는 것을 보고 즐거워했던 생각이 난다. 플랩이 회전하는 효과는 CSS rotateX를 사용했고 원근감 효과를 위해 perspective 프로퍼티를 사용했다.

시간은 쉼없이 멈추지 않고 흐른다. 하나씩 덮혀가는 플랩들을 바라보면 추억도 한꺼풀씩 다른 기억들로 덮여가는 느낌을 받는다.




글과 그림 ⓒ Wonkook Lee

🙏🏻 잘못된 정보가 있다면 지적해주세요

profile
© 가치 지향 프론트엔드 개발자

4개의 댓글

comment-user-thumbnail
2021년 9월 11일

초반 Digit부분을 string length 체크 후 padStart로 교체해도 좋을 듯 합니다!

1개의 답글
comment-user-thumbnail
2021년 12월 8일

진짜 토이 프로젝트 선정을 참 잘하시는 것 같아요!

1개의 답글