[웹으로 낙서하기#3]자바스크립트로 스탑워치를 만들어보자!

2020. 7. 30. 18:30웹돌이 단편선

2020/07/23 - [웹돌이 단편선] - [웹으로 낙서하기#1]자바스크립트를 사용해서 웹 브라우저에 디지털 시계를 올려놔보자!

2020/07/27 - [웹돌이 단편선] - [웹으로 낙서하기#2]디지털 시계의 시간을 흐르게 해보자!

이전 글에서는 디지털 시계를 만들고, 시간을 흐르게 해봤다. 이번 시간에는 디지털 시계의 외형과, 저번 글의 재귀호출에 영감을 받아, 스탑워치를 만들어보겠다.

 

스탑워치는 시간이 휙휙 빠르게 올라가는 모습이 상징적이라고 할수 있다. 이부분을 window.requestAnimationFrame을 사용하여 구현해보겠다.

 

1차적 구현

import React, { useState, useCallback, useEffect, useRef } from "react";

function StopWatch(props) {
  const [active, setActive] = useState(false);
  const activeRef = useRef(active);
  useEffect(() => {
    activeRef.current = active;
  }, [active]);

  const [startDate, setStartDate] = useState(new Date());
  const startDateRef = useRef(startDate);
  useEffect(() => {
    startDateRef.current = startDate;
  }, [startDate]);

  const [pausedTime, setPausedTime] = useState(0);
  const pausedTimeRef = useRef(pausedTime);
  useEffect(() => {
    pausedTimeRef.current = pausedTime;
  }, [pausedTime]);

  const [lapTimes, setLapTimes] = useState([]);
  const [elapsedTime, setElapsedTime] = useState(
    new Date() - startDate + pausedTime
  );

  const tick = useRef(() => {
    if (activeRef.current) {
      setElapsedTime(new Date() - startDateRef.current + pausedTimeRef.current);
      requestAnimationFrame(tick.current);
    }
  });

  useEffect(() => {
    requestAnimationFrame(tick.current);
  }, [active]);

  const handleStartClick = useCallback(() => {
    if (!active) {
      setActive(true);
      setStartDate(new Date());
    }
  }, [active]);
  const handleStopClick = useCallback(() => {
    if (active) {
      setActive(false);
      setPausedTime(elapsedTime);
    }
  }, [active, elapsedTime]);
  const handleLapTimeClick = useCallback(() => {
    if (active) {
      const newLapTimes = Array.from(lapTimes);
      newLapTimes.push(new Date() - startDate + pausedTime);
      setLapTimes(newLapTimes);
    }
  }, [active, lapTimes, pausedTime, startDate]);
  const handleResetClick = useRef(() => {
    setActive(false);
    setStartDate(new Date());
    setElapsedTime(0);
    setPausedTime(0);
    setLapTimes([]);
  });

  return (
    <>
      <div>
        <div>Elapsed Times</div>
        {elapsedTime}
      </div>
      <button onClick={handleStartClick}>Start</button>
      <button onClick={handleStopClick}>Stop</button>
      <button onClick={handleLapTimeClick}>Lap Time</button>
      <button onClick={handleResetClick.current}>Reset</button>
      <ol>
        {lapTimes.map((time, i) => (
          <li key={i}>{time}</li>
        ))}
      </ol>
    </>
  );
}

export default StopWatch;

1차적으로 시작, 정지, 랩타임, 초기화가 가능한 스탑워치를 만들어봤다. 시간 포맷은 일단 밀리초 단위로 보여주고 있다.


2차적 구현

2차적으로는 밀리초 단위의 시간 흐름을 일:시:분:초.밀리초 단위로 보여주는 것이다.

Date의 인스턴스간 빼기 연산시 두 인스턴스간 시간차를 밀리초 단위로 반환하기 때문에 밀리초 정수로부터 시간을 추출해내는 헬퍼함수를 만들어두면 요긴할 것 같다.

function getTimesFromMillis(source) {
  const timeUnits = [
    ["millis", 1000],
    ["seconds", 60],
    ["minutes", 60],
    ["hours", 24],
    ["days", 7],
    ["weeks", 52],
    ["years"],
  ];

  return timeUnits.reduce((acc, [unitKey, unitValue]) => {
    if (unitValue) {
      const value = source % unitValue;
      source = (source - value) / unitValue;
      return Object.assign({}, acc, { [unitKey]: value });
    }
    return Object.assign({}, acc, { [unitKey]: source });
  }, {});
}

밀리초로부터 시간을 가져오는 함수

위와 같이 getTimesFromMillis 함수를 정의하였고, millis 속성을 받아 함수 호출후 일:시:분:초.밀리초 단위로 시간 단위를 분배하여 보여주는 컴포넌트를 추가하면 될것같다.


3차적 구현

마지막으로 숫자를 이전시간에 만든 Digit 컴포넌트를 사용하여 표현하겠다.

이전 단계에서 getTimesFromMillis를 통해 시간 정보를 추출해낼수 있게 됐으니 시간정보를 Digit컴포넌트의 열거형으로 보여주는 Time 컴포넌트를 추가해보았다.

Time.js

import React from "react";
import PropTypes from "prop-types";
import getZeroPadString from "../../../helpers/getZeroPadString";
import mapDigit from "../../../helpers/mapDigit";

/* getZeroPadString, mapDigit는 이전글에서 다뤘던 내용이라 따로 다루지는 않겠다. */

function Time({ years, weeks, days, hours, minutes, seconds, millis }) {
  const yearsString = years.toString();
  const weeksString = weeks.toString();
  const daysString = days.toString();
  const hoursString = getZeroPadString(hours);
  const minutesString = getZeroPadString(minutes);
  const secondsString = getZeroPadString(seconds);
  const millisString = getZeroPadString(millis, 2);

  return (
    <>
      {Array.from(yearsString).map(mapDigit)}:
      {Array.from(weeksString).map(mapDigit)}:
      {Array.from(daysString).map(mapDigit)}:
      {Array.from(hoursString).map(mapDigit)}:
      {Array.from(minutesString).map(mapDigit)}:
      {Array.from(secondsString).map(mapDigit)}.
      {Array.from(millisString).map(mapDigit)}
    </>
  );
}

Time.defaultProps = {
  years: 0,
  weeks: 0,
  days: 0,
  hours: 0,
  minutes: 0,
  seconds: 0,
  millis: 0,
};
Time.propTypes = {
  years: PropTypes.number,
  weeks: PropTypes.number,
  days: PropTypes.number,
  hours: PropTypes.number,
  minutes: PropTypes.number,
  seconds: PropTypes.number,
  millis: PropTypes.number,
};

export default Time;

그리고, StopWatch의 jsx 반환 부분을 다음과 같이 변경하였다.

    <div className={c["wrapper"]}>
      <div>
        <div>Elapsed Times</div>
        <Time {...getTimesFromMillis(elapsedTime)} />
      </div>
      <button onClick={handleStartClick}>Start</button>
      <button onClick={handleStopClick}>Stop</button>
      <button onClick={handleLapTimeClick}>Lap Time</button>
      <button onClick={handleResetClick.current}>Reset</button>
      <ol>
        {lapTimes.map((time, i) => (
          <li key={i}>
            <Time {...getTimesFromMillis(time)} />
          </li>
        ))}
      </ol>
    </div>

결과를 보자.

연:주:일:시:분:초.밀리초 단위로 Digit 컴포넌트를 사용해서 보여줬다.

좀더 가다듬어서 웹 위젯을 구성해서 개인 웹앱에 추가하면 나름 쓸만한 곳이 있지 않을까 싶다.