본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · MOTIONDIVISION/MOTION · "Framer Motion"이 이름을 벗고 "Motion"이 되다

motiondivision/motion 딥다이브
— 브라우저 네이티브 엔진과 JS 엔진을 섞어 쓰는 하이브리드 애니메이션 라이브러리

라이브러리

빠른 판단

무엇
목표 상태를 선언하면 스프링·레이아웃 전환을 실행하는 JS·React 애니메이션 라이브러리
이럴 때
웹 요소의 등장·hover·배치 변화에 인터랙션 애니메이션을 붙이고 선언형으로 상태를 관리할 때
Motion(구 Framer Motion)은 JavaScript·React를 위한 오픈소스 애니메이션 라이브러리다. 저장소 이름은 여전히 motiondivision/motion이지만 npm 패키지는 motion이고, 예전에 쓰던 framer-motion 패키지는 지금도 배포되며 내부적으로 Motion의 진짜 엔진이다.

가장 큰 특징은 "하이브리드 엔진"이다 — 가능하면 브라우저의 WAAPI(Element.animate())로 GPU 가속 애니메이션을 돌리고, 안 되는 경우에만 자바스크립트로 직접 매 프레임 값을 계산한다. 그 위에 animate()·scroll()·inView() 같은 순수 함수형 API와, React 전용의 motion.div·AnimatePresence·레이아웃(FLIP) 애니메이션·MotionValue가 얹혀 있다.

(저장소 motiondivision/motion · 만든 사람 Matt Perry · 라이선스 MIT(Copyright Motion B.V.) · 현재 버전 12.43.0(2026-07-27 릴리스) · Yarn 워크스페이스 + Turborepo + Lerna 모노레포, 핵심 패키지 4개(motion-utils·motion-dom·framer-motion·motion) · 전체 추적 파일 3,165개, framer-motion 소스 306개 + motion-dom 소스 318개 · React 18/19 피어(선택 사항) · 최신 커밋 2026-07-28 a4e4b3a)
목차
  1. 한 줄 정체 — Motion이 뭐 하는 물건인가
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조 — 4개 패키지의 관계
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1한 줄 정체 — Motion이 뭐 하는 물건인가

"엘리먼트를 예쁘게 움직이는 코드를, 브라우저가 제일 잘하는 방식으로 대신 돌려주는 라이브러리"

Motion은 "무엇을 어떻게 움직일지"를 선언하면, 실제 실행은 라이브러리가 알아서 최적의 방식으로 처리해주는 애니메이션 라이브러리다. 개발자는 animate(".box", { x: 100 })나 <motion.div animate={{ x: 100 }} />처럼 "결과 상태"만 적으면 되고, 그 값을 60fps(또는 120fps)로 부드럽게 그려내는 계산·프레임 스케줄링·하드웨어 가속 여부 판단은 전부 라이브러리 내부에서 처리된다.

React 생태계에서는 <motion.div> 같은 컴포넌트로 가장 널리 알려져 있지만, 실제로는 React 없이도 동작하는 순수 JS 애니메이션 엔진(motion-dom) 위에 React 전용 레이어(framer-motion)가 얹힌 구조다. 그래서 바닐라 JS 페이지, Next.js 서버 컴포넌트, Vue(별도 패키지 motion-v) 등 여러 환경에서 같은 엔진을 재사용한다.

한 줄 비유

"수동 기어(JS 계산)"와 "오토 기어(브라우저 네이티브)"를 상황 따라 자동으로 바꿔 타는 차

애니메이션을 만드는 방법은 크게 두 가지다. ① 자바스크립트가 매 프레임 "지금 몇 픽셀이어야 하지?"를 직접 계산해서 그리는 방법(정교하지만 메인 스레드를 씀), ② 브라우저에게 "여기서 저기로 가줘"라고 맡기고 브라우저가 GPU로 그리는 방법(가볍지만 표현력에 제약).

Motion은 상황을 스스로 진단해서, 가능하면 ②(WAAPI)로 GPU에 맡기고, 콜백이 필요하거나 브라우저가 못 하는 경우에만 ①(JS 애니메이션)로 전환한다. 운전자는 어떤 기어인지 신경 쓸 필요가 없다.

동명이인 주의
Motion(motiondivision/motion) ≠ Remotion(remotion-dev/remotion)

이름이 비슷해 혼동하기 쉽지만 완전히 다른 프로젝트다. Motion은 웹 페이지 안에서 요소를 실시간으로 움직이는 브라우저 애니메이션 라이브러리이고, Remotion은 React 코드로 영상 파일(mp4 등)을 렌더링하는 완전히 다른 도구다. 검색 시 저장소 주소(motiondivision/motion vs remotion-dev/remotion)를 꼭 확인하자.

용어
WAAPI (Web Animations API)
브라우저에 내장된 애니메이션 실행 기능. element.animate(keyframes, options) 형태로 호출하면, 브라우저가 자바스크립트 엔진을 거치지 않고 렌더링 엔진(대개 GPU 합성 스레드)에서 직접 애니메이션을 그린다. CSS transition·@keyframes와 원리는 비슷하지만 JS에서 동적으로 제어할 수 있다.

2왜 지금 뜨는가

리브랜딩 완주 + "성능과 표현력 둘 다"라는 오래된 딜레마를 실제로 풀어낸 결과물

① "Framer Motion → Motion" 리브랜딩이 자리를 잡았다

2025년 1월 20일 배포된 v12.0.0부터 프로젝트명이 Framer Motion에서 Motion으로 바뀌었다. npm 패키지도 motion으로 새로 나왔고, 기존 framer-motion 패키지는 폐기되지 않고 내부 엔진으로 계속 유지된다. "React 전용 애니메이션 도구"에서 "JS·React·Vue를 아우르는 애니메이션 플랫폼"으로 정체성을 확장한 결과, 검색 트렌드와 채택률이 다시 올라가는 시기다.

② "가볍다 vs 표현력 있다"를 둘 다 만족시키는 하이브리드 엔진

