Motion: JavaScript & React animation library
Motion (prev Framer Motion) is a fast, production-grade animation library for React, JavaScript and Vue. Build smooth UI animations at a tiny footprint.
motion.dev
✔️ Framer Motion의 핵심 속성
- initial
- 시작 상태
- 애니메이션이 처음 등장할 때의 초기 스타일
- x축 위치, 투명도 등
- animate
- 목표 상태
- 컴포넌트가 최종적으로 도달해야 하는 스타일
- transition
- 움직임의 방식
- 애니메이션의 지속 시간(duration), 효과(spring, ease), 지연 시간(delay) 등
Get started
✔️ Install
npm install motion 명령어를 사용하여 다운로드
✔️ Create your first animation


// src/App.tsx
import Rotate from "./components/Rotate";
export default function App() {
return (
<>
<Rotate />
</>
);
}
// src/components/Rotate.tsx
import * as motion from "motion/react-client";
export default function Rotate() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div
className="w-24 h-24 bg-[#ff0055] rounded-lg"
animate={{ rotate: 360 }}
transition={{ duration: 2 }}
/>
</div>
);
}
- motion.div
- 일반 HTML div 태그에 애니메이션 능력을 부여한 Framer Motion 전용 컴포넌트
- animate={{ rotate: 360 }}
- 박스가 최종적으로 도달해야 하는 목표 상태
- 360도 회전
- transition={{ duration: 1 }}
- 목표 상태까지 도달하는 데 1초 동안 움직이도록 속도 조절
* 해당 코드에 initial 속성 X
--> 컴포넌트가 Mount 되는 순간(새로고침)에 자동 실행


// src/components/Rotate.tsx
import * as motion from "motion/react-client";
export default function Rotate() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div
className="w-24 h-24 bg-[#ff0055] rounded-lg"
animate={{ scale: 2 }}
transition={{ duration: 2 }}
/>
</div>
);
}
- animate={{ scale: 2 }}
- 크기 2배 증가
✔️ Enter animation


// src/components/EnterAnimation.tsx
import * as motion from "motion/react-client";
export default function EnterAnimation() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div
className="w-24 h-24 bg-[#ff0055] rounded-full"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.4,
scale: { type: "spring", visualDuration: 0.4, bounce: 0.5 },
}}
/>
</div>
);
}
- initial={{ opacity: 0, scale: 0 }}
- 투명도 0, 크기 0
- animate={{ opacity: 1, scale: 1}}
- 투명도 1, 크기 1
- transition={{ duration: 0.4, scale: { type: "spring", visualDuration: 0.4, bounce: 0.5}, }}
- type: "spring" : 고무공처럼 탄성을 가지고 움직이도록 설정
✔️ Hover & tap animation


// src/components/Gestures.tsx
import * as motion from "motion/react-client";
export default function Gestures() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div
className="w-24 h-24 bg-[#ff0055] rounded-lg"
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.8 }}
/>
</div>
);
}
- whileHover={{ scale: 1.2 }}
- 박스 위에 마우스 커서를 올렸을 때 실행
- 기존 크기보다 1.2배 증가
- whileTap={{ scale: 0.8 }}
- 박스를 클릭하고 있는 순간 실행
- 기존 크기보다 0.8배로 감소
✔️ Layout animation


