Timer ๊ตฌํ˜„ react + typescript

๊น€์€์„œยท2024๋…„ 6์›” 28์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
50/52
post-thumbnail

๐Ÿ“Œ ๊ทธ๋™์•ˆ ๊ณต๋ถ€ํ–ˆ๋˜ recoil, framer-motion ํ™œ์šฉํ•ด์„œ ํƒ€์ด๋จธ๋ฅผ ๋งŒ๋“ค์–ด๋ดค์Œ!

๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•˜๋Š” ํƒ€์ด๋จธ ๋ ˆํผ๋Ÿฐ์Šค ๐Ÿ‘‰ pomofocus

๐Ÿ“Œ ๋””์ž์ธ ๋ชฉ์—…

๊ธฐ๋Šฅ ๋„ฃ๊ธฐ ์ „์— ๊ธฐ๋ณธ์ ์ธ ๋””์ž์ธ ๋ชฉ์—…๋ถ€ํ„ฐ ๋งŒ๋“ค๊ณ  ์‹œ์ž‘!

  • styled-components๋ฅผ ์‚ฌ์šฉํ•ด ์คฌ์Œ!
  • ์•„์ด์ฝ˜ ๊ฐ€์ ธ์˜จ ๊ณณ ๐Ÿ‘‰ ์•„์ด์ฝ˜

๋””์ž์ธ ๋ชฉ์—…

๐Ÿ“Œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

  1. recoil ์‚ฌ์šฉํ•ด์„œ state ๊ด€๋ฆฌ
import { atom } from "recoil";

export const timerState = atom({
  key: "timerState",
  default: 1500, // ํƒ€์ด๋จธ ๊ธฐ๋ณธ ์…‹ํŒ… ์‹œ๊ฐ„ 25๋ถ„ 
});

// play, pause ๋ฒ„ํŠผ state
export const isTimerActiveState = atom<boolean>({
  key: "isTimerActiveState",
  default: false,
});

// round state
export const roundCountState = atom({
  key: "roundCountState",
  default: 0,
});

// goal state
export const goalState = atom({
  key: "goalState",
  default: 0,
});
  1. 25:00๋ถ€ํ„ฐ 00:00๊นŒ์ง€ ๊ฑฐ๊พธ๋กœ ํƒ€์ด๋จธ ๊ตฌํ˜„ํ•˜๊ธฐ (setInterval ํ•จ์ˆ˜ ์‚ฌ์šฉํ•ด์„œ ๊ตฌํ˜„)
  useEffect(() => {
    let intervalId: NodeJS.Timeout | undefined = undefined;

    if (timerActive) {
      intervalId = setInterval(() => {
        setTimer((prevTimer) => {
          if (prevTimer <= 0) {
            return 1500;
          }
          return prevTimer - 1;
        });
      }, 1000);
    }

    return () => {
      if (intervalId) clearInterval(intervalId);
    };
  }, [timerActive, setTimer]);
  1. ํƒ€์ด๋จธ play, pause ๊ตฌํ˜„
      <SvgContainer
        onClick={() => setTimerActive((prev) => !prev)} // state ๋ฐ”๊ฟ”์ฃผ๊ธฐ
      >
        {timerActive ? (
          <Svg
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true"
          >
            <path d="M5.75 3a.75.75 0 0 0-.75.75v12.5c0 .414.336.75.75.75h1.5a.75.75 0 0 0 .75-.75V3.75A.75.75 0 0 0 7.25 3h-1.5ZM12.75 3a.75.75 0 0 0-.75.75v12.5c0 .414.336.75.75.75h1.5a.75.75 0 0 0 .75-.75V3.75a.75.75 0 0 0-.75-.75h-1.5Z" />
          </Svg>
        ) : (
          <Svg
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true"
          >
            <path d="M6.3 2.84A1.5 1.5 0 0 0 4 4.11v11.78a1.5 1.5 0 0 0 2.3 1.27l9.344-5.891a1.5 1.5 0 0 0 0-2.538L6.3 2.841Z" />
          </Svg>
        )}
      </SvgContainer>
  1. 25๋ถ„์ด ๋๋‚ ๋•Œ๋งˆ๋‹ค ROUND ์นด์šดํŠธ ์ฆ๊ฐ€
  const handleTimerEnd = () => {
    setTimerActive(false); // ๋ฒ„ํŠผ pause๋กœ ๋ณ€๊ฒฝ
    setTimer(1500); // ํƒ€์ด๋จธ ์ˆซ์ž reset
    setRoundCount((prevCount) => {
      const newCount = prevCount + 1;
      if (newCount >= 4) {
        // roundCount๊ฐ€ 4์— ๋„๋‹ฌํ•˜๋ฉด 0์œผ๋กœ ๋ฆฌ์…‹
        return 0;
      }
      return newCount;
    });
  };
  1. ROUND 4๋ฒˆ์ด๋ฉด GOAL 1๋ฒˆ ์นด์šดํŠธ ์ฆ๊ฐ€
  useEffect(() => {
    if (initialRender) {
      // ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ๋Š” setInitialRender๋ฅผ false๋กœ ์„ค์ •ํ•˜์—ฌ ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์ด ์กฐ๊ฑด์„ ๊ฑด๋„ˆ๋›ฐ๊ฒŒ ํ•จ
      setInitialRender(false);
    } else if (roundCount === 0) {
      // roundCount๊ฐ€ 0์œผ๋กœ ๋ฆฌ์…‹๋˜์—ˆ๋‹ค๋ฉด goal ์—…๋ฐ์ดํŠธ ํ•„์š”
      setGoal((prevGoal) => {
        return prevGoal < 12 ? prevGoal + 1 : prevGoal;
      });
    }
  }, [roundCount]);

