본문 바로가기

React

React 앱에서 애니메이션을 위해 Framer Motion 활용하기

Framer Motion은 React에서 선언적이고 직관적인 애니메이션을 제공하는 라이브러리입니다. 컴포넌트 상태와 상호작용을 자연스럽게 연결하고, 레이아웃 변경·마운트/언마운트 전환까지 손쉽게 다룰 수 있습니다. 아래 예제와 패턴을 기준으로 실무에 바로 적용해보세요.

1. 설치 및 기본 세팅

프로젝트에 Framer Motion을 설치합니다.

// npm
a npm i framer-motion

// yarn
yarn add framer-motion

일반적인 React 환경에서 추가 설정 없이 바로 사용할 수 있습니다. Next.js 등 SSR 환경에서도 기본적으로 호환되며, 무거운 애니메이션 컴포넌트는 동적 로딩을 고려합니다.

2. 가장 기본: motion 컴포넌트

motion.* 요소에 initial, animate, transition을 선언해 자연스러운 입장 애니메이션을 구현합니다.

import { motion } from "framer-motion";

export function IntroBox() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: "easeOut" }}
      style={{ background: "#f5f5f5", padding: 16, borderRadius: 8 }}
    >
      안녕하세요! Framer Motion입니다.
    </motion.div>
  );
}

3. Variants로 상태 기반 모션 설계

Variants는 여러 상태를 선언적으로 관리해 컴포넌트와 자식들의 애니메이션을 일관성 있게 제어합니다.

import { motion } from "framer-motion";

const listVariants = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: { staggerChildren: 0.1, delayChildren: 0.1 }
  }
};

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

export function StaggerList({ items }) {
  return (
    <motion.ul variants={listVariants} initial="hidden" animate="show">
      {items.map((text) => (
        <motion.li key={text} variants={itemVariants}>{text}</motion.li>
      ))}
    </motion.ul>
  );
}

4. 마운트/언마운트 전환: AnimatePresence

조건부 렌더링 시 컴포넌트가 사라질 때도 exit 애니메이션을 적용합니다. 모달, 토스트 등에 유용합니다.

import { motion, AnimatePresence } from "framer-motion";

export function Modal({ open, onClose }) {
  return (
    <AnimatePresence>
      {open && (
        <motion.div
          className="backdrop"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)" }}
          onClick={onClose}
        >
          <motion.div
            role="dialog"
            aria-modal="true"
            initial={{ scale: 0.95, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            exit={{ scale: 0.95, opacity: 0 }}
            transition={{ type: "spring", stiffness: 260, damping: 20 }}
            style={{ width: 420, margin: "10% auto", background: "#fff", padding: 24, borderRadius: 12 }}
            onClick={(e) => e.stopPropagation()}
          >
            <h2>설정</h2>
            <button onClick={onClose}>닫기</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

5. 제스처 및 드래그 인터랙션

호버/탭, 드래그 같은 사용자의 입력에 실시간 반응하는 모션을 손쉽게 구현합니다.

import { motion } from "framer-motion";

export function ActionButton({ children }) {
  return (
    <motion.button
      whileHover={{ scale: 1.03 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 30 }}
      style={{ padding: "10px 16px", borderRadius: 8 }}
    >
      {children}
    </motion.button>
  );
}

export function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -60, right: 60, top: -40, bottom: 40 }}
      style={{ width: 160, height: 100, background: "#e0f7fa", borderRadius: 12 }}
    />
  );
}

6. 레이아웃 애니메이션과 공유 레이아웃

DOM 레이아웃 변경에도 부드럽게 전환되며, 두 화면 간 동일한 요소를 연결해 자연스러운 전환을 제공합니다.

import { motion, AnimatePresence } from "framer-motion";

export function ExpandingPanel({ open }) {
  return (
    <motion.div layout style={{ background: "#fafafa", borderRadius: 12, padding: 12 }}>
      <h4>레이아웃 전환</h4>
      <AnimatePresence>
        {open && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            상세 내용이 레이아웃 변화에 맞춰 자연스럽게 표시됩니다.
          </motion.div>
        )}
      </AnimatePresence>
    </motion.div>
  );
}

