지난달 사이드 프로젝트 하나를 Next.js 16.2로 올리면서 viewTransition 플래그를 켜봤다. Framer Motion의 AnimatePresence와 motion.div로 라우트 전환을 처리하던 코드가 200줄 가까이 됐는데, React 19.2의 <ViewTransition> 컴포넌트로 교체하니 절반 이상이 증발했다. 번들에서 Framer Motion 의존성을 빼자 42KB가 사라졌고, Lighthouse 성능 점수가 87에서 91로 뛰었다.
처음에는 반신반의했다. 브라우저 네이티브 API가 Framer Motion만큼 매끄러울 리 없다고 생각했으니까. 근데 틀렸다.
<ViewTransition>이 해결한 진짜 문제
브라우저에는 View Transitions API가 있다. document.startViewTransition()을 호출하면 현재 DOM 스크린샷을 캡처하고, 새 DOM으로 렌더링한 뒤 CSS 애니메이션으로 둘 사이를 보간해준다. 개념은 단순하다. Chrome 111부터 SPA에서, 126부터는 MPA 크로스 도큐먼트 전환까지 지원했고, 2026년 4월 현재 Safari 18과 Firefox 146도 동일한 API를 탑재했다.
문제는 React와의 궁합이었다. startViewTransition은 DOM 업데이트가 동기적이어야 동작한다. React의 상태 업데이트는 기본적으로 비동기다. 이 불일치가 프론트엔드 개발자들을 2년 넘게 괴롭혔다. flushSync를 끼워넣으면 동시성 렌더링의 장점을 날려야 했고, useLayoutEffect 안에서 수동으로 타이밍을 잡으면 경쟁 조건이 터졌다. 결국 대부분의 팀이 "그냥 Framer Motion이 안전하다"로 돌아갔다.
React 19.2의 <ViewTransition> 컴포넌트는 이 2년짜리 간극을 정면으로 메웠다. 동시성 렌더러 내부에서 브라우저의 트랜지션 라이프사이클과 직접 동기화하도록 스케줄러를 조정했다. flushSync 해킹도, 수동 타이밍 조절도 필요 없다. 전환 영역을 컴포넌트로 감싸기만 하면 된다.
Next.js 16.2에서 실제 코드
next.config.js에 플래그 하나 켠다:
module.exports = {
experimental: { viewTransition: true },
};
레이아웃 컴포넌트에서 전환 영역을 감싼다:
import { ViewTransition } from 'react';
export default function Layout({ children }) {
return (
<div className="app-shell">
<nav>...</nav>
<ViewTransition name="page">
{children}
</ViewTransition>
</div>
);
}
CSS에서 전환 애니메이션을 정의한다:
::view-transition-old(page) {
animation: fade-out 150ms ease-out;
}
::view-transition-new(page) {
animation: slide-in 200ms ease-in;
}
<Link>를 클릭하면 전환이 자동으로 걸린다. AnimatePresence의 mode="wait", exit prop, onAnimationComplete 콜백 — 전부 필요 없다.
Framer Motion과의 현실적인 비교
통째로 지울 수 있냐는 질문에 대한 답은 "뭘 쓰고 있느냐에 달렸다"이다.
| 기능 | <ViewTransition> |
Framer Motion |
|---|---|---|
| 라우트 전환 | ✅ 네이티브 | ✅ AnimatePresence |
| 스프링 물리 | ❌ | ✅ type: "spring" |
| 드래그 제스처 | ❌ | ✅ drag |
| 레이아웃 애니메이션 | ⚠️ 제한적 | ✅ layoutId |
| 공유 엘리먼트 전환 | ✅ view-transition-name |
✅ layoutId |
| 번들 사이즈 | 0KB | ~42KB gzip |
라우트 전환이 해당 라이브러리를 쓰는 주된 이유였다면 오늘 당장 교체할 수 있다. 내가 유지보수하는 세 프로젝트 중 두 개가 정확히 이 케이스였다. 페이지 이동할 때 크로스페이드 하나 걸려 있을 뿐인데, 그 하나 때문에 42KB짜리 패키지를 끌고 다녔다.
블로그, 대시보드, 이커머스 — "전환 효과는 있으면 좋고 없으면 밋밋한" 프로젝트가 애니메이션 라이브러리 사용처의 대다수다. 이 카테고리에서는 브라우저 네이티브 트랜지션이 완벽한 대체제다. CSS ease-in-out이면 충분한 곳에 스프링 물리를 쓸 필요가 애초에 없었다.
반면, 카드 드래그 정렬이나 공유 레이아웃 전환이 핵심인 앱은 다르다. 그런 인터랙션을 순수 CSS만으로 구현하기엔 아직 한계가 뚜렷하다. 다만 그런 경우에도 framer-motion 패키지 전체를 import하는 대신 @motionone/dom 같은 경량 대안을 검토할 만하다. 필요한 기능만 가져오는 게 42KB 전체를 끌고 다니는 것보다 합리적이다.
브라우저 지원은 걱정할 게 없다
React의 <ViewTransition>은 same-document 전환 API를 쓴다. Next.js App Router의 내비게이션은 클라이언트 사이드 라우팅이니까, 크로스 도큐먼트 지원 여부는 신경 쓸 필요가 없다. Chrome 111+, Safari 18+, Firefox 133+ 모두 same-document 전환을 지원한다. 모던 브라우저 전체 커버.
MPA에서 크로스 도큐먼트 트랜지션을 쓰고 싶다면 Firefox 쪽에서 아직 부분 지원이지만, SPA 프레임워크 기반 프로젝트라면 걱정할 이유가 없다.
마이그레이션 순서
next.config.js에viewTransition: true추가AnimatePresence로 라우트 전환만 하던 컴포넌트를<ViewTransition>으로 교체스프링이나 드래그 같은 다른 기능은 건드리지 않는다
::view-transition-old,::view-transition-new가상 요소로 CSS 조절일주일 후
import { motion }검색해서 남은 곳이 없으면 패키지 제거
설정 한 줄, CSS 세 줄, 번들 -42KB. 번들 사이즈에 집착하는 인간으로서, 안 할 이유를 아직 못 찾았다.