// src/components/LayoutAnimation.tsx
import * as motion from "motion/react-client";
import { useState } from "react";
export default function LayoutAnimation() {
const [isOn, setIsOn] = useState(false);
const toggleSwitch = () => {
setIsOn(!isOn);
};
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<button
className={`w-24 h-12 bg-[#ff0055]/30 rounded-full cursor-pointer flex p-1 items-center ${isOn ? "justify-start" : "justify-end"}`}
onClick={toggleSwitch}
>
<motion.div
className="w-8 h-8 bg-[#ff0055] rounded-full"
layout
transition={{ type: "spring", visualDuration: 0.2, bounce: 0.2 }}
/>
</button>
</div>
);
}
- bg-[#ff0055]/30
- 약 30% 투명도
- layout
- justify-start, justify-end 등 바뀐 정렬 방식을 알아채고 자동으로 이동


// src/components/SharedLayoutAnimation.tsx
import { AnimatePresence } from "motion/react";
import * as motion from "motion/react-client";
import { useState } from "react";
export default function SharedLayoutAnimation() {
const [selectedTab, setSelectedTab] = useState(tabs[0]);
return (
<>
<div className="flex items-center justify-center min-h-screen">
<div className="flex flex-col w-120 h-90 max-w-[calc(100% - 40px)] max-h-[calc(100% - 40px)] rounded-lg bg-white overflow-hidden shadow-2xl">
<nav className="bg-[#fdfdfd] pt-1 px-1 border-b border-[#eeeeee]">
<ul className="flex w-full p-0 m-0 list-none font-medium text-sm">
{tabs.map((item) => (
<motion.li
className="rounded-md w-full px-3.5 py-2.5 relative bg-white cursor-pointer h-6 flex justify-between items-center flex-1 min-w-0 select-none"
key={item.label}
initial={false}
animate={{
backgroundColor: item === selectedTab ? "#eee" : "#eee0",
}}
onClick={() => setSelectedTab(item)}
>
{`${item.icon} ${item.label}`}
{item === selectedTab ? (
<motion.div
className="absolute -bottom-0.5 left-0 right-0 h-0.5 bg-[#ff0055]"
layoutId="underline"
id="underline"
/>
) : null}
</motion.li>
))}
</ul>
</nav>
<main className="flex justify-center items-center flex-1">
<AnimatePresence mode="wait">
<motion.div
className="flex items-center justify-center text-[128px] leading-none select-none"
key={selectedTab ? selectedTab.label : "empty"}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{ duration: 0.2 }}
>
{selectedTab ? selectedTab.icon : "😋"}
</motion.div>
</AnimatePresence>
</main>
</div>
</div>
</>
);
}
/**
* ============== Data ================
*/
const allIngredients = [
{ icon: "🍅", label: "Tomato" },
{ icon: "🥬", label: "Lettuce" },
{ icon: "🧀", label: "Cheese" },
{ icon: "🥕", label: "Carrot" },
{ icon: "🍌", label: "Banana" },
{ icon: "🫐", label: "Blueberries" },
{ icon: "🥂", label: "Champers?" },
];
const [tomato, lettuce, cheese] = allIngredients;
const tabs = [tomato, lettuce, cheese];
- 구조
- 최상위 부모 <div> : 화면 전체를 사용, 박스를 화면 가로/세로 정중앙에 배치하기 위함
- 중간 카드 <div> : 탭과 이모지가 담길 컨테이너
- <nav> : 상단 메뉴
- <motion.li> : 각각의 탭 버튼 자체를 담당하는 컴포넌트
- <motion.div> : 탭 메뉴 밑의 핑크색 밑줄
- <main> : 콘텐츠
- max-w-[calc(100% - 40px)]
- 최대 너비를 브라우저 전체 가로폭(100%)에서 40px을 뺀 크기까지만 허용
- list-none
- <ul>, <li> 태그 사용 시 나타나는 왼쪽 점을 지워주는 역할
- select-none
- 글자나 이모지를 드래그하거나 더블클릭했을 때, 파랗게 선택되는 현상 방지
- initial={false}
- 처음 켤 때 빌드업 애니메이션을 생략
- ㄴ 첫 로딩 시 불필요한 색상 빌드업을 차단
- mode="wait"
- AnimatePresence 내부에서 교체가 일어날 때 기존 컴포넌트가 완전히 사라질 때까지 새 컴포넌트를 대기시킴
- 화면 전환이 깔끔해짐
- leading-none
- 폰트 줄간격을 1로 만들어 위아래 여백을 삭제
- allIngredients
- 확장성을 위한 전체 데이터베이스 역할
✔️ Exit animations