๐Ÿ“Œ framer-motion์œผ๋กœ animation ์ถ”๊ฐ€

  • motion์€ ์‚ฌ์‹ค์ƒ ๋งŽ์ด ์‚ฌ์šฉ์„ ์•ˆํ–ˆ์Œ...๐Ÿ˜…
    ๊ฐ„๋‹จํ•œ ์—๋‹ˆ๋ฉ”์ด์…˜๋งŒ ๋„ฃ์–ด์คฌ์Œ.
const boxVariants = {
  hidden: { scale: 0.8, opacity: 0 },
  visible: {
    scale: 1.2,
    opacity: 1,
    transition: { duration: 0.5, bounce: 0.25 },
  },
  exit: { scale: 1, opacity: 1 },
};

const svgVariants = {
  hover: { scale: 1.2 },
};
      <BoxArea
        variants={boxVariants}
        initial="hidden"
        animate="visible"
        exit="exit"
      >
        <Box
          variants={boxVariants}
          initial="hidden"
          animate="visible"
          exit="exit"
          key={`minutes-${Math.floor(timer / 60)}`}
        >
          {Math.floor(timer / 60)
            .toString()
            .padStart(2, "0")}
        </Box>
        <ColonArea>
          <Colon />
          <Colon />
        </ColonArea>
        <Box
          variants={boxVariants}
          initial="hidden"
          animate="visible"
          exit="exit"
          key={`seconds-${timer}`}
        >
          {(timer % 60).toString().padStart(2, "0")}
        </Box>
      </BoxArea>

๐Ÿ“Œ errors

  1. ๋ถ„, ์ดˆ ๊ฐ™์€ ์—๋‹ˆ๋ฉ”์ด์…˜์ด์ง€๋งŒ ๋ถ„, ์ดˆ ๊ฐ๊ฐ ์‹œ๊ฐ„์ด ๋ณ€ํ• ๋•Œ๋งŒ ์—๋‹ˆ๋ฉ”์ด์…˜๋˜๊ฒŒ ๋งŒ๋“ค๊ธฐ!

๋ฌธ์ œ๋Š” ํ•˜๋‚˜์˜ boxVariants์œผ๋กœ ๋ถ„, ์ดˆ๋ฅผ ๊ฐ๊ฐ ์—๋‹ˆ๋ฉ”์ด์…˜ํ•ด์•ผํ•˜๋Š” ๊ฑฐ์˜€์Œ..
์ฒ˜์Œ์—” ์ผ๋‹จ ์ด๋ ‡๊ฒŒ(๐Ÿ‘‡) ์ฝ”๋”ฉํ•ด๋ดค๋Š”๋ฐ ๋ถ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ์นด๋“œ๊ฐ€ ์ดˆ๊ฐ€ ๋ฐ”๋€”๋•Œ๋งˆ๋‹ค ๊ฐ™์ด ๋ฐ”๋€Œ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•จ..

      <BoxArea>
        <Box variants={boxVariants} initial="hidden" animate="visible">
          {Math.floor(timer / 60)
            .toString()
            .padStart(2, "0")}
        </Box>
        <ColonArea>
          <Colon />
          <Colon />
        </ColonArea>
        <Box variants={boxVariants} initial="hidden" animate="visible">
          {(timer % 60).toString().padStart(2, "0")}
        </Box>
      </BoxArea>

