개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Motion — React 애니메이션 라이브러리

2026년 6월 5일
React애니메이션MotionFramer Motion

CSS 애니메이션만으로는 처리하기 어려운 경우가 있습니다. 컴포넌트가 DOM에서 제거될 때 exit 애니메이션을 실행하거나, 레이아웃이 변경될 때 위치 이동을 자연스럽게 보간하거나, 드래그와 스크롤에 반응하는 물리 기반 애니메이션을 구현하려면 JavaScript의 도움이 필요합니다.

Motion(구 Framer Motion)은 이런 요구를 선언적 API로 해결합니다. 시작 상태와 끝 상태를 props로 정의하면 나머지는 Motion이 처리합니다. React의 선언적 스타일과 잘 어울립니다.

설치

npm install motion

1. Motion Component

motion.div, motion.span, motion.button 등 모든 HTML 요소에 대응하는 Motion 컴포넌트가 있습니다. 일반 HTML 요소처럼 사용하되, animate, initial, exit 같은 애니메이션 props를 추가로 지원합니다.

import { motion } from "motion/react";

// 기본 애니메이션
function FadeIn() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}   // 시작 상태
      animate={{ opacity: 1, y: 0 }}    // 목표 상태
      transition={{ duration: 0.4 }}    // 타이밍
    >
      콘텐츠
    </motion.div>
  );
}

// 상태에 따른 애니메이션
function ExpandablePanel({ isOpen }: { isOpen: boolean }) {
  return (
    <motion.div
      animate={{
        height: isOpen ? "auto" : 0,
        opacity: isOpen ? 1 : 0,
      }}
      transition={{ duration: 0.3, ease: "easeInOut" }}
      style={{ overflow: "hidden" }}
    >
      패널 내용
    </motion.div>
  );
}

initial은 컴포넌트가 처음 마운트될 때의 상태, animate는 목표 상태입니다. Motion은 두 상태 사이를 자동으로 보간합니다.


2. AnimatePresence

React는 컴포넌트를 조건부로 렌더링할 때 즉시 DOM에서 제거합니다. exit 애니메이션이 실행될 틈이 없습니다. AnimatePresence는 자식 컴포넌트의 언마운트를 가로채고, exit 애니메이션이 완료된 후 DOM에서 제거합니다.

import { motion, AnimatePresence } from "motion/react";

function Notification({ show, message }: { show: boolean; message: string }) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          key="notification"    // key 필수 — Motion이 요소를 추적함
          initial={{ opacity: 0, x: 100 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: 100 }}  // 언마운트 시 실행
          transition={{ duration: 0.3 }}
          className="fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg"
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

mode prop으로 여러 자식이 전환될 때의 동작을 제어합니다.

// mode="wait" — 이전 요소 exit 완료 후 다음 요소 enter
// mode="sync" — 동시에 실행 (기본값)
// mode="popLayout" — 이전 요소를 레이아웃에서 즉시 제거

<AnimatePresence mode="wait">
  {currentPage === "home" && <motion.div key="home" ...>홈</motion.div>}
  {currentPage === "about" && <motion.div key="about" ...>소개</motion.div>}
</AnimatePresence>

3. Transition

Transition: 애니메이션의 지속 시간, 지연, 이징 함수, 물리 파라미터 등 타이밍을 제어하는 옵션 객체

transition prop으로 애니메이션이 어떻게 재생될지 제어합니다. Motion은 tween, spring, inertia 세 가지 애니메이션 타입을 지원합니다.

// tween — CSS transition과 유사, duration과 ease로 제어
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: "tween",
    duration: 0.5,
    ease: "easeOut",     // "linear" | "easeIn" | "easeOut" | "easeInOut" | [x1, y1, x2, y2]
    delay: 0.2,
  }}
/>

// spring — 물리 기반 스프링 애니메이션, 자연스러운 움직임
<motion.div
  animate={{ scale: 1.2 }}
  transition={{
    type: "spring",
    stiffness: 300,   // 스프링 강도 (높을수록 빠름)
    damping: 20,      // 감쇠 (낮을수록 통통 튐)
    mass: 1,          // 질량
  }}
/>

// 속성별 다른 transition 적용
<motion.div
  animate={{ opacity: 1, x: 0 }}
  transition={{
    opacity: { duration: 0.2 },
    x: { type: "spring", stiffness: 400 },
  }}
/>

Motion의 기본 애니메이션 타입은 spring입니다. 숫자 값에는 spring을, 색상과 불투명도에는 tween을 자동으로 선택합니다.


4. Layout Animation

레이아웃 변경 애니메이션은 CSS만으로는 구현하기 까다롭습니다. 요소가 다른 위치로 이동하거나 크기가 바뀔 때 CSS transition은 절대 위치 변화를 추적하지 못합니다. Motion의 layout prop은 이를 해결합니다.

// 목록 정렬 애니메이션
function SortableList({ items }: { items: string[] }) {
  return (
    <ul>
      {items.map((item) => (
        <motion.li
          key={item}
          layout              // 위치 변경 시 자동 애니메이션
          layoutId={item}     // 다른 트리 간 이동 추적
          className="p-3 mb-2 bg-white rounded shadow"
        >
          {item}
        </motion.li>
      ))}
    </ul>
  );
}