// src/components/ExitAnimation.tsx
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
export default function ExitAnimation() {
const [isVisible, setIsVisible] = useState(true);
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<div className="flex flex-col relative w-24 h-44">
<AnimatePresence initial={false}>
{isVisible ? (
<motion.div
className="w-24 h-24 bg-[#ff0055] rounded-lg"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
key="box"
/>
) : null}
</AnimatePresence>
<motion.button
className="bg-[#ff0055] rounded-lg py-2.5 absolute bottom-0 left-0 right-0 cursor-pointer text-white"
onClick={() => setIsVisible(!isVisible)}
whileTap={{ y: 1 }}
>
{isVisible ? "Hide" : "Show"}
</motion.button>
</div>
</div>
);
}
- <AnimatePresence>
- 리액트) 조건부 렌더링 사용 --> 조건이 false가 되면 컴포넌트를 화면에서 즉시 삭제
- AnimatePresence가 사라져야 할 컴포넌트를 화면에 붙잡아둠
- ㄴ exit 애니메이션을 보여줘야 하기 때문에
- initial={false}
- initial : 컴포넌트가 화면에 처음 등장할 때의 애니메이션을 제어하는 속성
- exit={{ opacity: 0, scale: 0 }}
- exit : 컴포넌트가 화면에서 사라질 때의 애니메이션을 제어하는 속성
- 투명도 0, 크기 0
- key="box"
- Framer Motion은 어떤 컴포넌트가 사라졌고 생겼는지를 key 값을 기준으로 추적
- whileTap={{ y: 1 }}
- 버튼을 클릭하고 있는 순간 실행
- y축으로 1픽셀만큼 내려감
✔️ SVG aniamtions


// src/components/UseTransfomr.tsx
import { motion, useMotionValue, useTransform } from "motion/react";
export default function UseTransform() {
const x = useMotionValue(0);
const xInput = [-100, 0, 100];
const background = useTransform(x, xInput, [
"linear-gradient(180deg, #ff008c 0%, rgb(211, 9, 225) 100%)",
"linear-gradient(180deg, #7700ff 0%, rgb(68, 0, 255) 100%)",
"linear-gradient(180deg, rgb(230, 255, 0) 0%, rgb(3, 209, 0) 100%)",
]);
const color = useTransform(x, xInput, [
"rgb(211, 9, 225)",
"rgb(68, 0, 255)",
"rgb(3, 209, 0)",
]);
const tickPath = useTransform(x, [10, 100], [0, 1]);
const crossPathA = useTransform(x, [-10, -55], [0, 1]);
const crossPathB = useTransform(x, [-50, -100], [0, 1]);
return (
<div className="flex items-center justify-center min-h-screen">
<motion.div
className="flex justify-center items-center flex-1 w-125 h-75 max-w-full rounded-3xl"
style={{ background }}
>
<motion.div
className="w-35 h-35 bg-white rounded-3xl p-5 cursor-grab"
style={{ x }}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.5}
>
<svg viewBox="0 0 50 50">
{/* 배경 원형 패스 */}
<motion.path
fill="none"
strokeWidth="2"
stroke={color}
d="M 0, 20 a 20, 20 0 1,0 40,0 a 20, 20 0 1,0 -40,0"
style={{ x: 5, y: 5 }}
/>
{/* 틱(V) 아이콘 패스 */}
<motion.path
id="tick"
fill="none"
strokeWidth="2"
stroke={color}
d="M14,26 L 22,33 L 35,16"
strokeDasharray="0 1"
style={{ pathLength: tickPath }}
/>
{/* 엑스(X) 첫 번째 선 패스 */}
<motion.path
fill="none"
strokeWidth="2"
stroke={color}
d="M17,17 L33,33"
strokeDasharray="0 1"
style={{ pathLength: crossPathA }}
/>
{/* 엑스(X) 두 번째 선 패스 */}
<motion.path
id="cross"
fill="none"
strokeWidth="2"
stroke={color}
d="M33,17 L17,33"
strokeDasharray="0 1"
style={{ pathLength: crossPathB }}
/>
</svg>
</motion.div>
</motion.div>
</div>
);
}
- const x = useMotionValue(0)
- useMotionValue : 컴포넌트를 리렌더링하지 않고도 박스의 현재 좌표 값을 부드럽게 업데이트하며 저장
- 0 : 맨처음 시작하는 정중앙 위치
- const xInput = [-100, 0, 100]
- 색상과 모양을 바꾸기 위한 수치 기준 배열
- -100 : 왼쪽으로 100px
- 100 : 오른쪽으로 100px
- const background
- 드래그에 반응하는 배경 그라데이션 값
- const color
- SVG(동그라미, 체크, 엑스 패스) 색상 값
- const tickPath = useTransform(x, [10, 100], [0, 1])
- useTransform : 값 매핑 훅
- useTransform(감시할값, [입력기준점들], [결과물들])
- 배열 안의 요소들을 매핑
- 체크 패스를 그리는 네비게이터
- x 좌표가 10px에서 100px로 이동하는 동안
- 패스의 선 길이 비율을 0(0%)에서 1(100%)로 매핑
- useTransform : 값 매핑 훅
- const corssPath
- 엑스 패스를 2단계로 나눠 그리는 네비게이터
- const crossPathA = useTransform(x, [-10, -55], [0, 1])
- x 좌표가 -10px에서 -55px로 이동하는 동안
- const crossPathB = useTransform(x, [-50, -100], [0, 1])
- x 좌표가 -50px에서 -100px로 이동하는 동안
- drag="x"
- 가로 드래그 허용 활성화
- x로 지정했기 때문에 양옆으로만 이동 가능
- dragConstraints={{ left: 0, right: 0 }}
- 드래그의 이동 제한 구역을 설정하는 옵션
- 마우스 버튼에서 손을 떼는 순간 정중앙으로 되돌아오도록 설정
- dragElastic={0.5}
- 드래그할 때 묵직함을 주는 고무줄 탄성도
- 0 : 탄성 X --> 좌우로 움직이지 X
- 1 : 저항력 X --> 마우스 커서가 가볍게 따라옴
- strokeDasharray="0 1"
- 선그리기 애니메이션을 위한 SVG 세팅의 밑작업
- 선을 0만큼 그리고, 공백을 1만큼 채우라는 뜻
- == 처음에는 선을 완전히 숨겨 안 보이도록 만듦
- d="M 0,20 a 20,20 0 1,0 40,0 a 20,20 0 1,0 -40,0"
- M(Move to) : 시작점으로 이동 / a(Arc) : 부드러운 곡선
- M 0,20 : (0, 20) 좌표 지점에서 시작
- a 20,20 0 1,0 40,0
- 20,20 : 곡선의 가로 반지름 20px, 세로 반지름 20px
- 0 : 회전 각도 0도
- 1,0 : 위쪽으로 볼록한 반원을 그리며 시계방향으로 돌기
- 40,0 : 최종 목적지 좌표 (40, 0)
OverView
✔️ Keyframes



