motiondivision/motion이지만 npm 패키지는 motion이고, 예전에 쓰던 framer-motion 패키지는 지금도 배포되며 내부적으로 Motion의 진짜 엔진이다.
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)
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) 등 여러 환경에서 같은 엔진을 재사용한다.
애니메이션을 만드는 방법은 크게 두 가지다. ① 자바스크립트가 매 프레임 "지금 몇 픽셀이어야 하지?"를 직접 계산해서 그리는 방법(정교하지만 메인 스레드를 씀), ② 브라우저에게 "여기서 저기로 가줘"라고 맡기고 브라우저가 GPU로 그리는 방법(가볍지만 표현력에 제약).
Motion은 상황을 스스로 진단해서, 가능하면 ②(WAAPI)로 GPU에 맡기고, 콜백이 필요하거나 브라우저가 못 하는 경우에만 ①(JS 애니메이션)로 전환한다. 운전자는 어떤 기어인지 신경 쓸 필요가 없다.
이름이 비슷해 혼동하기 쉽지만 완전히 다른 프로젝트다. Motion은 웹 페이지 안에서 요소를 실시간으로 움직이는 브라우저 애니메이션 라이브러리이고, Remotion은 React 코드로 영상 파일(mp4 등)을 렌더링하는 완전히 다른 도구다. 검색 시 저장소 주소(motiondivision/motion vs remotion-dev/remotion)를 꼭 확인하자.
element.animate(keyframes, options) 형태로 호출하면, 브라우저가 자바스크립트 엔진을 거치지 않고 렌더링 엔진(대개 GPU 합성 스레드)에서 직접 애니메이션을 그린다. CSS transition·@keyframes와 원리는 비슷하지만 JS에서 동적으로 제어할 수 있다.
2025년 1월 20일 배포된 v12.0.0부터 프로젝트명이 Framer Motion에서 Motion으로 바뀌었다. npm 패키지도 motion으로 새로 나왔고, 기존 framer-motion 패키지는 폐기되지 않고 내부 엔진으로 계속 유지된다. "React 전용 애니메이션 도구"에서 "JS·React·Vue를 아우르는 애니메이션 플랫폼"으로 정체성을 확장한 결과, 검색 트렌드와 채택률이 다시 올라가는 시기다.
전통적으로 애니메이션 라이브러리는 CSS/WAAPI(가볍고 빠르지만 제약 많음) 아니면 순수 JS(자유롭지만 무겁고 메인 스레드를 먹음) 중 하나를 택해야 했다. Motion은 값 하나하나에 대해 "이 값·이 상황에서 WAAPI로 가속할 수 있는가"를 런타임에 판단해 자동으로 전환한다. 개발자 입장에서는 API가 하나뿐인데 성능은 최적에 가깝다.
layout prop 하나로 리스트 재정렬·그리드 변경 같은 레이아웃 변화까지 부드럽게 보간해주는 projection 시스템은 직접 구현하기 매우 어려운 기능이다. 이걸 선언적으로 제공한다는 점이 React 개발자들 사이에서 꾸준히 채택되는 핵심 이유다.
모노레포에 CircleCI(빌드→Jest→Cypress→Playwright) 파이프라인이 구성돼 있고, CHANGELOG.md에는 2026-07-27 릴리스(backgroundColor 하드웨어 가속 추가)까지 기록돼 있다. 최신 커밋(2026-07-28)도 실제 브라우저 동작 검증(WAAPI 가속 테스트) 관련 작업이다 — "실험적 토이"가 아니라 지금도 브라우저 호환성 최전선에서 다듬어지는 프로젝트임을 보여준다.
Motion이 없다면 개발자는 애니메이션 하나를 만들 때마다 세 가지를 손수 결정해야 한다. ① 어떤 실행 방식(CSS/WAAPI vs JS)을 쓸지, ② 중간에 값이 바뀌면(인터럽트) 어떻게 자연스럽게 이어 붙일지, ③ 리스트 순서·크기가 바뀔 때 요소들의 위치 변화를 어떻게 보간할지. 이 세 가지는 각각 별도의 난이도 높은 문제다.
WAAPI만 쓰면 빠르지만 onUpdate 콜백으로 매 프레임 값을 읽거나, 스프링 물리·인터럽트 같은 복잡한 동작을 표현하기 어렵다. JS만 쓰면 뭐든 되지만 메인 스레드를 계속 점유한다. Motion은 값·조건별로 자동으로 최적 경로를 골라 이 문제를 개발자 대신 풀어준다.
버튼에 마우스를 올렸다 뗐다를 빠르게 반복하면 목표 값이 계속 바뀐다. MotionValue가 현재 값과 속도(velocity)를 계속 추적하고 있어서, 새 애니메이션이 시작될 때 이전 속도를 이어받아 자연스럽게 방향을 트는 스프링 애니메이션을 만들 수 있다. 값을 처음부터 다시 계산하며 뚝뚝 끊기는 흔한 버그를 근본적으로 방지한다.
layout prop 하나로, DOM 위치·크기가 바뀌기 전과 후를 비교(FLIP)해 실제로는 순간이동한 변화를 시각적으로는 부드럽게 이동한 것처럼 보여준다. 이걸 직접 구현하려면 getBoundingClientRect를 앞뒤로 재서 transform을 역산하는 코드를 손으로 짜야 한다.
이 저장소는 Yarn 워크스페이스 + Turborepo + Lerna로 관리되는 모노레포다(packages/*). 핵심은 계층이 명확히 분리돼 있고, 아래로 갈수록(React에서 멀어질수록) 의존성이 적어진다는 점이다.
즉 npm install motion으로 설치해 쓰는 코드 대부분은 실제로는 framer-motion 패키지 안의 코드를 실행하는 것이다. motion 패키지는 motion/react·motion/mini처럼 더 명확한 하위 경로(subpath)를 제공하는 얇은 래퍼일 뿐이다. 이 저장소 밖에는 Vue용 motion-v 패키지도 있는데, 그건 별도 저장소로 관리되며 이 모노레포에는 포함돼 있지 않다.
packages/* 같은 규칙으로 묶어 함께 버전 관리·빌드하게 하는 방식. Motion은 4개 npm 패키지를 이 방식으로 한 저장소에서 관리한다.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(엔진)에 있다.
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 애니메이션) 배달한다. 받는 사람(개발자)은 어떤 방법으로 왔는지 신경 쓸 필요가 없다.
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"
animate(selector, keyframes, options) — CSS 셀렉터나 DOM 엘리먼트를 직접 애니메이션. React가 필요 없다.scroll(callback, options) — 스크롤 진행률(0~1)을 콜백으로 흘려주는 스크롤 연동 애니메이션. 브라우저가 지원하면 ScrollTimeline(네이티브 스크롤 타임라인)까지 활용한다.inView(selector, callback) — IntersectionObserver를 감싼 뷰포트 진입 감지 함수.이 세 함수는 React 컴포넌트 트리와 무관하게 동작하기 때문에, jQuery 스타일의 바닐라 JS 페이지나 서버 렌더링 위주 사이트에서도 애니메이션 로직만 가볍게 가져다 쓸 수 있다.
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)를 계산해 둔다. 그래서 애니메이션이 중간에 새 목표로 바뀌어도(인터럽트) 이전 속도를 이어받아 튀지 않고 자연스럽게 방향을 전환할 수 있다.
useMotionValue()·useSpring()·useTransform() 훅이 모두 이 객체를 반환한다.
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 유틸이 존재한다 — "정확한 스프링 값은 모르지만 어떤 느낌인지는 안다"는 개발자를 위한 배려다.
layout prop을 쓰면 HTMLProjectionNode(motion-dom/src/projection/node)가 리렌더링 전후의 getBoundingClientRect() 위치·크기를 기록해 두고, 그 차이만큼을 transform으로 역산해 "실제로는 순간 이동했지만 시각적으로는 부드럽게 움직인 것처럼" 보이게 만든다. 여러 요소가 중첩돼 있으면(부모가 커지고 자식이 그 안에서 재배치되는 경우 등) 이 계산이 트리 구조로 전파돼야 하는데, projection 시스템이 이 전파를 자동으로 처리한다.
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에서 자동 검증되므로, 실수로 번들이 커지는 회귀가 배포되기 어렵다.
랜딩 페이지처럼 초기 로딩 속도가 중요하고 애니메이션은 단순한 hover/tap 정도인 곳에서는 m + domAnimation(제스처만, 레이아웃 제외)만으로 충분하다. 관리자 대시보드처럼 번들 크기보다 개발 속도가 중요하면 그냥 motion.div를 써도 무방하다.
| 패키지 | 버전 | 런타임 의존성 | 역할 |
|---|---|---|---|
| motion-utils | 12.39.0 | 없음 | 이징 함수·수학 헬퍼·경고/에러 유틸(기반 계층) |
| motion-dom | 12.43.0 | motion-utils | 애니메이션 엔진 본체(프레임워크 무관) |
| framer-motion | 12.43.0 | motion-dom, motion-utils, tslib | React 구현(대부분의 실제 로직) |
| motion | 12.43.0 | framer-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/ 디렉토리) |
| CI | CircleCI(Node 20.11.1 브라우저 이미지) — 빌드 → Jest → Cypress → Playwright 순서로 게이트 |
| 린트 | ESLint(react-hooks·react-compiler·redos-detector 플러그인 포함) |
| 용도 | 설치 / 실행 |
|---|---|
| React / JS(권장) | npm install motion → import { motion } from "motion/react" 또는 import { animate } from "motion" |
| Vue | npm 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포트) |
레포의 CLAUDE.md에 명시된 사실이다: JSDOM은 Element.animate()(WAAPI)를 지원하지 않는다. 그래서 Jest 단위 테스트는 항상 JS 폴백 경로만 통과 여부를 확인하며, 실제 브라우저에서 벌어지는 하드웨어 가속 동작은 Cypress(실브라우저)로만 검증할 수 있다. 이 라이브러리를 포크해 테스트를 작성할 계획이라면 반드시 기억해야 할 함정이다.
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"을 주면 앞서 본 스프링 물리 엔진이 사용된다.
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 애니메이션이 끝날 때까지 실제 제거를 미룬다.
import { animate, scroll } from "motion"
scroll(
animate("#progress-bar", { scaleX: [0, 1] }),
{ target: document.querySelector("#article") }
)
React 프로젝트가 아니어도, <script type="module"> 안에서 CDN으로 motion을 불러와 이 코드 그대로 쓸 수 있다. scroll()은 스크롤 위치를 애니메이션의 진행률(타임라인)로 바꿔주는 역할을 한다.
가속 대상은 opacity·clipPath·filter·transform·backgroundColor 5종뿐이다. width·height·top·left 같은 레이아웃 속성은 애초에 하드웨어 가속 후보가 아니며 항상 JS 경로로 계산된다. 또한 가속 대상이라도 onUpdate 콜백을 넘기거나 repeatType: "mirror"를 쓰면 자동으로 JS 경로로 내려간다 — "느려졌다"고 느껴지면 이 조건들부터 의심하자.
튜토리얼·블로그 상당수가 여전히 import { motion } from "framer-motion"을 쓴다. 이건 틀린 게 아니라 구버전 진입점을 쓰는 것이다. 신규 프로젝트는 motion/react를 권장하지만, 두 코드가 결국 같은 framer-motion 소스(packages/framer-motion/src)를 실행한다는 점을 알아두면 "둘 중 뭐가 진짜냐"는 혼란이 사라진다.
JSDOM은 WAAPI를 지원하지 않는다. Jest로 통과한 로직이라도 실제 크롬·사파리에서 Element.animate() 경로를 타면 다르게 동작할 수 있다. 레포 자체 가이드(CLAUDE.md)도 "버그가 opacity·transform·레이아웃·스크롤과 관련되면 유닛 테스트를 건너뛰고 Cypress부터 쓰라"고 못 박아 둔 이유다.
Motion+는 일회성 결제로 330개 이상 예제·100개 이상 튜토리얼·프리미엄 API(Cursor·Ticker)·전환 편집기·전용 디스코드 등을 제공하는 유료 멤버십이다. 핵심 오픈소스 라이브러리(motion npm 패키지)는 100% MIT 무료이며 아무 기능도 잠겨 있지 않다. "Motion을 쓰려면 결제해야 하나?"는 흔한 오해지만 사실이 아니다.
"sideEffects": false로 표시돼 있어, import { animate } from "motion"처럼 필요한 함수만 가져오면 그 외 코드(예: React 컴포넌트 로직)는 번들에서 빠진다.| 도구 | 강점 | 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처럼 "상태를 계속 추적해야 하는" 애니메이션은 표현하기 어렵거나 불가능 |
React 프로젝트에서 선언적으로 짧게 쓰면서도 레이아웃 애니메이션·제스처·스프링 인터럽트가 필요하면 Motion이 최적이다. 복잡한 타임라인 시퀀스·SVG 모핑 등 전문 모션 디자인 도구가 필요하면 GSAP(유료 플러그인 포함)을, 아주 단순한 hover/fade 몇 개뿐이라면 CSS만으로 충분할 수 있다.
Motion은 "애니메이션을 잘 만들고 싶은데 브라우저 최적화까지 신경 쓸 여유는 없는" React/JS 개발자에게 가장 적합하다. 특히 다음 상황에서 진가를 발휘한다.
m + LazyMotion으로 애니메이션 코드를 code-split하고 싶을 때animate()·scroll()·inView())을 순수 JS/서버 렌더링 페이지에 재사용하고 싶을 때
반대로 영화 같은 정교한 타임라인 시퀀스·SVG 모핑을 대규모로 다뤄야 하는 모션 디자인 전문 작업이라면 GSAP 계열이 더 나을 수 있고, hover 몇 개 색만 바꾸는 정도라면 굳이 라이브러리를 추가할 필요 없이 CSS만으로 충분하다. 이 저장소를 학습용으로 읽는다면, motion-dom/src/animation/waapi/supports/waapi.ts(하이브리드 판단 로직)와 motion-dom/src/projection(레이아웃 FLIP)부터 시작하는 것을 추천한다 — 이 둘이 Motion이 다른 라이브러리와 구별되는 핵심 설계다.