๐ ๊ทธ๋์ ๊ณต๋ถํ๋ recoil, framer-motion ํ์ฉํด์ ํ์ด๋จธ๋ฅผ ๋ง๋ค์ด๋ดค์!
๊ตฌํํ๊ณ ์ ํ๋ ํ์ด๋จธ ๋ ํผ๋ฐ์ค ๐ pomofocus
๊ธฐ๋ฅ ๋ฃ๊ธฐ ์ ์ ๊ธฐ๋ณธ์ ์ธ ๋์์ธ ๋ชฉ์ ๋ถํฐ ๋ง๋ค๊ณ ์์!
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,
});
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]);
<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>
const handleTimerEnd = () => {
setTimerActive(false); // ๋ฒํผ pause๋ก ๋ณ๊ฒฝ
setTimer(1500); // ํ์ด๋จธ ์ซ์ reset
setRoundCount((prevCount) => {
const newCount = prevCount + 1;
if (newCount >= 4) {
// roundCount๊ฐ 4์ ๋๋ฌํ๋ฉด 0์ผ๋ก ๋ฆฌ์
return 0;
}
return newCount;
});
};
useEffect(() => {
if (initialRender) {
// ์ฒ์ ๋ ๋๋ง๋ ๋๋ setInitialRender๋ฅผ false๋ก ์ค์ ํ์ฌ ๋ค์๋ถํฐ๋ ์ด ์กฐ๊ฑด์ ๊ฑด๋๋ฐ๊ฒ ํจ
setInitialRender(false);
} else if (roundCount === 0) {
// roundCount๊ฐ 0์ผ๋ก ๋ฆฌ์
๋์๋ค๋ฉด goal ์
๋ฐ์ดํธ ํ์
setGoal((prevGoal) => {
return prevGoal < 12 ? prevGoal + 1 : prevGoal;
});
}
}, [roundCount]);
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>
๋ฌธ์ ๋ ํ๋์ 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 ๊ฐ์ ์ค์ค์์๋๋ฐ,
๋ง์ฝ 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]);
๋์ ๋ณด์ด๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋๋ ๋๋ฒ์งธ ์๋ฌ๊ฐ ํด๊ฒฐ๋ฌ์...ใ
ํด๊ฒฐ๋์ ์ข์ง๋ง ์ด ์ฐ์ฐํ ๊ธฐ๋ถ์ด ๋๋ฌด ๋ณ๋ก๋ค...
๋ญ๊ฐ ๋์ถฉ์ ์๊ฒ ๊ฐ์ง๋ง ์ ํํ ๋ช
๋ฃํ๊ฒ ์ดํด๊ฐ ์๋๋ ๋ต๋ตํ๋ค ๐ญ๐ญ๐ญ๐ญ๐ญ
๊ฒฐ๋ก : ๊ณต๋ถ๋ฅผ ๋ ์ด์ฌํ ํ์! ์ด๋ก ๋ ์ข์ง๋ง ์ญ์ ์ค์ ๋ก ๋ญ๊ฐ ๋ง๋ค์ด๋ด์ผ ์ค๋ ฅ์ด ๋ ๋๋๊ฒ ๊ฐ๋ค!! ์ด๊ฒ ์ ๊ฒ ๋ ๋ง๋ค์ด๋ด ์๋ค์์
์ค๊ฐ์ ๋ค์ ์ฒ์๋ถํฐ ํ๊ณ ์ถ์ ์๋ ์์ผ๋ ๋ฆฌ์ ๋ฒํผ ๋ง๋ค๊ธฐ
ํ์ด๋จธ ๋๋๋ฉด ์๋ฆผ ์๋ฆฌ๋๊ฒ ๋ง๋ค๊ธฐ