// src/components/Keyframes.tsx
import * as motion from "motion/react-client";
export default function Keyframes() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div
className="w-24 h-24 bg-white rounded-md"
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 180, 180, 0],
borderRadius: ["0%", "0%", "50%", "50%", "0%"],
}}
transition={{
duration: 2,
ease: "easeInOut",
times: [0, 0.2, 0.5, 0.8, 1],
repeat: Infinity,
repeatDelay: 1,
}}
/>
</div>
);
}
- animate={{ scale: [1, 2, 2, 1, 1], rotate: [0, 0, 180, 180, 0], borderRadius: ["0%", "0%", "50%", "50%", "0%"] }}
- scale: [1, 2, 2, 1, 1]
- 크기를 1배 --> 2배 --> 2배 유지 --> 1배 --> 1배 유지 순으로 변환
- rotate: [0, 0, 180, 180, 0]
- 회전각을 0도 --> 0도 유지 --> 180도 회전 --> 180도 유지 --> 0도 순으로 변환
- borderRadius: ["0%", "0%", "50%", "50%", "0%"]
- 모양을 네모 --> 네모 유지 --> 동그라미 --> 동그라미 유지 --> 네모 순으로 변환
- scale: [1, 2, 2, 1, 1]
- transition={{ duration: 2, ease: "easeInOut", times: [0, 0.2, 0.5, 0.8, 1], repeat: Infinity, repeatDelay: 1 }}
- duration: 2
- 전체 한 바퀴 도는 애니메이션의 총 재생 시간이 2초
- ease: "easeInOut"
- 시작할 때 부드럽게 가속, 끝날 때 부드럽게 감속
- times: [0, 0.2, 0.5, 0.8, 1]
- 배열 안의 배열들이 전체 2초 중 어느 타이밍에 일어날지 정하는 타임라인 인터벌
- repeat: Infinity
- 무한 반복
- repeatDelay: 1
- 다음 애니메이션 시작까지 1초 대기
- duration: 2
✔️ Motion along a path



