MPA에서 SPA 같은 페이지 전환을 CSS 2줄로 — Cross-document View Transitions
사실 SPA로 갈아타야 할 이유 중 상당 부분은 딱 하나였습니다. 페이지 전환이 부드럽다는 것. 그게 전부였는데, 이제는 그 이유가 조금씩 흔들리고 있습니다.
2024년 Chrome 126이 Cross-document View Transitions를 출시하고 Safari 18.2가 뒤따르면서, 전 세계 브라우저 점유율 기준 약 75%를 커버하는 환경에서 MPA도 SPA 수준의 전환 효과를 CSS만으로 구현할 수 있게 됐습니다. 라우팅 라이브러리를 붙이고, 상태 관리를 얹고, 클라이언트 사이드 렌더링 전략을 세우는 복잡한 과정 없이요. JavaScript 번들 0바이트 증가, 기존 MPA 구조 그대로 두고 CSS 파일에 두 줄 추가하는 것만으로요.
View Transitions API의 Cross-document 방식은 출발 페이지와 도착 페이지 양쪽 CSS에 @view-transition 선언 하나만 추가하는 것으로 동작합니다. 이 글에서는 핵심 작동 원리부터 이커머스 카드 전환 같은 실무 패턴, 그리고 LCP 영향처럼 놓치기 쉬운 함정까지 10분 안에 기존 사이트에 적용할 수 있을 만큼 구체적으로 풀어봅니다.
목차
핵심 개념
SPA 방식 vs. MPA 방식 — API 구조의 차이
View Transitions API는 두 갈래로 나뉩니다. 먼저 나온 Same-document 방식은 JavaScript의 document.startViewTransition() 호출이 진입점이고, React나 Vue 같은 SPA 프레임워크와 잘 맞습니다. DOM을 직접 조작하거나 상태를 바꾸는 시점에 전환 효과를 끼워 넣는 구조죠.
저도 처음에는 MPA에도 같은 방식을 써야 하나 고민했는데, 아니었습니다. 서로 다른 HTML 파일 간 이동이니까 접근 자체가 달라야 했던 거죠. Cross-document 방식은 JavaScript 없이 CSS만으로 동작합니다.
/* 출발 페이지와 도착 페이지 양쪽 CSS에 추가 */
@view-transition {
navigation: auto;
}이 두 줄이 전부입니다. 같은 origin 내 페이지 이동이라면 브라우저가 자동으로 전환 효과를 처리해줍니다. 단, 몇 가지 경우에는 navigation: auto가 적용되지 않습니다. download 속성이 있는 링크나 target="_blank" 링크, 그리고 form 제출은 Cross-document 전환이 트리거되지 않습니다. 같은 origin 내의 일반 앵커 이동에서만 동작한다고 보면 정확합니다.
내부 동작: 브라우저가 페이지 이동 직전 현재 화면의 스크린샷을 캡처하고, 새 페이지가 렌더링된 후 두 상태를
::view-transition-old(root)와::view-transition-new(root)가상 요소로 합성해 애니메이션을 만드는 방식. 기본값은 전체 화면 크로스페이드.
공유 요소 전환 — Shared Element Transition
기본 크로스페이드도 나쁘지 않지만, MPA View Transitions의 진짜 재미는 Shared Element Transition에 있습니다. view-transition-name CSS 속성으로 양쪽 페이지의 대응 요소에 같은 이름을 붙이면, 브라우저가 두 요소를 "같은 것"으로 인식하고 자연스러운 morphing 애니메이션을 만들어줍니다.
/* 목록 페이지 */
.product-thumbnail {
view-transition-name: product-hero;
}
/* 상세 페이지 */
.product-hero-image {
view-transition-name: product-hero;
}목록 페이지의 작은 썸네일이 상세 페이지의 히어로 이미지로 자연스럽게 확장되는 효과가 이렇게 만들어집니다. 네이티브 앱에서 보던 그 전환 효과를 CSS 몇 줄로 구현하는 거죠.
주의:
view-transition-name은 한 페이지에서 반드시 유일해야 합니다. 같은 이름을 가진 요소가 두 개 이상 존재하면 전환이 깨집니다. 목록 페이지처럼 동일한 카드가 여러 개 있을 때는 동적으로 이름을 할당하는 처리가 필요합니다.
전환 방향별 애니메이션 분기 — View Transition Types
2025년 업데이트에서 추가된 기능입니다. 앞으로 이동할 때와 뒤로 이동할 때 서로 다른 애니메이션을 적용할 수 있어서, 모바일 앱의 스택 네비게이션과 비슷한 경험을 줄 수 있습니다. 다만 이 분기를 쓰려면 Navigation API로 전환 타입을 지정하는 JavaScript가 조금 필요합니다 — 완전한 CSS-only 방식은 아닙니다.
/* 앞으로 이동: 왼쪽으로 슬라이드 */
:active-view-transition-type(forward) {
&::view-transition-old(root) { animation-name: slide-out-left; }
&::view-transition-new(root) { animation-name: slide-in-right; }
}
/* 뒤로 이동: 오른쪽으로 슬라이드 */
:active-view-transition-type(backward) {
&::view-transition-old(root) { animation-name: slide-out-right; }
&::view-transition-new(root) { animation-name: slide-in-left; }
}
/* 위 animation-name이 참조하는 keyframe 정의 — 이 부분을 빠뜨리면 애니메이션이 붙지 않습니다 */
@keyframes slide-out-left {
to { transform: translateX(-100%); }
}
@keyframes slide-in-right {
from { transform: translateX(100%); }
}
@keyframes slide-out-right {
to { transform: translateX(100%); }
}
@keyframes slide-in-left {
from { transform: translateX(-100%); }
}view-transition-class를 활용하면 여러 공유 요소를 하나의 그룹으로 묶어 동일한 애니메이션 설정을 적용할 수도 있습니다. 예를 들어 상품 이미지들에 product-image라는 그룹 클래스를 붙이면 다음과 같이 씁니다.
.product-card img {
view-transition-name: var(--product-transition-name); /* 요소별 고유 이름 */
view-transition-class: product-image; /* 그룹 클래스 */
}
/* product-image 그룹 전체에 공통 설정 적용 */
::view-transition-group(product-image) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}실전 적용
예시 1: 이커머스 상품 카드 → 상세 페이지 전환
실무에서 가장 자주 맞닥뜨리는 패턴입니다. 카드 목록에서 썸네일을 클릭하면 해당 이미지가 상세 페이지의 히어로 영역으로 morphing되며 확장되는 효과입니다.
카드는 여러 개이므로 view-transition-name이 유일해야 한다는 제약 때문에 동적으로 이름을 할당해야 합니다. 인라인 style로 처리하는 게 가장 간단합니다. PHP든 Django든 Rails든, 서버에서 HTML을 렌더링할 때 id를 포함한 이름을 출력하면 됩니다.
<!-- 목록 페이지 HTML -->
<!-- CSS는 <head>의 <link>로 연결된 stylesheet 파일이나 <style> 태그에 추가 -->
<a href="/products/42">
<img
src="/images/product-42.jpg"
style="view-transition-name: product-42"
alt="상품명"
/>
</a>
<a href="/products/43">
<img
src="/images/product-43.jpg"
style="view-transition-name: product-43"
alt="상품명"
/>
</a>/* 목록 페이지 CSS — <head>의 stylesheet에 추가 */
@view-transition {
navigation: auto;
}
/* 모션 민감 사용자를 위한 필수 처리 */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}<!-- 상세 페이지: /products/42 -->
<img
src="/images/product-42-hero.jpg"
style="view-transition-name: product-42"
class="hero-image"
alt="상품 히어로 이미지"
/>/* 상세 페이지 CSS — 양쪽 모두 같은 @view-transition 선언이 필요합니다 */
@view-transition {
navigation: auto;
}
.hero-image {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}| 포인트 | 설명 |
|---|---|
인라인 style 사용 |
클래스로는 여러 카드에 같은 이름이 붙어버리므로, 요소별 고유 이름을 위해 인라인 스타일이 필요 |
product-{id} 패턴 |
목록과 상세 페이지 간 이름을 일치시키는 가장 단순한 방법 |
prefers-reduced-motion |
전정 장애 등 모션에 민감한 사용자의 접근성을 위한 필수 처리 |
prefers-reduced-motion에 대해 한 마디 더 보태자면, 이건 "여유가 있으면 넣는 것"이 아닙니다. 전정 장애가 있는 분들에게 갑작스러운 화면 움직임은 실제로 어지러움이나 메스꺼움을 유발할 수 있고, 시스템 설정에서 이를 제어할 수 있게 하는 것이 접근성의 기본 계약입니다. 솔직히 이 부분을 빠뜨리는 경우를 실무에서 꽤 봤는데, 백엔드를 주로 다루는 분들도 화면 전환 효과를 구현할 때는 꼭 챙겨두시면 좋습니다.
예시 2: Astro 사이트에 전체 적용
Astro를 사용하고 있다면 더 간단한 방법이 있습니다. Astro 3.x부터 도입된 <ViewTransitions /> 컴포넌트를 레이아웃에 추가하는 것만으로 사이트 전체에 View Transitions가 활성화됩니다. 미지원 브라우저 fallback까지 자체적으로 처리해주니 브라우저 호환성 걱정도 덜 수 있습니다.
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>{Astro.props.title}</title>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>---
// 특정 요소에 전환 이름 지정
const { productId } = Astro.props;
---
<img
src={`/images/product-${productId}.jpg`}
transition:name={`product-${productId}`}
alt="상품 이미지"
/>Astro는 transition:name 디렉티브로 view-transition-name을 선언적으로 관리할 수 있어서, 인라인 style을 직접 쓰는 것보다 훨씬 관리하기 편합니다.
장단점 분석
장점
솔직히 장점 목록을 보면 "이게 되면 왜 다들 SPA 쓰나" 싶은 마음도 드는데, 현실적으로는 지원 범위와 성능 이슈가 걸립니다. 그 전에 장점부터 정리하면 이렇습니다.
JavaScript가 전혀 필요 없으니 번들 크기에 영향이 없고, 브라우저 합성(compositor) 레이어에서 직접 처리되기 때문에 GPU가 메인 스레드 부담 없이 60fps 애니메이션을 유지합니다. 우리 팀에서 직접 확인해봐도 CPU 부하 없이 부드럽게 굴러가는 게 체감됐습니다. 미지원 브라우저에서는 전환 효과 없이 그냥 정상 동작하는 graceful degradation도 자연스럽게 처리됩니다. SPA와 달리 실제 HTML 문서 간 이동이라 크롤러가 각 페이지를 온전히 읽을 수 있고, 기존 MPA 구조를 건드리지 않고 CSS 파일 수정만으로 UX를 개선할 수 있다는 것도 큰 매력입니다.
| 항목 | 내용 |
|---|---|
| JavaScript 0 추가 | CSS만으로 완성, 번들 크기 영향 없음 |
| 하드웨어 가속 | GPU가 메인 스레드 부담 없이 직접 처리해 60fps 애니메이션 기대 가능 |
| 점진적 향상 | 미지원 브라우저에서는 전환 없이 정상 동작 |
| SEO 친화적 | 실제 HTML 문서 간 이동이므로 크롤러가 각 페이지를 온전히 읽음 |
| 기존 코드 유지 | MPA 구조 그대로, CSS 수정만으로 UX 개선 가능 |
단점 및 주의사항
LCP 영향이 가장 즉각적인 문제입니다. Core Web Vitals 연구에서 모바일 반복 방문 시 LCP가 약 +70ms 증가한다는 결과가 나왔습니다. 히어로 이미지처럼 LCP 요소 자체에 전환을 적용할 때 특히 주의가 필요합니다. 상세 페이지로 넘어가는 그 이미지가 사이트의 LCP를 결정하는 요소라면, 전환 효과로 오히려 검색 순위에 역효과가 날 수 있습니다.
4초 제한도 실무에서 의외로 자주 걸립니다. 페이지 로드가 4초를 넘으면 Chrome이 전환을 그냥 건너뛰기 때문에, 아무리 멋진 전환 효과를 만들어도 성능이 나쁘면 사용자가 볼 수 없습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| LCP 영향 | 모바일 반복 방문 시 LCP +70ms 증가 보고 | LCP 요소에는 전환 적용 신중히 고려 |
| 4초 제한 | 페이지 로드 4초 초과 시 Chrome이 전환을 건너뜀 | Core Web Vitals 기준 확보가 선행 조건 |
| 이름 유일성 제약 | 동일 view-transition-name이 두 개 이상이면 전환 깨짐 |
동적 ID 기반 이름 할당 처리 필요 |
| Same-origin 제한 | 다른 도메인 간 이동에는 적용 불가 | 동일 origin 내 이동 구조 설계 필요 |
| Firefox 미지원 | Cross-document 전환은 Firefox에서 작동 안 함 | graceful degradation으로 전환 없이 동작하도록 설계 |
| 스크린 리더 혼란 | 전환 중 두 DOM이 동시 존재하는 구간에서 혼란 가능 | prefers-reduced-motion 처리 필수 |
LCP(Largest Contentful Paint): 페이지에서 뷰포트 안에 보이는 가장 큰 콘텐츠 요소가 렌더링되는 시점을 측정하는 Core Web Vitals 지표. 히어로 이미지나 큰 텍스트 블록이 주로 대상이 되며, Google 검색 순위에도 영향을 줌.
실무에서 가장 흔한 실수
-
view-transition-name중복 지정: 목록 페이지에서 모든 카드에 동일한 CSS 클래스로view-transition-name을 적용하면 한 페이지에 같은 이름이 여러 개 생겨 전환이 깨집니다. 반드시 요소마다 고유한 이름이 되도록 인라인 스타일이나 서버 사이드 렌더링으로 동적 할당이 필요합니다. -
prefers-reduced-motion처리 누락: 화려한 전환 효과에 집중하다 보면 모션 민감 사용자를 위한 처리를 빠뜨리기 쉽습니다. 나중에 추가하려면 번거로우니 초기 설정 단계에서@view-transition과 함께 넣어두는 편이 훨씬 편합니다. -
페이지 성능 최적화 없이 전환 적용: View Transitions는 페이지 로드가 느릴수록 오히려 역효과입니다. 4초 제한에 걸려 전환이 건너뛰어지거나, LCP 지표가 악화될 수 있습니다. Core Web Vitals 기준을 먼저 확인해보시면 좋습니다.
마치며
View Transitions의 Cross-document 방식은 기존 멀티페이지 아키텍처를 유지하면서 SPA의 핵심 UX 이점을 CSS만으로 가져올 수 있는, 현시점에서 가장 현실적인 선택지입니다. Firefox 미지원이라는 한계가 있지만 graceful degradation으로 자연스럽게 처리할 수 있고, Interop 2025 목표에 포함된 만큼 지원 범위는 계속 넓어질 예정입니다.
그렇다면 지금 당장 시작할 수 있는 것부터:
- 기존 MPA 사이트 CSS 파일에
@view-transition { navigation: auto; }를 양쪽 페이지에 추가해보시면 됩니다. 기본 크로스페이드 전환이 즉시 적용됩니다. - 바로 아래에
@media (prefers-reduced-motion: reduce)블록을 함께 추가해 접근성 기준을 갖춰두시면 됩니다. - 대표적인 전환 포인트를 골라
view-transition-name으로 Shared Element Transition을 적용해보시면 됩니다. Chrome DevTools의 View Transitions 패널에서 전환 단계를 시각화해 디버깅할 수 있으니, 처음 설정할 때 활용해보시면 큰 도움이 됩니다.
참고 자료
- View Transition API | MDN
- Cross-document view transitions for MPA | Chrome for Developers
- What's new in view transitions (2025 update) | Chrome for Developers
- @view-transition CSS at-rule | MDN
- Using View Transition Types | MDN
- How to implement view transitions in multi-page apps | LogRocket Blog
- View Transitions | Astro Docs
- View Transition API & meta frameworks: a practical guide | Bejamas
- MPA View Transitions Deep Dive | Bram.us
- The Impact of CSS View Transitions on Web Performance | Core Web Vitals
- 7 View Transitions Recipes to Try | CSS-Tricks