์—…๋กœ๋“œ์ค‘..

์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋‚˜ ์ฐพ์•„๋ณด๋‹ค๊ฐ€ key ์†์„ฑ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑธ ์•Œ๊ฒŒ๋จ

<BoxArea>
  <Box
    key={Math.floor(timer / 60)} // ํƒ€์ด๋จธ์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด key
    variants={boxVariants}
    initial="hidden"
    animate="visible"
  >
    {Math.floor(timer / 60)
      .toString()
      .padStart(2, "0")}
  </Box>
  <ColonArea>
    <Colon />
    <Colon />
  </ColonArea>
  <Box
    key={-timer} // Box ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‘ ๊ฐœ์ด๋ฏ€๋กœ, ๋‘ ๋ฒˆ์งธ Box์—๋Š” ํƒ€์ด๋จธ ๊ฐ’์— ์Œ์ˆ˜๋ฅผ ์ทจํ•ด ์œ ๋‹ˆํฌํ•œ key
    variants={boxVariants}
    initial="hidden"
    animate="visible"
  >
    {(timer % 60).toString().padStart(2, "0")}
  </Box>
</BoxArea>

๐Ÿ˜ตโ€๐Ÿ’ซ์ด๋ ‡๊ฒŒ ๊ณ ์ณค๋”๋‹ˆ ์˜คํžˆ๋ ค 2๊ฐœ์˜ error๋ฅผ ๋” ์–ป์Œ ๐Ÿ˜ต
1. Timer.tsx:118 Warning: Encountered two children with the same key, 0. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted โ€” the behavior is unsupported and could change in a future version.
2. Cannot destructure property 'animation' of 'this.resolved' as it is undefined.

ํ•˜๋‚˜์”ฉ ํ•ด๊ฒฐํ•ด๋ณด์ž๋ฉด

1๋ฒˆ ์—๋Ÿฌ
๋ฆฌ์•กํŠธ์—์„œ ๋ Œ๋”๋ง๋˜๋Š” ๋ฆฌ์ŠคํŠธ์˜ ๊ฐ ํ•ญ๋ชฉ์ด ๊ณ ์œ ์˜ key ๊ฐ’์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค๋Š” ์›์น™์„ ์œ„๋ฐ˜ํ–ˆ์„ ๋•Œ ๋‚˜ํƒ€๋‚œ๋‹ค๊ณ  ํ•จ.
key={Math.floor(timer / 60)} , key={-timer} ๋กœ ๊ฐ๊ฐ ๊ณ ์œ ํ•œ key ๊ฐ’์„ ์ค€์ค„์•Œ์•˜๋Š”๋ฐ,

  • ์ฒซ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ์˜ key๋Š” Math.floor(timer / 60). ํƒ€์ด๋จธ์˜ ๋ถ„์„ ๋‚˜ํƒ€๋ƒ„.
  • ๋‘ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ์˜ key๋Š” timer. ํƒ€์ด๋จธ์˜ ์ด ์ดˆ๋ฅผ ๋‚˜ํƒ€๋ƒ„.

๋งŒ์•ฝ timer ์‹œ๊ฐ„์ด ๋๋‚˜์„œ 0์ด๋˜๋ฉด,
์ฒซ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ์˜ key๋„ 0์ด ๋˜๊ณ ,
๋‘ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ์˜ key๋„ 0์ด ๋˜์–ด ๋‘ ์ปดํฌ๋„ŒํŠธ์˜ key๊ฐ€ ๋™์ผํ•ด์ง€๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•จ.

ํ•ด๊ฒฐ
๊ฐ ์ปดํฌ๋„ŒํŠธ์— ๊ณ ์œ ํ•œ key ๊ฐ’์„ ํ• ๋‹นํ•˜๊ธฐ. ๐Ÿ‘‡

<Box
  key={`minutes-${Math.floor(timer / 60)}`} // ๋ถ„์„ ๋‚˜ํƒ€๋‚ด๋Š” Box์˜ key ์ˆ˜์ •
  variants={boxVariants}
  initial="hidden"
  animate="visible"
