프론트엔드/React

(React) Framer Motion 애니메이션 만들어보기

그린티_ 2025. 12. 1. 11:50
반응형

Framer Motion으로 React 애니메이션 마스터하기

서론

React에서 애니메이션을 만들려고 하니까 CSS 애니메이션도 있고, 라이브러리도 여러 개더라 그 중에 Framer Motion이라는 게 있다고 들었음. 처음엔 "그냥 CSS로 하면 되지 않나?" 싶었는데, 찾아보니 훨씬 강력하더라 Framer Motion을 쓰면 복잡한 애니메이션을 간단하게 만들 수 있다고 함. 그래서 Framer Motion이 뭐고, 어떻게 쓰는지 정리해봄.

본론

Framer Motion이 뭐야?

Framer Motion은 React를 위한 애니메이션 라이브러리 선언적 문법으로 복잡한 애니메이션을 간단하게 만들 수 있음

Framer Motion의 철학:

  • 애니메이션을 "선언적으로" 작성 (CSS 프레임처럼)
  • JSX 안에서 완전히 제어 가능
  • 상태 변화에 따라 자동으로 애니메이션
  • 성능 최적화 (GPU 가속)

기본 문법부터 시작

설치:

npm install framer-motion

가장 간단한 애니메이션

import { motion } from 'framer-motion';

export default function SimpleAnimation() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      안녕하세요!
    </motion.div>
  );
}

뭐가 뭐냐면:

  • motion.div: 애니메이션 가능한 div
  • initial: 초기 상태 (처음 모양)
  • animate: 최종 상태 (어떻게 변할지)
  • transition: 애니메이션 속도 (0.5초)

흐름:

1. 컴포넌트 마운트
   ↓ initial 상태로 시작
   opacity: 0 (투명), y: 20 (아래로 20px)
   ↓ 0.5초에 걸쳐 animate로 변함
   ↓ 최종 상태
   opacity: 1 (보임), y: 0 (원래 위치)

상태 변화에 따른 애니메이션

위 예시는 마운트할 때만 애니메이션됨 그런데 상태가 바뀔 때마다 애니메이션하려면?

import { motion } from 'framer-motion';
import { useState } from 'react';

export default function StateAnimation() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? '닫기' : '열기'}
      </button>

      <motion.div
        initial={{ height: 0, opacity: 0 }}
        animate={{ height: isOpen ? 300 : 0, opacity: isOpen ? 1 : 0 }}
        transition={{ duration: 0.5 }}
        style={{ overflow: 'hidden', background: '#f0f0f0' }}
      >
        <div style={{ padding: '20px' }}>
          <p>이것은 토글 가능한 콘텐츠입니다!</p>
        </div>
      </motion.div>
    </div>
  );
}

동작:

  1. "열기" 버튼 클릭
  2. isOpen 상태 변경
  3. Framer Motion이 자동으로 감지
  4. animate 값을 새로 계산
  5. 0.5초에 걸쳐 애니메이션 재생

Variants로 복잡한 애니메이션 관리

복잡한 애니메이션이 많으면 코드가 복잡해짐 그럴 땐 variants로 정리하는 게 좋음

import { motion } from 'framer-motion';

// 애니메이션 정의를 따로 만듦
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1 // 자식들이 순차적으로 애니메이션
    }
  }
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
};

export default function ListAnimation() {
  const items = ['항목 1', '항목 2', '항목 3', '항목 4'];

  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.li
          key={item}
          variants={itemVariants}
          style={{ listStyle: 'none', padding: '10px' }}
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

뭐가 뭐냐면:

  • containerVariants: 부모 애니메이션 정의
  • itemVariants: 자식 애니메이션 정의
  • staggerChildren: 0.1초씩 차이나면서 순차 애니메이션

결과:

항목 1 → (0.1초 대기) → 항목 2 → (0.1초 대기) → 항목 3 → ...

제스처 애니메이션 (Hover, Tap)

마우스 호버나 클릭에 반응하는 애니메이션

import { motion } from 'framer-motion';

export default function GestureAnimation() {
  return (
    <motion.button
      whileHover={{ scale: 1.1, backgroundColor: '#ff6b6b' }}
      whileTap={{ scale: 0.95 }}
      transition={{ duration: 0.2 }}
      style={{
        padding: '10px 20px',
        fontSize: '16px',
        border: 'none',
        borderRadius: '4px',
        backgroundColor: '#4ecdc4',
        color: 'white',
        cursor: 'pointer'
      }}
    >
      클릭해보세요!
    </motion.button>
  );
}

동작:

  • whileHover: 마우스 올렸을 때 (크기 1.1배, 색상 변경)
  • whileTap: 클릭했을 때 (크기 0.95배로 축소 - 누르는 느낌)

결과:

마우스 올림 → 버튼 커짐 + 색상 변경
클릭 → 살짝 축소 (누르는 느낌)

드래그 애니메이션

마우스로 드래그해서 움직이는 애니메이션

import { motion } from 'framer-motion';

export default function DragAnimation() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
      dragElastic={0.2}
      whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
      style={{
        width: '100px',
        height: '100px',
        backgroundColor: '#4ecdc4',
        borderRadius: '8px',
        cursor: 'grab'
      }}
    />
  );
}