// src/components/AddToBasket.tsx
import { motion, arc, useAnimate, useMotionValue } from "motion/react";
import { useRef, useState } from "react";
const PRODUCT_SIZE = 160;
const BASKET_BOX = 56;
const FLY_SCALE = BASKET_BOX / PRODUCT_SIZE;
type Direction = "auto" | "cw" | "ccw";
interface AddToBasketProps {
strength?: number;
peak?: number;
rotate?: number;
duration?: number;
basketVelocityFactor?: number;
direction?: Direction;
}
export default function AddToBasket({
strength = 0.5,
peak = 0.15,
rotate = 0.9,
duration = 0.45,
basketVelocityFactor = 0.05,
direction = "cw",
}: AddToBasketProps = {}) {
const [scope, animate] = useAnimate();
const productRef = useRef<HTMLDivElement>(null);
const basketRef = useRef<HTMLDivElement>(null);
const ringRef = useRef<HTMLDivElement>(null);
const [isFlying, setIsFlying] = useState(false);
const productX = useMotionValue(0);
const productY = useMotionValue(0);
const addToBasket = async () => {
const product = productRef.current;
const basket = basketRef.current;
const ring = ringRef.current;
if (!product || !basket || !ring || isFlying) return;
setIsFlying(true);
const from = product.getBoundingClientRect();
const to = basket.getBoundingClientRect();
const dx = to.left + to.width / 2 - (from.left + from.width / 2);
const dy = to.top + to.height / 2 - (from.top + from.height / 2);
// product를 조종하는 animate
await animate(
product,
{
x: dx,
y: dy,
scale: FLY_SCALE,
opacity: [1, 1, 0],
},
{
duration,
path: arc({
strength,
peak,
rotate,
direction: direction === "auto" ? undefined : direction,
}),
ease: [0.74, 0.18, 0.93, 0.69],
opacity: { inherit: true, times: [0, 0.95, 1] },
},
);
// basket을 조종하는 animate
animate(
basket,
{ x: 0, y: 0 },
{
type: "spring",
stiffness: 500,
damping: 12,
x: {
inherit: true,
velocity: productX.getVelocity() * basketVelocityFactor,
},
y: {
inherit: true,
velocity: productY.getVelocity() * basketVelocityFactor,
},
},
);
// 파동을 조종하는 animate
animate(
ring,
{ scale: [1, 2.2], opacity: [0.8, 0] },
{ duration: 0.5, ease: "easeOut" },
);
// 충돌 이후 product를 조종하는 animate
animate(
product,
{
x: 0,
y: 0,
scale: 0.9,
rotate: 0,
opacity: 0,
clipPath: "inset(0%)",
},
{ duration: 0 },
);
// 재등장하는 product를 조종하는 animate
await animate(
product,
{ opacity: 1, scale: 1 },
{
scale: { type: "spring", visualDuration: 0.4, bounce: 0.35 },
opacity: { duration: 0.25, ease: "easeOut" },
},
);
setIsFlying(false);
};
return (
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<div
className="absolute inset-0 flex items-center justify-center overflow-hidden font-mono text-white"
ref={scope}
>
<style>{`#sandbox { position: relative }`}</style>
<div
className="absolute top-20 right-20 flex items-center justify-center bg-[--layer] border border-[--border] text-[#ff0055] will-change-transform"
style={{ width: BASKET_BOX, height: BASKET_BOX }}
ref={basketRef}
>
<motion.div
className="absolute -inset-px border border-[--accent] opacity-0 pointer-events-none will-change-transform"
ref={ringRef}
/>
<BasketIcon />
</div>
<div className="flex flex-col items-center gap-4.5">
<motion.div
className="flex items-center justify-center bg-[--layer] border border-[--border] will-change-transform"
style={{
x: productX,
y: productY,
width: PRODUCT_SIZE,
height: PRODUCT_SIZE,
}}
ref={productRef}
>
<span className="text-7xl leading-none select-none">👟</span>
</motion.div>
<div className="flex items-baseline gap-3 font-mono text-xs tracking-wide">
<span className="text-white">Campus 00s</span>
<span className="text-[#ff0055]">£128</span>
</div>
<motion.button
className="mt-1 px-6.5 py-3 border-none bg-[#ff0055] text-[--background] font-mono text-xs tracking-[0.12em] uppercase cursor-pointer"
type="button"
onClick={addToBasket}
disabled={isFlying}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
style={{
opacity: isFlying ? 0.55 : 1,
pointerEvents: isFlying ? "none" : "auto",
}}
>
Add to Basket
</motion.button>
</div>
</div>
</div>
);
}
function BasketIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 11-1 9" />
<path d="m19 11-4-7" />
<path d="M2 11h20" />
<path d="m3.5 11 1.6 7.4a2 2 0 0 0 2 1.6h9.8a2 2 0 0 0 2-1.6l1.7-7.4" />
<path d="M4.5 15.5h15" />
<path d="m5 11 4-7" />
<path d="m9 11 1 9" />
</svg>
);
}
- const productRef = useRef<HTMLDivElement>(null)
- useRef 훅 : 실제 HTML 태그에 DOM 접근을 하거나, 화면 리렌더링 없이 값을 저장할 수 있게 해주는 역할
- 실제 신발 상자(product) <div> 태그의 위치 좌표와 알맹이 속성에 접근하기 위한 화살표 선언
- const productX = useMotionValue(0)
- useMotionValue() : 리액트 화면을 리렌더링하지 않고도, 움직이는 요소의 수치를 부드럽게 저장, 추적할 수 있게 해주는 역할
- 움직이는 product의 실시간 x좌표값과 물리 속도를 측정하기 위한 엔진 선언
- const addToBasket = async () => {}
- async(Asynchronous) : 함수 내부에서 비동기 제어를 할 것임을 선언
- await : 해당 작업이 완벽하게 끝날 때까지 아랫줄의 코드들이 실행되지 않도록 정지
- const product = productRef.current
- .current : useRef 안에 들어있는 실제 HTML 태그를 꺼내오도록 하는 속성
- const from = product.getBoundingClientRect()
- getBoundingClientRect() : 브라우저 화면 내에서 현재 태그의 실제 x, y 좌표와 크기 정보를 실시간으로 따오는 함수
- const dx = to.left + to.width / 2 - (from.left + from.width / 2)
- product의 정중앙 지점부터 basket의 정중앙 지점까지 가로로 몇 픽셀, 세로로 몇 픽셀 이동해야 하는지 계산해 낸 최종 거리값
- stiffness : 500
- spring이 얼마나 팽팽한지 결정
- dampting : 12
- 브라우저 공기 저항이나 마찰력을 의미
- 小 : 공기 저항 ↓ / 大 : 공기 저항 ↑
- x: { inherit: true, velocity: productX.getVelocity() * basketVelocityFactor }
- inherit: true
- product가 날아오던 힘과 방향을 그대로 상속받아 basket에게 충격 전달
- inherit: true
- style={{ width: BASKET_BOX, heitght: BASKET_BOX }}
- tailwind css에서는 상수값만큼 값을 줄 수가 X
- style로 따로 빼서 값 전달
- ref={basketRef}
- 미리 선언해둔 화살표 변수(basketRef)를 실제 HTML 태그에 수동으로 바인딩
- 앞으로 basketRef.current를 호출하면, 방금 그린 장바구니 <div> 태그 알맹이를 통째로 꺼내 전달하게 됨
✔️ Animate Content