// 크기 변경 애니메이션
function Card({ isExpanded }: { isExpanded: boolean }) {
  return (
    <motion.div
      layout                  // 높이 변경 시 자동 보간
      className="bg-white rounded-lg p-4 shadow"
    >
      <h2>제목</h2>
      {isExpanded && (
        <motion.p layout="position">  {/* 위치만 추적 */}
          확장된 내용
        </motion.p>
      )}
    </motion.div>
  );
}

layout 값으로 true(전체), "position"(위치만), "size"(크기만)를 선택할 수 있습니다.

layoutId를 사용하면 컴포넌트가 다른 위치에 마운트되어도 이전 위치에서 부드럽게 이동하는 공유 레이아웃 애니메이션을 구현할 수 있습니다.

// 탭 인디케이터 공유 레이아웃
function Tabs({ tabs, active, onChange }: TabsProps) {
  return (
    <div className="flex">
      {tabs.map((tab) => (
        <button key={tab} onClick={() => onChange(tab)} className="relative px-4 py-2">
          {tab}
          {active === tab && (
            <motion.div
              layoutId="tab-indicator"   // 같은 layoutId끼리 공유 애니메이션
              className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
            />
          )}
        </button>
      ))}
    </div>
  );
}

5. Gesture

Gesture: whileHover, whileTap, whileDrag 등 사용자 인터랙션에 반응하여 애니메이션을 실행하는 props

Motion은 마우스, 터치, 드래그 등의 제스처를 props로 선언합니다. 이벤트 핸들러를 직접 작성하지 않아도 됩니다.

// 호버와 클릭 애니메이션
<motion.button
  whileHover={{ scale: 1.05, backgroundColor: "#2563eb" }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
  className="px-6 py-3 bg-blue-600 text-white rounded-lg"
>
  클릭
</motion.button>

// 드래그 가능한 요소
<motion.div
  drag                              // x, y 방향 모두 드래그
  dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
  dragElastic={0.1}                 // 경계 밖으로 나가는 탄성
  whileDrag={{ scale: 1.1 }}
  className="w-20 h-20 bg-blue-500 rounded-full cursor-grab"
/>

// 스크롤 연동 (useScroll + useTransform)
import { useScroll, useTransform } from "motion/react";

function ParallaxHeader() {
  const { scrollY } = useScroll();
  const y = useTransform(scrollY, [0, 300], [0, -150]);
  const opacity = useTransform(scrollY, [0, 200], [1, 0]);

  return (
    <motion.div style={{ y, opacity }} className="fixed top-0 w-full h-64">
      배경 이미지
    </motion.div>
  );
}

6. Variants

Variants는 애니메이션 상태를 이름으로 추상화하고, 복잡한 컴포넌트 트리에서 애니메이션을 조율합니다. 부모의 animate 변경이 자식에 자동으로 전파되어 순차적 애니메이션을 선언적으로 표현할 수 있습니다.

// Variant 정의
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,    // 자식 간 딜레이
      delayChildren: 0.2,      // 첫 자식 딜레이
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { type: "spring", stiffness: 300 },
  },
};

function AnimatedList({ items }: { items: string[] }) {
  return (
    // 부모의 animate="visible" 이 자식에 전파됨
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.li
          key={item}
          variants={itemVariants}  // animate prop 없어도 부모에서 전파받음
          className="p-3 mb-2 bg-white rounded shadow"
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}
  Container: animate="visible"
       |
       | 전파 (staggerChildren: 0.1)
       v
  Item 1: hidden -> visible  (0.2s 후 시작)
  Item 2: hidden -> visible  (0.3s 후 시작)
  Item 3: hidden -> visible  (0.4s 후 시작)

Variants는 useAnimation 훅과 결합하여 명령형으로 제어할 수도 있습니다.

import { useAnimation } from "motion/react";

function ControlledAnimation() {
  const controls = useAnimation();

  const handleClick = async () => {
    await controls.start("highlight");
    await controls.start("normal");
  };

  return (
    <motion.div
      animate={controls}
      variants={{
        highlight: { backgroundColor: "#fbbf24", scale: 1.05 },
        normal: { backgroundColor: "#ffffff", scale: 1 },
      }}
      onClick={handleClick}
    />
  );
}

실전 패턴: 페이지 전환 애니메이션

// PageTransition.tsx
import { motion, AnimatePresence } from "motion/react";
import { useLocation } from "react-router-dom";

const pageVariants = {
  initial: { opacity: 0, x: -20 },
  enter: { opacity: 1, x: 0 },
  exit: { opacity: 0, x: 20 },
};

function PageTransition({ children }: { children: React.ReactNode }) {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={location.pathname}
        variants={pageVariants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{ duration: 0.25 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

정리

Motion은 React의 선언적 패러다임을 애니메이션 영역으로 확장합니다.

개념역할
Motion Componentmotion.div 등 애니메이션 props를 가진 HTML 래퍼
AnimatePresence언마운트 시 exit 애니메이션 실행
Transition속도, 지연, 이징, 스프링 파라미터 제어
Layout Animationlayout prop으로 크기/위치 변경 자동 보간
Gesturehover, tap, drag 인터랙션 선언적 처리
Variants상태를 이름으로 정의하고 자식에 전파

CSS 애니메이션으로 처리하기 어려운 언마운트 효과, 레이아웃 변경 보간, 목록 재정렬 같은 시나리오에서 Motion이 특히 강점을 발휘합니다.

← 이전 글React Testing Library — 사용자 관점에서 컴포넌트를 테스트
다음 글 →Radix UI — 접근성을 고려한 Headless 컴포넌트