뽀모도로 구현(1)

차차·2023년 1월 28일
0
post-thumbnail

이번 스터디 프로젝트에서 뽀모도로라는 타이머 기능을 추가하자는 아이디어가 나와 구현을 맡게 되었다.

❓ 뽀모도로 기법?

뽀모도로 기법(Pomodoro Technique)은 시간 관리 방법론으로 1980년대 후반 ‘프란체스코 시릴로’(Francesco Cirillo)가 제안했다. 타이머를 이용해서 25분간 집중해서 일한 다음 5분간 휴식하는 방식이다. ‘뽀모도로’는 이탈리아어로 토마토를 뜻한다. 프란체스코 시릴로가 대학생 시절 토마토 모양으로 생긴 요리용 타이머를 이용해 25분간 집중 후 휴식하는 일 처리 방법을 제안한 데서 그 이름이 유래했다. (출처 : 위키백과)


왜 이렇게 어려워…

처음에는 단순 타이머인줄 알고 빨리 끝나겠거니 생각하였는데 막상 구현하고자 하니 예외사항이 여럿있었다.

  1. 집중 시간과 쉬는 시간을 입력 받는다.
  2. 집중 시간이 끝난 뒤에 쉬는 시간이 나오는데 쉬는 시간이 끝나면 집중 시간이 다시 나오는 총 4 싸이클을 돌린다.
  3. 마지막 싸이클에서는 쉬는 시간을 포함시키지 않는다.
  4. 집중 시간 / 쉬는 시간은 각 상태가 있어 변경될 때마다 화면에 표시해주어야한다.

useInterval 생성

첫 번째로 타이머 기능을 만들기 위해 1초마다 callback을 실행시키는 useInterval hook을 만들었다.

// useInterval.ts

import { useEffect, useRef } from "react";

// isRun은 interval을 실행시킬지 아닐지를 결정하는 상태값이다.
// callback은 interval 시킬 함수이다.
const useInterval = (isRun: boolean, callback: () => void) => {
  const intervalRef = useRef<NodeJS.Timer | null>(null);

  useEffect(() => {
    if (isRun) {
      if (!intervalRef.current) {
        intervalRef.current = setInterval(() => {
          callback();
        }, 1000);
      }
    } else {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
    }
		// 재사용했을 때 중복을 막기 위해 클린업 함수를 실행시킨다.
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
    };
  }, [isRun, callback]);
};

export default useInterval;

진짜 intervalRef 타입 지정 때문에 시간을 엄청 소비했다… 결국 저 부분만 가져가 chatGpt로 intervalRef 타입 지정해달라고 하니 아주 친절하게 지정해주었다…


본격 기능 구현

import useInterval from "@hooks/useInterval";
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

type StudyState = "study" | "rest" | "end";

const Timer = () => {
  const [isRun, setIsRun] = useState(false);
  const [defaultTime, setDefaultTiem] = useState({ studyTime: 0, restTime: 0 });
  const [time, setTime] = useState(0);
  const [totalTime, setTotalTime] = useState(0);
  const [studyState, setStudyState] = useState<StudyState>("study");
  const studyRef = useRef<HTMLInputElement>(null);
  const restRef = useRef<HTMLInputElement>(null);
  const cycleRef = useRef<number>(0);

  // 1초마다 실행시킬 함수
  const interValCallback = useCallback(() => {
    setTotalTime((prev) => prev - 1);
  }, []);

  // isRun이 true일 때 interval 실행
  useInterval(isRun, interValCallback);

  // isRun을 true false로 토글하는 기능. 32번째 줄은 필요없어 보인다.
  const onClickToggle = useCallback(() => {
    if (cycleRef.current === 0) cycleRef.current = 3;
    setIsRun((prev) => !prev);
  }, []);

  // reset 버튼을 클릭했을 때 실행시킬 함수
  const onClickReset = useCallback(() => {
    if (!studyRef.current || !restRef.current) return;
    const studyTime = +studyRef.current?.value;
    const restTime = +restRef.current?.value;
    setIsRun(false);
    setTotalTime(studyTime * 4 + restTime * 3);
  }, []);

  // 시간을 문자열로 변환하여 00:00처럼 보이게 하는 기능
  const viewTime = useMemo(() => {
    const minutes = (time / 60) | 0;
    const seconds = time % 60;
    return `${minutes < 10 ? "0" + minutes : minutes}:${
      seconds < 10 ? "0" + seconds : seconds
    }`;
  }, [time]);

  // 집중시간 / 쉬는시간을 입력하고 세팅하는 함수
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!studyRef.current || !restRef.current) return;
    // const studyTime = +studyRef.current?.value * 60;
    // const restTime = +restRef.current?.value * 60;
    const studyTime = +studyRef.current?.value;
    const restTime = +restRef.current?.value;
    setTotalTime(studyTime * 4 + restTime * 3);
    setDefaultTiem({ studyTime, restTime });
  };

  // 총시간과 디폴트 시간이 변경되었을 때 실행  
  useEffect(() => {
    const { studyTime, restTime } = defaultTime;
    if (!studyTime || !restTime) return;
    const sumTime = studyTime + restTime;
    const cycle = ((totalTime + restTime) / sumTime) | 0;
    const remainTime = (totalTime + restTime) % sumTime || sumTime;
    cycleRef.current = cycle;
    if (totalTime === 0) {
      setStudyState("study");
      setTotalTime(studyTime * 4 + restTime * 3);
      setTime(studyTime);
      setIsRun(false);
      return;
    }
    if (cycle) {
      setTime(
        remainTime <= restTime
          ? remainTime % studyTime || restTime
          : (remainTime - restTime) % studyTime || studyTime
      );
      setStudyState(remainTime <= restTime ? "rest" : "study");
    } else {
      setTime((remainTime - restTime) % studyTime || studyTime);
      setStudyState("study");
    }
  }, [totalTime, defaultTime]);

  return (
    <div className="flex-center h-screen">
      <div className="flex w-[30rem] flex-col items-center justify-center space-y-10 bg-red-200 py-[5rem]">
        <span>{studyState}</span>
        <span className="text-[2rem]">{viewTime}</span>
        <div className="space-x-3">
          <button
            className="border border-primary-600 p-2 text-[1.6rem]"
            onClick={onClickToggle}
          >
            {isRun ? "stop" : "start"}
          </button>
          <button
            className="border border-primary-600 p-2 text-[1.6rem]"
            onClick={onClickReset}
          >
            reset
          </button>
        </div>
        <form className="flex flex-col space-y-3" onSubmit={onSubmit}>
          <input type="number" placeholder="공부시간" ref={studyRef} />
          <input type="number" placeholder="짧은 휴식" ref={restRef} />
          <button className="w-full border border-primary-700 bg-gray-200 py-3">
            세팅
          </button>
        </form>
      </div>
    </div>
  );
};

export default Timer;

뽀모도로 리액트로 구현을 구글링해도 정말 하나도 안나와서 머리 깨지는 줄 알았다…

코드가 많이 지저분하다… 정말 useEffect 부분에서 3시간 이상은 쏟은거 같다… 그럼에도 아직까지 남은 문제점이 있는데 지금은 너무 피곤하기에 자고 일어나서 작성해보아야겠다.

문제 간단 요약

  1. 새로고침 / 페이지 이동시에 타이머 날아가는 문제.
  2. 원형 애니메이션을 통해 진행률을 표현해야하는데 이것 또한 1초를 기준으로 잡아야하는지 ??
profile
나는야 프린이

0개의 댓글