전통적으로 애니메이션 라이브러리는 CSS/WAAPI(가볍고 빠르지만 제약 많음) 아니면 순수 JS(자유롭지만 무겁고 메인 스레드를 먹음) 중 하나를 택해야 했다. Motion은 값 하나하나에 대해 "이 값·이 상황에서 WAAPI로 가속할 수 있는가"를 런타임에 판단해 자동으로 전환한다. 개발자 입장에서는 API가 하나뿐인데 성능은 최적에 가깝다.

③ 레이아웃 애니메이션(FLIP)이라는 어려운 문제를 표준 API로 제공

layout prop 하나로 리스트 재정렬·그리드 변경 같은 레이아웃 변화까지 부드럽게 보간해주는 projection 시스템은 직접 구현하기 매우 어려운 기능이다. 이걸 선언적으로 제공한다는 점이 React 개발자들 사이에서 꾸준히 채택되는 핵심 이유다.

신호
활발한 유지보수 지표

모노레포에 CircleCI(빌드→Jest→Cypress→Playwright) 파이프라인이 구성돼 있고, CHANGELOG.md에는 2026-07-27 릴리스(backgroundColor 하드웨어 가속 추가)까지 기록돼 있다. 최신 커밋(2026-07-28)도 실제 브라우저 동작 검증(WAAPI 가속 테스트) 관련 작업이다 — "실험적 토이"가 아니라 지금도 브라우저 호환성 최전선에서 다듬어지는 프로젝트임을 보여준다.

3무엇을 해결하나

"애니메이션을 손으로 계산하지 않게" + "성능과 개발자 경험을 동시에" + "레이아웃 변화까지 애니메이션으로"

Motion이 없다면 개발자는 애니메이션 하나를 만들 때마다 세 가지를 손수 결정해야 한다. ① 어떤 실행 방식(CSS/WAAPI vs JS)을 쓸지, ② 중간에 값이 바뀌면(인터럽트) 어떻게 자연스럽게 이어 붙일지, ③ 리스트 순서·크기가 바뀔 때 요소들의 위치 변화를 어떻게 보간할지. 이 세 가지는 각각 별도의 난이도 높은 문제다.

해결 ①

"성능이냐 표현력이냐" 양자택일을 없앰

WAAPI만 쓰면 빠르지만 onUpdate 콜백으로 매 프레임 값을 읽거나, 스프링 물리·인터럽트 같은 복잡한 동작을 표현하기 어렵다. JS만 쓰면 뭐든 되지만 메인 스레드를 계속 점유한다. Motion은 값·조건별로 자동으로 최적 경로를 골라 이 문제를 개발자 대신 풀어준다.

해결 ②

애니메이션 도중 목표가 바뀌는 문제(인터럽트)

버튼에 마우스를 올렸다 뗐다를 빠르게 반복하면 목표 값이 계속 바뀐다. MotionValue가 현재 값과 속도(velocity)를 계속 추적하고 있어서, 새 애니메이션이 시작될 때 이전 속도를 이어받아 자연스럽게 방향을 트는 스프링 애니메이션을 만들 수 있다. 값을 처음부터 다시 계산하며 뚝뚝 끊기는 흔한 버그를 근본적으로 방지한다.

해결 ③

레이아웃 변화(리스트 재정렬·필터링·아코디언)를 애니메이션으로

layout prop 하나로, DOM 위치·크기가 바뀌기 전과 후를 비교(FLIP)해 실제로는 순간이동한 변화를 시각적으로는 부드럽게 이동한 것처럼 보여준다. 이걸 직접 구현하려면 getBoundingClientRect를 앞뒤로 재서 transform을 역산하는 코드를 손으로 짜야 한다.

4전체 구조 — 4개 패키지의 관계

motion-utils(기반) → motion-dom(엔진) → framer-motion(React 구현) → motion(공개 진입점)