// src/components/HTMLContent.tsx
import { animate } from "motion";
import { motion, useMotionValue, useTransform } from "motion/react";
import { useEffect } from "react";
export default function HTMLContent() {
const count = useMotionValue(0);
const rounded = useTransform(() => Math.round(count.get()));
useEffect(() => {
const controls = animate(count, 100, { duration: 5 });
return () => controls.stop();
}, [count]);
return (
<>
<div className="flex items-center justify-center min-h-screen bg-[#0f1115]">
<motion.div className="font-mono text-6xl leading-none text-[#ff0055]">
{rounded}
</motion.div>
</div>
</>
);
}
- const count = useMotionValue(0)
- 컴포넌트를 리렌더링하지 않고 메모리 속에서만 숫자 데이터를 빠르게 계산하며 저장
- const rounded = useTransform(() => Math.round(count.get()))
- count가 변할 때마다 가져와서 반올림한 정수만 rounded에 저장
- useEffect(() => {}, [count])
- 컴포넌트가 마운트 된 순간 + count를 감지하여 값이 변할 때마다 함수 실행
- const controls = animate(count, 100, { duration: 5 })
- count 안의 숫자를 5초 동안, 100까지 증가
- return () => controls.stop()
- 컴포넌트가 화면에서 사라질 때는 애니메이션 즉시 중단
'React' 카테고리의 다른 글
| 리액트 - 모달 구현 (0) | 2026.06.25 |
|---|---|
| 리액트 - Toggle Button 구현 (0) | 2026.05.28 |