React

리액트 애니메이션 - Framer Motion

favor 2026. 6. 13. 19:04

https://motion.dev/

 

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

360도 회전하는 div 박스

// 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 되는 순간(새로고침)에 자동 실행

 

크기가 2배 커지는 div 박스

// 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

탄성을 가지고 등장하는 div 박스

// 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

hover, tap 시 크기가 바뀌는 div 박스

// 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

토글 div 박스

// 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 등 바뀐 정렬 방식을 알아채고 자동으로 이동

 

nav 선택값에 따라 데이터를 출력하는 div 박스

// 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

버튼으로 등장, 퇴장하는 div 박스

// 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

인터랙티브 드래그에 따라 실시간으로 디자인이 변경되는 div 박스

// 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%)로 매핑 
  • 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

시간 흐름에 따라 모양이 달라지는 div 박스

// 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%"]
      • 모양을 네모 --> 네모 유지 --> 동그라미 --> 동그라미 유지 --> 네모 순으로 변환 
  • 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초 대기 

 

✔️ Motion along a path

포물선으로 날아가는 div 박스와 파동으로 흔들리는 div 박스

// 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에게 충격 전달  
  • style={{ width: BASKET_BOX, heitght: BASKET_BOX }}
    • tailwind css에서는 상수값만큼 값을 줄 수가 X
    • style로 따로 빼서 값 전달
  • ref={basketRef}
    • 미리 선언해둔 화살표 변수(basketRef)를 실제 HTML 태그에 수동으로 바인딩
    • 앞으로 basketRef.current를 호출하면, 방금 그린 장바구니 <div> 태그 알맹이를 통째로 꺼내 전달하게 됨 

 

✔️ Animate Content

0부터 100까지 숫자가 증가하는 div 박스

// 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