React로 스톱워치를 만들어보자! start, stop, reset, lap

지원·2023년 8월 12일
3

진행하는 프로젝트에서 스톱워치를 구현해야할 일이 있어서 스톱워치를 만들어보았다.

요구사항

  1. start 버튼을 누르면 카운트 시작
  2. stop 버튼을 누르면 시간 카운트 정지
  3. lap 버튼을 누르면 시간 기록
  4. 특정 lap을 삭제하는 기능

구현

1. 대략적인 레이아웃 설명

  • 타이머가 보이는 부분 (여기서는 <Timer />)
  • stop, start, reset 버튼
  • lap들이 나열되는 records 부분
  • 시간 측정이 시작되면 보이는 Lap 버튼
   <Container gap={12}>
      <Timer />
      <StartStopButton>
        {running ? 'Stop' : 'Start'}
      </StartStopButton>
      <ResetButton>Reset</ResetButton>
      <Records />
      <LapButton>Lap</LapButton>
    </Container>

2. 관리해야할 상태들

  • 스톱워치 기능을 구현하기 위해서 3가지 state와 타이머의 실시간 ref를 정의했다.
// 1. 스톱 워치가 작동(running) 하고 있는지 여부
  const [running, setRunning] = useState<boolean>(false);
// 2. 실시간으로 측정되고 있는 시간
  const [time, setTime] = useState<number>(0);
// 3. lap 버튼을 눌렀을 때에 기록된 시간들
  const [laps, setLaps] = useState<Record[]>([]);
// timer의 실시간(?)을 관리하는 변수 
 const intervalRef = useRef<NodeJS.Timeout | undefined>(undefined);

3. 필요한 함수들

필요한 함수는 총 5가지가 있다.

  1. 스톱워치를 시작하고, 정지하는 함수
  2. 스톱워치를 reset 하는 함수
  3. 해당 시간을 Lap에 기록하는 함수
  4. 특정 lap을 삭제하는 기능
  5. 기록된 시간을 0:00:00 형태로 formating 하는 함수

하나하나 차근차근 구현을 해보자.

1. 스톱워치를 시작하고, 정지하는 함수

  • START

    • 버튼을 클릭했을 때, running이 false이면, 1000ms (===1초) 마다 타이머가 실행됨
    • setTime 함수를 호출하여 이전 시간에 1000을 더한 값이 time이 된다.
    • 그리고 running state를 true로 만들어 준다.
  • STOP

    • 버튼을 클릭했을 때, running이 true이면, 현재 실행 중인 타이머를 clearInterval을 사용하여 중지시킨다.
    • running 상태를 false로 변경한다.
  const startStopwatch = () => {
    if (!running) {
      intervalRef.current = setInterval(() => {
        setTime((prevTime) => prevTime + 1000);
      }, 1000);
      setRunning(true);
    } else {
      clearInterval(intervalRef.current);
      setRunning(false);
    }
  };

2. 스톱워치를 reset 하는 함수

reset은 지금까지 변경된 state와 ref를 초기화 시켜 주면 된다.

  const resetStopwatch = () => {
    clearInterval(intervalRef.current);
    setTime(0);
    setLaps([]);
    setRunning(false);
  };

3. 해당 시간을 Lap에 기록하는 함수

나는 삭제 및 DB에 저장 등의 기능을 고려하여, record에 id를 주어 { id : number , lap : number } 의 객체 타입으로 정의하였다.

  • laps가 빈 배열이면 id를 1부터 주고, 그 다음부터는 마지막 id에 1을 추가해준다.
  const recordLap = () => {
    const newLap = {
      id: laps.length === 0 ? 1 : laps[laps.length - 1].id + 1,
      lap: time,
    };
   // patchRecord(newLap); : DB에 해당 기록을 patch하는 로직 
    setLaps((prevLaps) => [...prevLaps, newLap]);
  };

patchRecord 부분은 DB에 해당 기록을 patch하는 로직으로 생략가능!

4. 특정 lap을 삭제하는 기능

삭제하는 기능은 해당 랩을 누르고 삭제버튼을 눌렀을때 id를 받아 해당 id의 기록을 삭제하는 기능이다.

  const deleteLap = (id: number) => {
    const filteredLaps = laps
      .filter((record) => record.id !== id)
      .map((record, index) => ({ ...record, id: index + 1 }));
  //  updateRecords({ laps: filteredLaps }); DB에 기록들을 PATCH하는 로직
    setLaps(filteredLaps);
  };

uploadRecords 부분은 DB에 기록하는 로직이라 생략 가능!

5. 시간을 format 하는 함수

이제 마지막으로 기록을 0:00:00 의 형태로 변환하는 로직이다. time은 지금까지 ms (=> number) 로 업데이트 되고 있었다. 사용자에게 보여주기 위해서는 시/분/초의 계산이 필요하다.

export const formatTime = (timeInMillis: number): string => {
  const hours = Math.floor(timeInMillis / 3600000);
  const minutes = Math.floor((timeInMillis % 3600000) / 60000);
  const seconds = Math.floor((timeInMillis % 60000) / 1000);
  const formattedTime = `${hours}:${minutes < 10 ? '0' : ''}${minutes}:${
    seconds < 10 ? '0' : ''
  }${seconds}`;
  return formattedTime;
};

전체 완성된 코드


const LapTime = ({}: P) => {
  const [running, setRunning] = useState<boolean>(false);
  const [time, setTime] = useState<number>(0);
  const [laps, setLaps] = useState<Record[]>([]);
  const intervalRef = useRef<NodeJS.Timeout | undefined>(undefined);

  const startStopwatch = () => {
    if (!running) {
      intervalRef.current = setInterval(() => {
        setTime((prevTime) => prevTime + 10);
      }, 10);
      setRunning(true);
    } else {
      clearInterval(intervalRef.current);
      setRunning(false);
    }
  };

  const resetStopwatch = () => {
    clearInterval(intervalRef.current);
    setTime(0);
    setLaps([]);
    setRunning(false);
  };

  const recordLap = () => {
    const newLap = {
      id: laps.length === 0 ? 1 : laps[laps.length - 1].id + 1,
      lap: time,
    };
    postRecord(newLap);
    setLaps((prevLaps) => [...prevLaps, newLap]);
  };

  return (
    <Container gap={12}>
      <Timer lapTime={formatTime(time)} />
      <ButtonWrapper>
        <StartButton onClick={startStopwatch}>
          {running ? 'Stop' : 'Start'}
        </StartButton>
        <ResetButton onClick={resetStopwatch}>Reset</ResetButton>
      </ButtonWrapper>
      <Records laps={laps} setLaps={setLaps} />
      {running && <LapButton onClick={recordLap}>Lap</LapButton>}
    </Container>
  );
};

export default LapTime;

timer에는 format 된 time을 넘겨주고,
startStopButton은 동작하지 않을땐 start, 동작중일땐 stop을 보여준다.
records에는 laps를 넘겨주어 map하여 기록들을 보여주면 될 것이다!
lap 버튼은 동작하고 있지 않을때에는 시간이 기록될 필요가 없으므로, running 중일때만 보여주면 된다.

profile
안녕하세요 지원입니다.

1개의 댓글

comment-user-thumbnail
2023년 8월 12일

정보에 감사드립니다.

답글 달기