>
  {Math.floor(timer / 60)
    .toString()
    .padStart(2, "0")}
</Box>
...
<Box
  key={`seconds-${timer}`} // ์ดˆ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Box์˜ key ์ˆ˜์ •
  variants={boxVariants}
  initial="hidden"
  animate="visible"
>
  {(timer % 60).toString().padStart(2, "0")}
</Box>

2๋ฒˆ ์—๋Ÿฌ
์‚ฌ์‹ค ์ฒซ ๋ฒˆ์งธ ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ์—๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ์‚ฌ๋ผ์งˆ ์ค„ ์•Œ์•˜์œผ๋‚˜,, ๊ทธ๋Ÿฐ์ผ์€ ์ผ์–ด๋‚˜์ง€ ์•Š์•˜์Œ,,,

๋‘ ๋ฒˆ์งธ ์—๋Ÿฌ๋Š” ์•„๋ฌด๋ฆฌ ์ฐพ์•„๋ด๋„ ๋ชจ๋ฅด๊ฒ ์–ด์„œ ๊ฒฐ๊ตญ chat gpt์—๊ฒŒ ๋ฌผ์–ด๋ดค์Œ..

๋ฐ›์€ ๋‹ต๋ณ€
์ฃผ์–ด์ง„ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋Š” "Cannot destructure property 'animation' of 'this.resolved' as it is undefined."๋กœ,
ํŠน์ • ๊ฐ์ฒด์—์„œ destructuring์„ ์‹œ๋„ํ•  ๋•Œ ํ•ด๋‹น ๊ฐ์ฒด ๋˜๋Š” ํ”„๋กœํผํ‹ฐ๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•˜์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ด ์ฝ”๋“œ์—์„œ๋Š” ์ง์ ‘์ ์œผ๋กœ animation์ด๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹นํ•˜๋Š” ๋ถ€๋ถ„์ด ๋ณด์ด์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์˜ค๋ฅ˜์˜ ์›์ธ์€ framer-motion ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. framer-motion์€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋ฉฐ, ์—ฌ๊ธฐ์„œ ์‚ฌ์šฉ๋œ ์ปดํฌ๋„ŒํŠธ(motion.div, motion.svg)์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ ๊ณผ์ •์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์œผ๋กœ ์ถ”์ •๋ฉ๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์„ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ํ™•์ธ: framer-motion ๋˜๋Š” ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋ฒ„์ „์ด ์ตœ์‹ ์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”. ๋•Œ๋•Œ๋กœ ์˜ค๋ž˜๋œ ๋ฒ„์ „์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์–ด ์ดํ›„ ๋ฒ„์ „์—์„œ ์ˆ˜์ •๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์˜์กด์„ฑ ํ™•์ธ: ํ”„๋กœ์ ํŠธ์˜ package.json์„ ํ™•์ธํ•˜์—ฌ framer-motion๊ณผ ๋‹ค๋ฅธ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์น˜๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ํ•„์š”ํ•˜๋‹ค๋ฉด npm install ๋˜๋Š” yarn์„ ์‹คํ–‰ํ•˜์—ฌ ์˜์กด์„ฑ์„ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜์„ธ์š”.
์ฝ”๋“œ ๊ฒ€ํ† : motion.div์™€ motion.svg ์ปดํฌ๋„ŒํŠธ์˜ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์„ ๋‹ค์‹œ ํ•œ ๋ฒˆ ๊ฒ€ํ† ํ•˜์„ธ์š”. variants, initial, animate ํ”„๋กœํผํ‹ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , framer-motion์˜ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
์—๋Ÿฌ ๋กœ๊ทธ ๋ถ„์„: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ •ํ™•ํ•œ ์œ„์น˜๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ(Console, Sources ํƒญ)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ์˜ค๋ฅ˜ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ํ†ตํ•ด ๋ฌธ์ œ์˜ ๊ทผ์›์„ ์ข€ ๋” ๋ช…ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ปค๋ฎค๋‹ˆํ‹ฐ ๋„์›€ ์š”์ฒญ: framer-motion์˜ GitHub ์ด์Šˆ ํŠธ๋ž˜์ปค๋‚˜ Stack Overflow ๋“ฑ์˜ ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ์— ํ•ด๋‹น ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ๋„์›€์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น„์Šทํ•œ ๋ฌธ์ œ๋ฅผ ๊ฒช์—ˆ๋˜ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž์˜ ๊ฒฝํ—˜์ด ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค ํ•ด๋ด๋„ ์•ˆ๋จ...