export function SharedAvatar({ showDetail, onBack }) {
  return (
    <AnimatePresence mode="wait">
      {showDetail ? (
        <motion.div key="detail" style={{ padding: 24 }}>
          <motion.img layoutId="avatar" src="/avatar.png" alt="사용자" style={{ width: 160, borderRadius: "50%" }} />
          <button onClick={onBack}>뒤로</button>
        </motion.div>
      ) : (
        <motion.div key="list" style={{ display: "flex", gap: 12, alignItems: "center" }}>
          <motion.img layoutId="avatar" src="/avatar.png" alt="사용자" style={{ width: 48, borderRadius: "50%" }} />
          <span>홍길동</span>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

7. 스크롤 기반 애니메이션

스크롤 진행도에 따라 스타일을 매핑해 진행 바, 섹션 페이드인 등 구현이 가능합니다.

import { motion, useScroll, useTransform } from "framer-motion";

export function ScrollProgressBar() {
  const { scrollYProgress } = useScroll();
  const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
  return (
    <motion.div style={{ position: "fixed", top: 0, left: 0, right: 0, height: 4, background: "#eee" }}>
      <motion.div style={{ height: "100%", background: "#0070f3", transformOrigin: "0% 50%", scaleX }} />
    </motion.div>
  );
}

8. 접근성: 사용자 설정 존중

사용자의 모션 감소 설정을 감지해 대체 애니메이션을 제공하면 접근성이 향상됩니다.

import { motion, useReducedMotion } from "framer-motion";

export function AccessibleFadeIn({ children }) {
  const shouldReduce = useReducedMotion();
  const initial = shouldReduce ? { opacity: 0 } : { opacity: 0, y: 12 };
  const animate = shouldReduce ? { opacity: 1 } : { opacity: 1, y: 0 };
  return (
    <motion.div initial={initial} animate={animate} transition={{ duration: 0.3 }}>
      {children}
    </motion.div>
  );
}

9. 실무 성능 최적화 체크리스트

애니메이션은 UI 반응성을 저하시킬 수 있으므로 다음을 확인합니다.

1) transform/opacity 위주로 애니메이션합니다. width/height/top/left 변경은 레이아웃 계산 비용이 큽니다.

2) Variants와 부모-자식 제어로 렌더 트리를 단순화합니다.

3) 대량 항목은 지연(stagger), 가상화, 조건부 애니메이션으로 초기 비용을 줄입니다.

4) 애니메이션 중 복잡한 상태 업데이트를 피하고, memo로 불필요한 재렌더를 줄입니다.

5) SSR/Next.js에서 무거운 섹션은 동적 import로 초기 페인트를 가볍게 합니다.

10. 자주 쓰는 패턴 요약

• 페이지 진입 페이드+슬라이드: initial/animate 조합으로 간단히 구현합니다.

• 리스트 항목 순차 등장: 부모/자식 variants와 staggerChildren 사용.

• 모달/토스트: AnimatePresence로 exit 처리.

• 공유 요소 전환: layoutId로 두 화면 사이의 동일 객체 연결.

• 스크롤 트리거: useScroll+useTransform으로 진행도 기반 애니메이션.

11. 도입 전략

컴포넌트 설계 단계에서 인터랙션 목적을 먼저 정의하고, 성능·접근성 기준을 체크한 뒤 프레임 단위로 모션을 설계합니다. 작은 단위에서 시작해 공통 variants와 전환 규칙을 라이브러리화하면 유지보수가 쉬워집니다.

Framer Motion은 선언적 설계, 강력한 레이아웃 전환, 제스처 반응성까지 모두 갖춘 실무 친화적인 도구입니다. 위 패턴을 기준으로 프로젝트에 점진적으로 도입해보세요.