뭐가 뭐냐면:

  • drag: 드래그 가능하게 만듦
  • dragConstraints: 드래그 범위 제한
  • dragElastic: 탄성 효과 (0.2 = 20% 반동)
  • whileDrag: 드래그 중 애니메이션

결과:

마우스로 잡아서 끌기 가능
범위 넘어가면 자동으로 돌아옴 (탄성)

스크롤 기반 애니메이션

스크롤할 때 요소가 애니메이션

import { motion, useViewportScroll, useTransform } from 'framer-motion';

export default function ScrollAnimation() {
  const { scrollY } = useViewportScroll();
  const opacity = useTransform(scrollY, [0, 300], [1, 0]);
  const y = useTransform(scrollY, [0, 300], [0, -100]);

  return (
    <div style={{ height: '200vh' }}>
      <motion.div
        style={{ opacity, y }}
        style={{
          position: 'fixed',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
          fontSize: '24px'
        }}
      >
        스크롤하면 사라집니다!
      </motion.div>

      <div style={{ padding: '100px', background: '#f5f5f5' }}>
        <p>여기서부터 스크롤하면 위의 텍스트가 사라져요!</p>
      </div>
    </div>
  );
}

동작:

  • scrollY가 0일 때: opacity 1 (완전히 보임), y 0
  • scrollY가 300일 때: opacity 0 (투명), y -100 (위로 100px)
  • 그 사이는 자동으로 보간됨

CSS 애니메이션 vs Framer Motion

항목 CSS 애니메이션 Framer Motion

문법 복잡함 (@keyframes) 간단함 (JSX)
상태 연동 어려움 쉬움
제스처 없음 풍부함 (hover, tap, drag)
성능 좋음 좋음 (GPU 가속)
번들 크기 0kb ~40kb
복잡도 낮음 높을 수 있음

CSS 애니메이션:

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.element {
  animation: slideIn 0.5s ease-out;
}

Framer Motion:

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5, ease: 'easeOut' }}
/>

Framer Motion이 훨씬 간단하고 상태와 연동하기 쉬움!

실제 활용 예시: 모달 애니메이션

import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

const backdropVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 }
};

const modalVariants = {
  hidden: { y: 50, opacity: 0 },
  visible: { y: 0, opacity: 1 },
  exit: { y: 50, opacity: 0 }
};

export default function ModalAnimation() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>
        모달 열기
      </button>

      <AnimatePresence>
        {isOpen && (
          <>
            {/* 배경 */}
            <motion.div
              variants={backdropVariants}
              initial="hidden"
              animate="visible"
              exit="hidden"
              onClick={() => setIsOpen(false)}
              style={{
                position: 'fixed',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: 'rgba(0, 0, 0, 0.5)'
              }}
            />

            {/* 모달 */}
            <motion.div
              variants={modalVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              style={{
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                backgroundColor: 'white',
                padding: '40px',
                borderRadius: '8px',
                zIndex: 10
              }}
            >
              <h2>모달 제목</h2>
              <p>모달 내용입니다.</p>
              <button onClick={() => setIsOpen(false)}>
                닫기
              </button>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}

핵심:

  • AnimatePresence: 컴포넌트가 DOM에서 제거될 때 exit 애니메이션 실행
  • 배경과 모달이 동시에 나타났다가 동시에 사라짐

성능 최적화 팁

1. 불필요한 리렌더링 방지

// ❌ 나쁜 예
const containerVariants = {
  hidden: { opacity: 0 }
};

export default function Bad() {
  const [count, setCount] = useState(0);
  // 매번 새로운 variants 객체 생성
  return <motion.div variants={containerVariants} />;
}

// ✅ 좋은 예
const containerVariants = {
  hidden: { opacity: 0 }
};

export default function Good() {
  const [count, setCount] = useState(0);
  // variants는 컴포넌트 밖에서 정의
  return <motion.div variants={containerVariants} />;
}

2. will-change 활용

<motion.div
  animate={{ x: 100 }}
  style={{ willChange: 'transform' }}
/>

3. reduce-motion 존중

import { useReducedMotion } from 'framer-motion';

export default function AccessibleAnimation() {
  const prefersReducedMotion = useReducedMotion();

  return (
    <motion.div
      animate={{ x: prefersReducedMotion ? 0 : 100 }}
      transition={{ duration: prefersReducedMotion ? 0 : 0.5 }}
    />
  );
}

결론

Framer Motion은 React에서 애니메이션을 만드는 가장 강력한 라이브러리

CSS 애니메이션보다 문법이 간단하고, 상태와 연동하기 쉬움 제스처 애니메이션, 드래그, 스크롤 기반 애니메이션 등 복잡한 애니메이션도 가능함

처음엔 initial, animate, transition만 해도 충분하지만 나중에 variants, whileHover, drag 등으로 점점 확장할 수 있음

가챠픽이나 로또 시뮬레이션 같은 프로젝트에서도 Framer Motion으로 부드러운 애니메이션을 만들 수 있음!

앞으로 React 프로젝트에서 애니메이션이 필요하면 Framer Motion을 적극 활용해봐야겠음! 🎬

반응형