๊ทธ๋ž˜์„œ ํ™”๋ฉด์„ ํ•œ์ฐธ ์ณ๋‹ค๋ณด๋‹ค๊ฐ€ ๋ฐœ๊ฒฌํ–ˆ๋Š”๋ฐ
ํƒ€์ด๋จธ๊ฐ€ ๋๋‚˜๋ฉด 00 : 00 ์ดˆ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  25:00์œผ๋กœ ๋ฆฌ์…‹์ด ๋˜์•ผํ•˜๋Š”๋ฐ 00:01์ดˆ์—์„œ ๋ฐ”๋กœ 25:00์œผ๋กœ ๋ฐ”๋€Œ๋Š” ๊ฑธ ๋ด๋ฒ„๋ฆผ!๐Ÿ˜ฑ

๊ธฐ์กด ์ฝ”๋“œ ๐Ÿ‘‡

  useEffect(() => {
    if (timer === 0) {
      handleTimerEnd();
    }
  }, [timer]);

์ˆ˜์ • ์ฝ”๋“œ ๐Ÿ‘‡

useEffect(() => {
  if (timer === 0) {
    // ํƒ€์ด๋จธ๊ฐ€ 0์— ๋„๋‹ฌํ•˜๋ฉด ์ผ์‹œ์ ์œผ๋กœ ํƒ€์ด๋จธ๋ฅผ ๋ฉˆ์ถ”๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ 00:00์„ ๋ณด์—ฌ์คŒ
    setTimeout(() => {
      handleTimerEnd();
    }, 1000); // 1์ดˆ(1000๋ฐ€๋ฆฌ์ดˆ) ๋™์•ˆ 00:00์„ ๋ณด์—ฌ์ค€ ํ›„, handleTimerEnd ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•จ
  }
}, [timer]);

๋ˆˆ์— ๋ณด์ด๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋”๋‹ˆ ๋‘๋ฒˆ์งธ ์—๋Ÿฌ๊ฐ€ ํ•ด๊ฒฐ๋ฌ์Œ...ใ…Ž
ํ•ด๊ฒฐ๋˜์„œ ์ข‹์ง€๋งŒ ์ด ์ฐ์ฐํ•œ ๊ธฐ๋ถ„์ด ๋„ˆ๋ฌด ๋ณ„๋กœ๋‹ค...
๋ญ”๊ฐ€ ๋Œ€์ถฉ์€ ์•Œ๊ฒƒ ๊ฐ™์ง€๋งŒ ์ •ํ™•ํžˆ ๋ช…๋ฃŒํ•˜๊ฒŒ ์ดํ•ด๊ฐ€ ์•ˆ๋˜๋‹ˆ ๋‹ต๋‹ตํ•˜๋‹ค ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ

๊ฒฐ๋ก : ๊ณต๋ถ€๋ฅผ ๋” ์—ด์‹ฌํžˆ ํ•˜์ž! ์ด๋ก ๋„ ์ข‹์ง€๋งŒ ์—ญ์‹œ ์‹ค์ œ๋กœ ๋ญ”๊ฐˆ ๋งŒ๋“ค์–ด๋ด์•ผ ์‹ค๋ ฅ์ด ๋” ๋Š๋Š”๊ฒƒ ๊ฐ™๋‹ค!! ์ด๊ฒƒ ์ €๊ฒƒ ๋” ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค์•„์•„

๐Ÿ“Œ ์ถ”๊ฐ€ ๊ตฌํ˜„ํ•˜๊ณ ์‹ถ์€๊ฒƒ.

  • ์ค‘๊ฐ„์— ๋‹ค์‹œ ์ฒ˜์Œ๋ถ€ํ„ฐ ํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ ๋ฆฌ์…‹ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ธฐ

  • ํƒ€์ด๋จธ ๋๋‚˜๋ฉด ์•Œ๋ฆผ ์†Œ๋ฆฌ๋‚˜๊ฒŒ ๋งŒ๋“ค๊ธฐ

์ „์ฒด ์ฝ”๋“œ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ ๐Ÿ‘‰ github

0๊ฐœ์˜ ๋Œ“๊ธ€