이 저장소는 Yarn 워크스페이스 + Turborepo + Lerna로 관리되는 모노레포다(packages/*). 핵심은 계층이 명확히 분리돼 있고, 아래로 갈수록(React에서 멀어질수록) 의존성이 적어진다는 점이다.

┌─────────────────────────────────────────────────────────┐ │ MOTION 모노레포 — 패키지 의존 흐름 │ └─────────────────────────────────────────────────────────┘ motion-utils (버전 12.39.0, 의존성 0개) │ 이징 함수 · 수학 유틸 · 경고/에러 헬퍼 ▼ motion-dom (버전 12.43.0, motion-utils 의존) │ ★ 진짜 애니메이션 엔진 — 프레임워크 무관 │ animate() · scroll() · inView() · MotionValue │ WAAPI/JS 하이브리드 판단 · spring · projection(FLIP) ▼ framer-motion (버전 12.43.0, motion-dom + motion-utils 의존) │ ★ React 구현체 — 실제 로직 대부분이 여기 있음 │ motion.div · AnimatePresence · LazyMotion · Reorder │ useMotionValue · useSpring · useScroll · useTransform ▼ motion (버전 12.43.0, 공개 npm 진입점) "motion/react.ts" → import * as fm from "framer-motion" 자체 로직 없음. framer-motion을 더 깔끔한 경로로 재노출

즉 npm install motion으로 설치해 쓰는 코드 대부분은 실제로는 framer-motion 패키지 안의 코드를 실행하는 것이다. motion 패키지는 motion/react·motion/mini처럼 더 명확한 하위 경로(subpath)를 제공하는 얇은 래퍼일 뿐이다. 이 저장소 밖에는 Vue용 motion-v 패키지도 있는데, 그건 별도 저장소로 관리되며 이 모노레포에는 포함돼 있지 않다.

용어
모노레포(monorepo) · Yarn 워크스페이스
여러 개의 관련 패키지를 하나의 git 저장소 안에 두고, 패키지 매니저가 이들을 packages/* 같은 규칙으로 묶어 함께 버전 관리·빌드하게 하는 방식. Motion은 4개 npm 패키지를 이 방식으로 한 저장소에서 관리한다.
용어
Turborepo · Lerna
모노레포 안의 여러 패키지를 효율적으로 빌드·테스트·배포하기 위한 도구들. Turborepo는 변경된 패키지만 골라 빌드(캐싱)하고, Lerna는 여러 패키지의 버전을 한 번에 올리고 npm에 배포하는 역할을 한다.
진짜 코드는 어디에
"motion" 패키지 = 빈 껍데기

packages/motion/src/react.ts는 실제로 이렇게 되어 있다:

import * as fm from "framer-motion"

export const motion: typeof fm.motion = fm.motion
export const m: typeof fm.m = fm.m

export * from "framer-motion"

버그를 고치거나 기능을 파악하려고 packages/motion/ 안을 뒤지면 시간을 낭비한다. 실제 구현은 packages/framer-motion/src(React 레이어)와 packages/motion-dom/src(엔진)에 있다.

5핵심 설계·동작 원리

가장 중요한 섹션 — 하이브리드 엔진 판단 로직 · MotionValue · 스프링 물리 · projection(FLIP) · 번들 최적화

① 하이브리드 엔진 — "이 값은 WAAPI로 가속해도 되는가?"를 매번 판단

Motion의 핵심은 motion-dom/src/animation/waapi/supports/waapi.ts의 supportsBrowserAnimation() 함수다. 애니메이션을 시작하기 직전마다 아래 조건을 모두 검사해서, 하나라도 걸리면 JS 애니메이션으로 자동 전환한다.

// motion-dom/src/animation/waapi/supports/waapi.ts (요약)
const acceleratedValues = new Set([
  "opacity", "clipPath", "filter", "transform", "backgroundColor"
])

function supportsBrowserAnimation(options) {
  const subject = options.motionValue?.owner?.current
  // ① HTML/SVG 엘리먼트가 아니면(다른 창·팝업 등) 가속 안 함
  if (!(subject instanceof HTMLElement) &&
      !(subject instanceof SVGElement)) return false

  return supportsWaapi() &&
    // ② 가속 가능한 값 목록에 있거나, oklch 등 브라우저 전용 색상 포맷
    (acceleratedValues.has(name) || isBrowserOnlyColor) &&
    // ③ transform인데 커스텀 템플릿이 있으면 WAAPI 불가
    (name !== "transform" || !transformTemplate) &&
    // ④ onUpdate로 매 프레임 값을 읽어야 하면 WAAPI 불가(읽을 방법이 없음)
    !onUpdate &&
    !repeatDelay &&
    repeatType !== "mirror" &&
    damping !== 0 &&
    type !== "inertia"
}

즉 gpu 가속 대상은 opacity·clipPath·filter·transform·backgroundColor 5종 뿐이고, 그마저도 조건이 하나라도 안 맞으면(예: onUpdate를 넘겼다면) 자동으로 JS 경로로 내려간다. WAAPI 경로는 NativeAnimation, JS 경로는 JSAnimation이라는 각각의 클래스가 맡되, 개발자가 부르는 animate() API는 완전히 동일하다.

비유

택배 배송 방법을 매번 자동으로 고르는 물류 시스템이라 생각하면 쉽다. 가벼운 소포(opacity·transform 같은 값)이고 특별 요청(콜백으로 실시간 위치 추적 등)이 없으면 드론 배송(WAAPI, GPU)으로 빠르게 처리한다. 무겁거나(색상 특수 포맷), 실시간 추적이 필요하거나, 특수 조건이 붙으면 사람이 직접 트럭으로(JS 애니메이션) 배달한다. 받는 사람(개발자)은 어떤 방법으로 왔는지 신경 쓸 필요가 없다.

용어
컴포지터 속성 (compositor property)
브라우저가 레이아웃·페인트를 다시 계산하지 않고 GPU의 합성(compositing) 단계에서만 값을 바꿀 수 있는 CSS 속성. opacity·transform 등이 대표적이며, WAAPI 하드웨어 가속의 대상이 된다. 반대로 width·height는 레이아웃을 다시 계산해야 해서 가속 대상이 아니다.

② animate()·scroll()·inView() — React 없이도 쓰는 독립 함수

packages/framer-motion/src/dom.ts(motion/react가 아닌 motion 기본 진입점이 참조하는 파일)는 다음처럼 세 함수를 노출한다.

// packages/framer-motion/src/dom.ts
export * from "motion-dom"
export * from "motion-utils"
export { animate, createScopedAnimate } from "./animation/animate"
export { scroll } from "./render/dom/scroll"
export { inView } from "./render/dom/viewport"

이 세 함수는 React 컴포넌트 트리와 무관하게 동작하기 때문에, jQuery 스타일의 바닐라 JS 페이지나 서버 렌더링 위주 사이트에서도 애니메이션 로직만 가볍게 가져다 쓸 수 있다.

③ React 레이어 — motion.div · AnimatePresence · MotionValue

React를 쓸 때는 <motion.div animate={{ x: 100 }} />처럼 태그 이름 앞에 motion.을 붙인다. 내부적으로 motion은 Proxy 객체라서 motion.div·motion.span·motion.custom 등 모든 HTML 태그에 반응한다. 언마운트되는 컴포넌트를 즉시 없애지 않고 exit 애니메이션이 끝날 때까지 붙잡아 두는 것이 AnimatePresence의 역할이다.

값 하나하나는 MotionValue 인스턴스로 관리된다(motion-dom/src/value/index.ts). 이 클래스는 단순히 "현재 값"만 들고 있는 게 아니라, 최근 프레임들 사이의 변화량으로 속도(velocity)를 계산해 둔다. 그래서 애니메이션이 중간에 새 목표로 바뀌어도(인터럽트) 이전 속도를 이어받아 튀지 않고 자연스럽게 방향을 전환할 수 있다.

용어
MotionValue
애니메이션 중인 값 하나(예: x 좌표, opacity)를 감싸는 객체. 현재 값·속도·구독자(subscriber) 목록을 들고 있어서, React 리렌더링 없이도 값이 바뀔 때마다 DOM을 직접 갱신할 수 있다. useMotionValue()·useSpring()·useTransform() 훅이 모두 이 객체를 반환한다.

④ 스프링 물리 — 물리 상수 아니면 지속시간·통통 튐(bounce) 중 선택

motion-dom/src/animation/generators/spring.ts의 기본값은 다음과 같다.

const springDefaults = {
  stiffness: 100, damping: 10, mass: 1.0, velocity: 0.0, // 물리 기반
  duration: 800, bounce: 0.3, visualDuration: 0.3,       // 지속시간 기반(대안)
}

stiffness(뻣뻣함)·damping(감쇠, 저항)·mass(질량) 세 물리 상수로 스프링을 정의할 수도 있고, 물리를 몰라도 되게 duration + bounce(몇 초 동안, 얼마나 튕길지)로 지정할 수도 있다. 내부적으로 후자를 전자로 역산하는 createGeneratorEasing 유틸이 존재한다 — "정확한 스프링 값은 모르지만 어떤 느낌인지는 안다"는 개발자를 위한 배려다.

⑤ 레이아웃 애니메이션(projection) — FLIP을 시스템으로 만든 것

layout prop을 쓰면 HTMLProjectionNode(motion-dom/src/projection/node)가 리렌더링 전후의 getBoundingClientRect() 위치·크기를 기록해 두고, 그 차이만큼을 transform으로 역산해 "실제로는 순간 이동했지만 시각적으로는 부드럽게 움직인 것처럼" 보이게 만든다. 여러 요소가 중첩돼 있으면(부모가 커지고 자식이 그 안에서 재배치되는 경우 등) 이 계산이 트리 구조로 전파돼야 하는데, projection 시스템이 이 전파를 자동으로 처리한다.

용어
FLIP (First, Last, Invert, Play)
레이아웃 변화를 애니메이션처럼 보이게 만드는 기법. 바뀌기 전 위치(First)와 바뀐 후 위치(Last)를 재고, 그 차이를 즉시 되돌리는 transform을 건 뒤(Invert), 그 transform을 0으로 애니메이션(Play)한다. 브라우저가 실제로 레이아웃을 다시 계산하지 않고도 이동한 것처럼 보이게 하는 고전적 기법이다.

⑥ 번들 크기 최적화 — m 컴포넌트 + LazyMotion

motion(전체 motion.*)을 그대로 쓰면 제스처·드래그·레이아웃 기능이 모두 포함돼 번들이 커진다. 대신 기능이 빠진 가벼운 버전인 m 컴포넌트를 쓰고, 필요한 기능 묶음(domAnimation/domMax)을 LazyMotion으로 동기 또는 비동기 code-split으로 불러올 수 있다.

// 동기 로딩
import { LazyMotion, m, domAnimation } from "motion/react"
function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div animate={{ scale: 2 }} />
    </LazyMotion>
  )
}
// 비동기 로딩 — 초기 번들에서 애니메이션 로직을 완전히 뺀다
<LazyMotion features={() => import("./domMax-features")}>

package.json의 bundlesize 검사 기준으로 실제 크기 상한이 명시돼 있다: motion 전체 34.9kB · m 6kB · domAnimation 기능 묶음 17.85kB · domMax(레이아웃·드래그 포함) 29.8kB · 독립 함수 animate()만 19.1kB. 이 수치가 CI에서 자동 검증되므로, 실수로 번들이 커지는 회귀가 배포되기 어렵다.

실무 팁
언제 m + LazyMotion을 쓰나

랜딩 페이지처럼 초기 로딩 속도가 중요하고 애니메이션은 단순한 hover/tap 정도인 곳에서는 m + domAnimation(제스처만, 레이아웃 제외)만으로 충분하다. 관리자 대시보드처럼 번들 크기보다 개발 속도가 중요하면 그냥 motion.div를 써도 무방하다.

6기술 스택·의존성

런타임 의존성은 사실상 0개에 가깝다 — 버전 진실원은 각 패키지의 package.json
패키지버전런타임 의존성역할
motion-utils12.39.0없음이징 함수·수학 헬퍼·경고/에러 유틸(기반 계층)
motion-dom12.43.0motion-utils애니메이션 엔진 본체(프레임워크 무관)
framer-motion12.43.0motion-dom, motion-utils, tslibReact 구현(대부분의 실제 로직)
motion12.43.0framer-motion 재노출공개 npm 진입점(motion/react, motion/mini 등)

버전의 진실원(source of truth)은 각 패키지의 package.json이다. 저장소 루트 package.json은 "name": "motion-one", "version": "0.0.0"로, 실제 배포 버전과 무관한 워크스페이스 메타데이터일 뿐이니 착각하지 말아야 한다. framer-motion의 peerDependencies는 react ^18.0.0 || ^19.0.0이면서 전부 optional: true다 — React 없이 animate()·scroll()만 쓰는 순수 JS 사용도 정식으로 지원된다는 뜻이다.

개발용 도구 체인

영역도구
모노레포 관리Yarn Workspaces + Turborepo(빌드 캐싱) + Lerna(버전·배포)
빌드TypeScript(tsc) + Rollup(ESM/CJS 동시 산출) + 자체 check-bundle.js(번들 크기 검증)
단위 테스트Jest(JSDOM 환경 — WAAPI 미지원, JS 폴백 경로만 검증 가능)
E2E(실브라우저)Cypress(dev/react·dev/react-19 앱 대상, WAAPI 가속 여부까지 검증) + Playwright(바닐라 HTML/JS,tests/ 디렉토리)
CICircleCI(Node 20.11.1 브라우저 이미지) — 빌드 → Jest → Cypress → Playwright 순서로 게이트
린트ESLint(react-hooks·react-compiler·redos-detector 플러그인 포함)
용어
피어 디펜던시(peer dependency)
라이브러리가 "이 버전의 패키지가 호스트 프로젝트에 있어야 한다"고 요구하지만, 직접 설치하지는 않는 의존성 방식. React를 피어로 두면 여러 라이브러리가 자기만의 React를 중복 설치하는 문제(버전 충돌·번들 중복)를 막을 수 있다.

7설치·요구사항

npm install motion 하나로 끝 — React는 선택, Vue는 별도 패키지
용도설치 / 실행
React / JS(권장)npm install motion → import { motion } from "motion/react" 또는 import { animate } from "motion"
Vuenpm install motion-v(이 저장소가 아닌 별도 프로젝트)
레거시 호환기존 framer-motion 패키지도 계속 배포되며 그대로 동작(단, 신규 프로젝트는 motion 권장)
레포 직접 빌드(기여자)Node 22(.nvmrc) + make bootstrap → yarn build(반드시 루트에서 실행)
개발 서버dev/react(React 18, 9990포트) · dev/react-19(React 19, 9991포트) · dev/next(3000포트) · dev/html(바닐라, 8000포트)
주의
JSDOM(Jest)로는 WAAPI 경로를 검증할 수 없다

레포의 CLAUDE.md에 명시된 사실이다: JSDOM은 Element.animate()(WAAPI)를 지원하지 않는다. 그래서 Jest 단위 테스트는 항상 JS 폴백 경로만 통과 여부를 확인하며, 실제 브라우저에서 벌어지는 하드웨어 가속 동작은 Cypress(실브라우저)로만 검증할 수 있다. 이 라이브러리를 포크해 테스트를 작성할 계획이라면 반드시 기억해야 할 함정이다.

8실제 사용 흐름

가장 단순한 hover 애니메이션부터 레이아웃 애니메이션까지 3단계
단계 1 — 가장 단순한 선언형 애니메이션

마운트 시 페이드인 + 스프링 hover

import { motion } from "motion/react"

function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      whileHover={{ scale: 1.05 }}
      transition={{ type: "spring", stiffness: 300, damping: 20 }}
    >
      카드 내용
    </motion.div>
  )
}

initial은 시작 상태, animate는 도착 상태, whileHover는 마우스가 올라가 있는 동안의 상태다. transition에 type: "spring"을 주면 앞서 본 스프링 물리 엔진이 사용된다.

단계 2 — 등장/퇴장을 함께 다루는 AnimatePresence

모달·리스트 아이템 exit 애니메이션

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

function Modal({ isOpen }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      )}
    </AnimatePresence>
  )
}

React는 원래 조건부 렌더링에서 컴포넌트를 즉시 DOM에서 제거하지만, AnimatePresence가 이를 가로채 exit 애니메이션이 끝날 때까지 실제 제거를 미룬다.

단계 3 — React 없이 순수 JS로 스크롤 연동

vanilla JS 페이지에서 스크롤 진행률에 따라 움직이기

import { animate, scroll } from "motion"

scroll(
  animate("#progress-bar", { scaleX: [0, 1] }),
  { target: document.querySelector("#article") }
)

React 프로젝트가 아니어도, <script type="module"> 안에서 CDN으로 motion을 불러와 이 코드 그대로 쓸 수 있다. scroll()은 스크롤 위치를 애니메이션의 진행률(타임라인)로 바꿔주는 역할을 한다.

9함정·오해 방지

이름 혼동 · 패키지 착각 · 가속 조건 오해 · Motion+ 유료층과의 경계
함정 1
"모든 애니메이션이 GPU 가속된다"는 착각

가속 대상은 opacity·clipPath·filter·transform·backgroundColor 5종뿐이다. width·height·top·left 같은 레이아웃 속성은 애초에 하드웨어 가속 후보가 아니며 항상 JS 경로로 계산된다. 또한 가속 대상이라도 onUpdate 콜백을 넘기거나 repeatType: "mirror"를 쓰면 자동으로 JS 경로로 내려간다 — "느려졌다"고 느껴지면 이 조건들부터 의심하자.

함정 2
"framer-motion" 패키지는 죽지 않았다 — 오히려 진짜 엔진이다

튜토리얼·블로그 상당수가 여전히 import { motion } from "framer-motion"을 쓴다. 이건 틀린 게 아니라 구버전 진입점을 쓰는 것이다. 신규 프로젝트는 motion/react를 권장하지만, 두 코드가 결국 같은 framer-motion 소스(packages/framer-motion/src)를 실행한다는 점을 알아두면 "둘 중 뭐가 진짜냐"는 혼란이 사라진다.

함정 3
Jest 테스트가 통과했다고 브라우저에서도 맞는 게 아니다

JSDOM은 WAAPI를 지원하지 않는다. Jest로 통과한 로직이라도 실제 크롬·사파리에서 Element.animate() 경로를 타면 다르게 동작할 수 있다. 레포 자체 가이드(CLAUDE.md)도 "버그가 opacity·transform·레이아웃·스크롤과 관련되면 유닛 테스트를 건너뛰고 Cypress부터 쓰라"고 못 박아 둔 이유다.

함정 4
Motion+는 라이브러리 자체가 아니라 "학습 콘텐츠 멤버십"이다

Motion+는 일회성 결제로 330개 이상 예제·100개 이상 튜토리얼·프리미엄 API(Cursor·Ticker)·전환 편집기·전용 디스코드 등을 제공하는 유료 멤버십이다. 핵심 오픈소스 라이브러리(motion npm 패키지)는 100% MIT 무료이며 아무 기능도 잠겨 있지 않다. "Motion을 쓰려면 결제해야 하나?"는 흔한 오해지만 사실이 아니다.

용어
tree-shaking(트리 쉐이킹)
번들러가 실제로 사용되지 않는 코드를 최종 결과물에서 자동으로 제거하는 최적화. Motion의 각 패키지는 "sideEffects": false로 표시돼 있어, import { animate } from "motion"처럼 필요한 함수만 가져오면 그 외 코드(예: React 컴포넌트 로직)는 번들에서 빠진다.

10대안 비교

GSAP · react-spring · anime.js · 순수 CSS 애니메이션과 무엇이 다른가
도구강점Motion과의 차이
GSAP가장 강력한 타임라인 제어, SplitText·MorphSVG 등 고급 플러그인, 프레임워크 무관 역사가 길다React 전용 레이아웃(FLIP) 시스템이 없고, 고급 플러그인은 유료. Motion은 하이브리드 엔진과 React 통합이 기본 제공
react-spring스프링 물리 기반 훅 API, React 철학과 잘 맞음WAAPI 하드웨어 가속이 없고 항상 JS(rAF)로 계산. 레이아웃 애니메이션·제스처 통합도 Motion만큼 완성도 있지 않음
anime.js매우 가볍고 API가 단순, 순수 JS/타임라인 애니메이션에 강함React 바인딩·레이아웃 애니메이션이 없음. "요소를 움직이는 것" 이상(제스처·AnimatePresence 등)은 직접 구현해야 함
CSS transition/@keyframes브라우저 네이티브라 가장 가볍고 빠름(추가 JS 0바이트)동적인 인터럽트·속도 이어받기·스크롤 연동·레이아웃 FLIP처럼 "상태를 계속 추적해야 하는" 애니메이션은 표현하기 어렵거나 불가능
선택 기준
언제 Motion을, 언제 다른 걸 쓸까

React 프로젝트에서 선언적으로 짧게 쓰면서도 레이아웃 애니메이션·제스처·스프링 인터럽트가 필요하면 Motion이 최적이다. 복잡한 타임라인 시퀀스·SVG 모핑 등 전문 모션 디자인 도구가 필요하면 GSAP(유료 플러그인 포함)을, 아주 단순한 hover/fade 몇 개뿐이라면 CSS만으로 충분할 수 있다.

11정리 — 누가 쓰면 좋은가

React 개발자, 성능과 표현력을 동시에 원하는 팀, 레이아웃 애니메이션이 필요한 UI

Motion은 "애니메이션을 잘 만들고 싶은데 브라우저 최적화까지 신경 쓸 여유는 없는" React/JS 개발자에게 가장 적합하다. 특히 다음 상황에서 진가를 발휘한다.

반대로 영화 같은 정교한 타임라인 시퀀스·SVG 모핑을 대규모로 다뤄야 하는 모션 디자인 전문 작업이라면 GSAP 계열이 더 나을 수 있고, hover 몇 개 색만 바꾸는 정도라면 굳이 라이브러리를 추가할 필요 없이 CSS만으로 충분하다. 이 저장소를 학습용으로 읽는다면, motion-dom/src/animation/waapi/supports/waapi.ts(하이브리드 판단 로직)와 motion-dom/src/projection(레이아웃 FLIP)부터 시작하는 것을 추천한다 — 이 둘이 Motion이 다른 라이브러리와 구별되는 핵심 설계다.

12직접 해볼 수 있는 실습 과제

스프링 조절에서 시작해 값의 연결, 레이아웃 변화, 실행 경로 검증까지

실습은 의존성과 Cypress 실행 환경이 준비된 개인 작업 사본에서 진행한다. 아래 명령은 별도 안내가 없으면 Motion 레포 루트에서 실행하며, study-로 시작하는 파일은 독자가 새로 만들 실습 파일이다.

yarn build
yarn workspace react-env dev

첫 명령은 패키지를 빌드하고, 두 번째 명령은 React 개발 앱을 띄운다. dev/react/src/App.tsx는 tests/ 안에서 이름 붙여 내보낸 App을 ?test=파일명으로 불러온다. 파일을 저장한 뒤 브라우저를 새로고침해 변경을 확인한다.

과제 1 · 입문

스프링 버튼의 움직임을 직접 조절하기

목표: 목표 위치를 선언하는 코드와 움직임의 성격을 정하는 설정을 구분한다.

  1. dev/react/src/tests/study-spring.tsx를 만들고 아래 코드를 넣는다.
  2. 개발 앱에서 ?test=study-spring을 열고 버튼을 눌러 위치를 바꾼다.
  3. stiffness를 고정한 채 damping을 10과 30으로 바꿔 목표를 지나치는 정도를 비교한다. 이어서 버튼에 키보드 포커스를 두고 Enter를 반복해 눌러 이동 중 목표를 바꿔 본다.
import { useState } from "react"
import { motion } from "motion/react"

export function App() {
    const [moved, setMoved] = useState(false)

    return (
        <motion.button
            type="button"
            animate={{ x: moved ? 160 : 0 }}
            whileHover={{ scale: 1.05 }}
            transition={{
                type: "spring",
                stiffness: 200,
                damping: 20
            }}
            onClick={() => setMoved(!moved)}
        >
            움직임 전환
        </motion.button>
    )
}

버튼의 목표는 animate가 정하고, 그 목표를 향해 움직이는 방식은 transition이 정한다. 물리 계산은 packages/motion-dom/src/animation/generators/spring.ts에서 찾아볼 수 있다.

완료 기준: 버튼이 두 목표 위치에 멈추며, 감쇠 설정에 따른 차이를 설명하고 이동 중 방향을 바꿔도 반응하는지 확인한다.

과제 2 · 중급

스크롤 값에 스프링과 값 변환 연결하기

목표: 스크롤 위치를 React 상태에 매번 복사하지 않고, MotionValue 사이의 연결로 화면을 갱신한다.

cp dev/react/src/tests/scroll-spring.tsx dev/react/src/tests/study-scroll.tsx
  1. ?test=study-scroll을 연다. 원본의 useScroll(), useSpring(scrollYProgress, ...), 진행 막대의 scaleX 연결을 찾는다.
  2. 막대의 scaleX에 연결된 값을 springProgress에서 scrollYProgress로 바꿔 즉시 반응하는 화면을 보고, 다시 되돌려 스크롤을 멈춘 뒤에도 잠깐 따라오는 차이를 확인한다.
  3. 기존 가져오기 목록에 useTransform을 추가하고 아래 값을 만든다. 진행 막대의 기존 스타일 객체에 opacity도 연결해 길이와 투명도가 함께 변하게 한다.
const opacity = useTransform(
    scrollYProgress,
    [0, 1],
    [0.3, 1]
)

packages/framer-motion/src/value/use-scroll.ts, use-spring.ts, use-transform.ts를 차례로 읽으면 입력값을 받아 다른 MotionValue를 만드는 흐름을 따라갈 수 있다.

완료 기준: 문서 맨 위와 맨 아래에서 진행 막대의 길이와 투명도가 달라지고, 스프링을 연결한 길이는 스크롤을 멈춘 뒤 목표에 도달한다. 값의 흐름을 스크롤 진행률 → 스프링 → 길이와 스크롤 진행률 → 값 변환 → 투명도로 설명한다.

과제 3 · 중급

퇴장하는 요소와 빈자리로 이동하는 요소 분리하기

목표: AnimatePresence가 제거 시점을 관리하고, layout이 남은 요소의 위치 변화를 처리한다는 차이를 확인한다.

cp dev/react/src/tests/animate-presence-pop-list.tsx dev/react/src/tests/study-exit-layout.tsx
  1. 복사한 파일에서 key="a", key="b", key="c"인 요소와 mode="popLayout"을 찾는다. 이 키들은 그대로 유지한다.
  2. 부모와 a·c 요소의 전환을 transition={{ duration: 0.4, ease: "linear" }}로 맞춘다. 원본의 ease: () => 1은 즉시 끝값을 반환하므로 교체하고, b의 exit 안에 있는 전환 시간도 0.4로 줄인다.
  3. ?test=study-exit-layout에서 상자를 눌러 b를 제거한다. b가 흐려지는 동안 c가 빈자리로 이동하는 시점을 관찰한다.
  4. mode를 "sync"로 바꿔 같은 동작을 반복한다. 퇴장 중인 b가 공간을 차지하는 동안과 실제로 제거된 뒤의 배치를 비교한다.

제거를 기다리는 로직은 packages/framer-motion/src/components/AnimatePresence/, 위치와 크기의 차이를 처리하는 로직은 packages/motion-dom/src/projection/에서 읽는다.

완료 기준: 두 모드 모두 퇴장 완료 뒤 b가 DOM에서 제거되고, c가 최종 빈자리에 놓인다. 두 모드에서 c의 이동 시작 시점이 어떻게 달라지는지 기록한다.

과제 4 · 심화

같은 투명도 애니메이션의 WAAPI 경로와 JS 경로 검증하기

목표: 화면이 움직인다는 관찰에 더해, 실행 중인 브라우저 애니메이션과 실제 표시 값을 함께 검사한다.

  1. packages/motion-dom/src/animation/waapi/supports/waapi.ts에서 onUpdate 조건을 읽는다. dev/react/src/examples/WAAPI-opacity.tsx도 참고한다.
  2. dev/react/src/tests/study-waapi.tsx를 만들고 아래 코드를 넣는다. ?test=study-waapi와 ?test=study-waapi&fallback은 같은 목표와 시간으로 실행하되, 두 번째 화면에만 콜백을 붙인다.
import { motion } from "motion/react"

export function App() {
    const fallback = new URLSearchParams(
        window.location.search
    ).has("fallback")

    return (
        <motion.button
            data-testid="probe"
            initial={{ opacity: 1 }}
            animate={{ opacity: 0.2 }}
            transition={{
                type: "tween",
                duration: 10,
                ease: "linear"
            }}
            onUpdate={fallback ? () => {} : undefined}
        >
            실행 경로 관찰
        </motion.button>
    )
}

다음으로 packages/framer-motion/cypress/integration/study-waapi.ts를 새로 만든다. 기존 waapi.ts의 cy.visit() 패턴을 따라 두 주소를 각각 열고, 시작 후 0.5초 지점에서 아래처럼 측정한다.

cy.get('[data-testid="probe"]')
    .wait(500)
    .then(([$element]: any) => {
        const opacity = Number(getComputedStyle($element).opacity)
        const running = $element.getAnimations().filter(
            (animation: Animation) =>
                animation.playState === "running"
        )

        expect(opacity).to.be.greaterThan(0.2)
        expect(opacity).to.be.lessThan(1)
        expect(running.length > 0).to.equal(!fallback)
    })

위 검사에서 fallback은 각 테스트 사례가 연 주소에 맞춰 정한 불리언 값이다. 중간 시점은 재시도하는 .should() 대신 .then()으로 한 번 측정하고, 종료 값은 충분한 대기 한도를 둔 .should()로 따로 확인한다.

앞서 띄운 React 개발 서버에 더해, 레포 루트의 다른 터미널에서 yarn workspace react-19-env dev를 실행한다. 테스트는 아래처럼 각 설정으로 실행한다.

cd packages/framer-motion
yarn cypress run --config-file cypress.json --spec cypress/integration/study-waapi.ts
yarn cypress run --config-file cypress.react-19.json --spec cypress/integration/study-waapi.ts

완료 기준: WAAPI를 지원하는 실제 브라우저에서 기본 사례는 실행 중인 브라우저 애니메이션이 있고, 콜백 사례는 없으며, 두 사례 모두 중간 투명도가 변하고 최종값 0.2에 도달한다. 두 React 환경에서 같은 검사가 통과해야 한다.

13관련 기술 심화 학습 로드맵

6주 동안 공개 API에서 엔진 내부로 내려간 뒤, 검증 가능한 화면으로 돌아오기

매주 읽은 파일을 요약하는 데서 끝내지 말고, 화면·관찰 기록·테스트 중 하나를 남긴다. 아래 경로는 모두 레포 루트 기준이며, 패키지의 역할과 값의 흐름을 먼저 이해한 뒤 세부 구현을 읽는 순서다.

주차와 개념이 레포에서 읽을 곳결과물
1주차
패키지 경계와 공개 진입점
루트 package.json의 워크스페이스 설정, packages/motion/src/react.ts, dev/react/src/App.tsx motion/react에서 구현 패키지로 이어지는 가져오기 지도와 과제 1의 실행 화면. 예제 파일이 주소의 쿼리로 선택되는 과정도 설명한다.
2주차
이징과 스프링
packages/motion-utils/src/easing/, packages/motion-dom/src/animation/generators/spring.ts, 같은 폴더의 __tests__/spring.test.ts 감쇠를 달리한 움직임 비교표. 정해진 진행 곡선과 물리 설정이 각각 무엇을 바꾸는지 구분하고, 과제 1의 설정 선택 이유를 적는다.
3주차
값의 구독과 프레임 예약
packages/motion-dom/src/value/index.ts, packages/motion-dom/src/frameloop/, packages/framer-motion/src/value/use-spring.ts 과제 2의 값 연결 그림과 실행 화면. 값 변경, 다음 프레임 작업 예약, 화면 반영이 어떤 관계인지 설명하고 읽기·계산·쓰기 단계를 구분한다.
4주차
브라우저 실행과 JS 실행
packages/motion-dom/src/animation/waapi/supports/waapi.ts, packages/motion-dom/src/animation/NativeAnimation.ts, packages/motion-dom/src/animation/JSAnimation.ts 실행 경로의 조건표와 과제 4의 중간 시점 측정 기록. 단순히 부드러워 보인다는 인상과 실제 실행 경로의 증거를 구분한다.
5주차
요소의 생명주기와 레이아웃 보간
packages/framer-motion/src/components/AnimatePresence/index.tsx, packages/motion-dom/src/projection/node/create-projection-node.ts, dev/react/src/tests/animate-presence-pop-list.tsx 과제 3의 모드 비교 기록. 상태에서 제거되는 순간, 퇴장이 끝나는 순간, DOM에서 없어지는 순간을 나눠 표시한다.
6주차
기능 선택과 회귀 검증
packages/framer-motion/src/components/LazyMotion/index.tsx, packages/framer-motion/src/render/dom/features-animation.ts, packages/framer-motion/src/render/dom/features-max.ts, packages/framer-motion/cypress/integration/ 한 화면을 motion 방식과 m·LazyMotion 방식으로 구성한 비교본, 빌드 결과 비교, 두 React 환경의 테스트 결과. 레이아웃 기능을 쓸 때 domMax가 필요한 이유도 적는다.

마지막에는 입력 변화 → MotionValue → 실행 경로 선택 → 화면 반영 → 완료와 정리를 자신의 실습 화면에 대입해 설명해 본다. 설명이 끊기는 지점의 파일을 다시 읽으면 다음 학습 범위를 좁힐 수 있다.

14핵심 키워드 사전

실습과 소스 읽기에 필요한 15개 용어를 한곳에 모으기
모노레포
관련된 여러 패키지를 한 저장소에서 관리하는 방식으로, 여러 작업실이 같은 건물에 있는 것과 비슷하다. 이 레포는 루트 package.json의 워크스페이스 설정으로 packages/*와 dev/*를 묶는다.
키프레임
움직임에서 꼭 지나갈 기준값으로, 여행의 출발지와 경유지에 해당한다. Motion에서는 opacity: [0, 1]처럼 값 배열로 지정할 수 있고 엔진이 그 사이의 값을 계산한다.
보간
두 기준값 사이의 값을 진행 정도에 맞춰 구하는 계산이다. 이 레포의 useTransform()은 입력 범위를 다른 출력 범위에 연결해, 스크롤 진행률을 투명도 같은 값으로 바꾸는 데 쓰인다.
이징
같은 이동 거리와 시간에서도 출발·중간·도착의 속도감을 바꾸는 진행 곡선이다. ease: "linear"는 일정한 비율로 진행하며, 관련 함수는 packages/motion-utils/src/easing/에 모여 있다.
스프링
목표에 고무줄로 연결된 물체처럼 움직이게 하는 계산 방식이다. Motion의 물리 설정에서 stiffness는 당기는 힘의 강도, damping은 흔들림을 줄이는 저항, mass는 질량을 나타낸다.
속도
현재 위치만이 아니라 어느 방향으로 얼마나 빠르게 변하는지를 나타내는 값이다. MotionValue.getVelocity()는 추적 가능한 값의 변화량을 초당 기준으로 계산하며, 물리 기반 애니메이션을 이어갈 때 참고할 수 있다.
MotionValue
값과 변경 알림을 함께 관리하는 상자로, React 상태를 매번 갱신하지 않고도 애니메이션 값을 연결할 수 있게 한다. 구현은 packages/motion-dom/src/value/index.ts에 있고, useMotionValue()·useSpring()·useTransform()으로 다룬다.
프레임 루프
다음 화면을 그릴 때 할 일을 반복해서 실행하는 작업 순서표다. packages/motion-dom/src/frameloop/는 읽기·갱신·렌더 같은 단계로 작업을 예약하고 처리한다.
WAAPI
자바스크립트가 매번 표시 값을 쓰는 대신 브라우저에 애니메이션 실행을 맡길 수 있는 표준 API다. 이 레포의 NativeAnimation은 브라우저 애니메이션의 재생·일시정지·종료 같은 제어를 감싼다.
하이브리드 엔진
같은 요청을 여러 실행 방식으로 처리하는 구조다. Motion은 supportsBrowserAnimation() 같은 조건 검사로 브라우저 실행 가능성을 판단하며, 조건에 맞지 않는 애니메이션은 JS 경로로 처리한다.
FLIP
바뀌기 전과 후의 배치를 비교하고, 그 차이를 역변환으로 되돌린 뒤 변환을 풀어 이동을 보여주는 기법이다. 이 레포의 projection 시스템은 위치·크기의 차이를 계산해 레이아웃 애니메이션을 처리한다.
레이아웃 애니메이션
요소의 실제 배치나 크기가 바뀔 때 그 전후를 부드럽게 이어 보여주는 기능이다. layout을 붙인 Motion 요소는 측정한 배치 차이를 이용하므로, 독자가 중간 좌표를 직접 나열할 필요가 없다.
AnimatePresence
화면에서 제거될 요소를 퇴장 애니메이션이 끝날 때까지 유지하는 관리자다. 자식의 고유한 key로 제거 여부를 추적하고 exit와 mode 설정에 따라 퇴장을 처리한다.
LazyMotion
가벼운 m 컴포넌트에 필요한 기능 묶음을 공급하는 장치다. domAnimation은 애니메이션과 제스처 기능을 담고, domMax는 여기에 드래그와 레이아웃 기능을 더하며 비동기 로딩에도 활용할 수 있다.
실제 브라우저 통합 테스트
클릭 같은 입력부터 실제 DOM과 화면 값의 변화까지 함께 확인하는 검사로, 이 레포에서는 Cypress와 Playwright를 사용한다. JSDOM의 단위 테스트만으로 확인하기 어려운 WAAPI 실행이나 레이아웃 동작을 검증하는 데 필요하다.