본문 바로가기
Frontend2026년 8월 30일19분 읽기

View Transitions API — 웹에서 네이티브 앱 같은 화면 전환의 원리와 실전

YS
김영삼
조회 15
View Transitions API — 웹에서 네이티브 앱 같은 화면 전환의 원리와 실전

화면이 바뀔 때 요소가 '뚝' 사라졌다 다시 나타나는 웹의 오래된 이질감. View Transitions API는 브라우저가 직접 전후 화면을 스냅샷으로 찍어 그 사이를 애니메이션으로 이어준다.

지금까지는 이런 전환을 흉내 내려면 두 화면을 동시에 DOM에 올려두고 좌표를 계산하는 복잡한 코드가 필요했다. 이제는 document.startViewTransition() 한 줄, 그리고 MPA(여러 문서) 사이의 전환은 CSS 몇 줄이면 된다. 2026년 현재 크로스 도큐먼트 전환까지 크로미엄에 안정 탑재되면서 실무 선택지가 됐다.

1줄
같은 문서 전환 시작 코드
0줄 JS
MPA 전환(CSS만으로 가능)
~75%
전역 브라우저 지원(크로미엄+사파리, 대략)
2024~
크로스 도큐먼트 안정화 시점

문제: SPA 라우팅 전환은 왜 늘 어색했나

웹에서 페이지를 이동하면 기본적으로 화면 전체가 한 번 깜빡이며 새 내용으로 교체된다. SPA(단일 페이지 앱)는 이 깜빡임을 없애려고 라우팅을 자바스크립트로 가로채지만, 그 대가로 전환의 '연속성'을 잃는다. 목록에서 카드를 눌러 상세로 가면, 방금 보던 썸네일은 그냥 사라지고 상세 페이지의 큰 이미지가 뜬금없이 나타난다. 사용자의 머릿속에서 두 화면은 연결돼 있는데, 화면은 그 관계를 전혀 표현하지 못한다.

네이티브 모바일 앱은 이걸 오래전부터 해왔다. iOS의 화면 전환, 안드로이드의 공유 요소 전환(shared element transition)처럼, 이전 화면의 특정 요소가 다음 화면의 대응 요소로 부드럽게 '자라나며' 이동한다. 웹에서 이를 흉내 내려면 전환 순간 두 화면을 동시에 렌더링하고, 요소의 위치·크기를 getBoundingClientRect()로 재서 FLIP 기법으로 보간해야 했다. 라이브러리(Framer Motion 등)가 이걸 대신 해줬지만, 상태 관리가 복잡하고 스크롤·리스트 재정렬 같은 경우엔 쉽게 깨졌다.

핵심 난제는 '전환하는 동안 두 상태가 동시에 존재해야 한다'는 점이다. 옛 화면은 이미 사라지려 하고 새 화면은 아직 자리를 잡는 중인데, DOM은 원래 한 시점에 하나의 상태만 갖는다. View Transitions API는 이 문제를 브라우저 엔진 레벨에서 해결한다.

원리: 스냅샷과 의사요소 트리

같은 문서 안에서 document.startViewTransition(callback)를 호출하면 브라우저는 대략 다음 순서로 동작한다. (1) 현재 화면의 시각적 스냅샷(old)을 이미지처럼 캡처한다. (2) 콜백을 실행해 DOM을 새 상태로 바꾼다. (3) 새 상태의 스냅샷(new)을 캡처한다. (4) 두 스냅샷을 겹쳐 놓고 기본적으로 old는 페이드아웃, new는 페이드인하는 크로스페이드를 재생한다. 이 모든 게 렌더링 파이프라인 안에서 일어나므로 개발자가 좌표를 계산할 필요가 없다.

전환이 진행되는 동안 브라우저는 문서 루트 위에 전환 전용 의사요소 트리를 만든다. 이 트리는 CSS로 직접 겨냥할 수 있다.

::view-transition                 /* 전환 전체를 덮는 최상위 오버레이 */
└─ ::view-transition-group(name)  /* 이름 있는 요소 하나의 전환 그룹 */
   └─ ::view-transition-image-pair(name)
      ├─ ::view-transition-old(name)   /* 이전 상태 스냅샷 */
      └─ ::view-transition-new(name)   /* 새 상태 스냅샷 */

기본 크로스페이드도 결국 이 의사요소들에 걸린 기본 애니메이션일 뿐이다. 따라서 애니메이션을 바꾸고 싶으면 이 의사요소에 CSS animation을 얹으면 된다. 예를 들어 전체 전환 속도만 조정하려면 이렇게 한다.

/* 기본 크로스페이드를 조금 느리게, 이징 조정 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

여기서 root는 이름을 따로 주지 않은 경우 전체 페이지에 자동으로 붙는 기본 전환 이름이다.

가장 단순한 형태: 같은 문서에서 상태 바꾸기

SPA나 위젯 내부에서 DOM을 갱신할 때 쓰는 기본 패턴은 다음과 같다. 콜백 안에서 하던 DOM 변경을 그대로 감싸기만 하면 된다.

function updateView(nextState) {
  // API가 없는 브라우저를 위한 폴백: 그냥 즉시 갱신
  if (!document.startViewTransition) {
    render(nextState);
    return;
  }
  // 지원 브라우저: 전환으로 감싸기
  document.startViewTransition(() => {
    render(nextState); // 실제 DOM을 새 상태로 교체
  });
}

startViewTransitionViewTransition 객체를 돌려주며, 여기엔 단계별로 기다릴 수 있는 프로미스가 들어 있다. ready는 의사요소가 만들어져 애니메이션이 시작될 준비가 된 시점, finished는 전환이 끝난 시점, updateCallbackDone은 콜백(DOM 변경)이 끝난 시점이다.

const transition = document.startViewTransition(() => render(next));

try {
  await transition.ready;      // 애니메이션 시작 직전
  // 여기서 의사요소에 Web Animations API로 커스텀 애니메이션도 가능
} catch (e) {
  // 전환이 스킵됐을 때(reduced-motion 등)
}

await transition.finished;     // 완전히 끝난 뒤 후처리
참고콜백이 비동기 함수를 반환하면 브라우저는 그 프로미스가 풀릴 때까지 old 스냅샷을 유지한 채 기다린다. 다만 이 대기 시간이 길어지면 화면이 그 동안 멈춘 것처럼 보이므로, 데이터 페칭 같은 오래 걸리는 작업은 콜백 밖에서 끝내고 콜백 안에서는 '이미 준비된 데이터로 DOM만 교체'하는 게 원칙이다.

공유 요소 애니메이션: view-transition-name

진짜 '네이티브 같은' 느낌은 여기서 나온다. 전후 화면에 논리적으로 같은 요소가 있을 때, 두 요소에 동일한 view-transition-name을 주면 브라우저가 이를 하나의 그룹으로 묶어 위치·크기·모양을 보간한다. 목록의 작은 썸네일이 상세의 큰 히어로 이미지로 매끄럽게 자라나는 효과가 이 한 줄로 나온다.

/* 목록 페이지의 썸네일 */
.card--active .thumb {
  view-transition-name: hero-image;
}
/* 상세 페이지의 큰 이미지 — 같은 이름 */
.detail .hero {
  view-transition-name: hero-image;
}

규칙이 하나 있다. view-transition-name한 시점에 문서 전체에서 유일해야 한다. 같은 이름이 동시에 둘 이상 존재하면 브라우저는 전환을 스킵하고 콘솔에 경고를 남긴다. 그래서 리스트의 여러 카드 중 '지금 클릭된 하나'에만 이름을 부여하는 식으로, 전환 직전에 동적으로 이름을 붙였다 떼는 패턴을 자주 쓴다.

// 클릭된 카드에만 전환 이름을 부여한 뒤 전환 시작
card.style.viewTransitionName = 'hero-image';

const t = document.startViewTransition(() => navigateToDetail(id));
await t.finished;

// 전환이 끝나면 이름을 회수해 다음 전환에 대비
card.style.viewTransitionName = '';

2026년 기준으로는 클래스 기반 선택을 돕는 view-transition-class도 크로미엄에 들어와, 이름이 각각 다른 여러 요소에 공통 애니메이션을 한 번에 지정할 수 있다. 리스트 아이템 수십 개에 각기 고유 이름을 주되 동일한 이징을 걸고 싶을 때 유용하다.

같은 문서 vs 크로스 도큐먼트(MPA) 전환

View Transitions에는 두 갈래가 있다. 하나는 지금까지 본 같은 문서(same-document) 전환으로, SPA에서 startViewTransition을 직접 호출한다. 다른 하나는 크로스 도큐먼트(cross-document) 전환으로, 실제로 다른 HTML 문서로 내비게이션하는 전통적 MPA에서 자바스크립트 없이 CSS만으로 전환을 켠다. 후자는 같은 오리진(origin) 내 내비게이션에서만 동작한다.

MPA 전환은 양쪽 문서의 CSS에 옵트인 한 줄만 넣으면 된다.

/* 두 문서(떠나는 쪽·도착하는 쪽) 모두의 CSS에 */
@view-transition {
  navigation: auto;
}

이렇게만 해도 페이지 이동 시 기본 크로스페이드가 걸린다. 공유 요소도 동일하게 view-transition-name으로 이어지며, 서버 렌더링된 정적 페이지·전통적 멀티페이지 사이트에서도 프레임워크 없이 앱 같은 전환을 얻는다. 이것이 2024년 이후 가장 큰 변화다. 아래 표로 둘을 비교한다.

구분같은 문서(SPA)크로스 도큐먼트(MPA)
시작 방법startViewTransition() 호출@view-transition CSS 옵트인
JS 필요필요(라우터가 호출)불필요(CSS만)
범위 제한없음(같은 페이지 내 상태)동일 오리진 내비게이션만
세밀 제어프로미스로 단계별 제어 가능pageswap·pagereveal 이벤트로 제어
대표 사용처React/Vue SPA, 위젯 상태 전환Astro/정적 사이트, 전통 MPA

MPA 전환에서 세밀한 제어가 필요하면 pageswap(떠나기 직전)과 pagereveal(새 페이지가 드러날 때) 이벤트를 쓴다. 예컨대 이동 방향(뒤로 가기 vs 앞으로)에 따라 슬라이드 방향을 바꾸거나, 어떤 요소에 전환 이름을 붙일지 마지막 순간에 결정할 수 있다.

프레임워크 통합: React/Next · Astro · SvelteKit

대부분의 프레임워크는 이미 얇은 래퍼를 제공한다. 원리만 알면 사용은 거의 한 줄이다.

  • Astro: 가장 먼저·가장 매끄럽게 통합했다. <ClientRouter />를 레이아웃에 넣으면 MPA임에도 크로스 도큐먼트 전환이 켜지고, 요소에 transition:name·transition:animate 디렉티브로 공유 요소와 애니메이션을 선언한다.
  • React / Next.js: React 19대의 실험적 unstable_ViewTransition 컴포넌트로 요소를 감싸면 리렌더·라우팅 전환에 자동으로 이름과 전환이 붙는다. Next.js App Router도 이를 앞단에서 연결하는 실험 옵션을 제공한다. 직접 제어가 필요하면 라우터 이벤트에서 startViewTransition을 부르는 방식도 여전히 유효하다.
  • SvelteKit: onNavigate 훅 안에서 document.startViewTransition을 호출하는 공식 권장 패턴이 문서화돼 있다.
// SvelteKit — onNavigate에서 전환 적용 (공식 패턴)
import { onNavigate } from '$app/navigation';

onNavigate((navigation) => {
  if (!document.startViewTransition) return;

  return new Promise((resolve) => {
    document.startViewTransition(async () => {
      resolve();                 // 전환 콜백 안에서
      await navigation.complete;  // 실제 내비게이션 완료를 기다림
    });
  });
});
참고React의 ViewTransition은 2026년 현재 실험적(unstable) API다. 접두사가 붙은 API는 시그니처가 바뀔 수 있으니, 프로덕션에서는 버전을 고정하고 릴리스 노트를 추적하라. 반면 순수 CSS @view-transitiondocument.startViewTransition 자체는 이미 표준으로 안정화된 부분이다.

접근성: prefers-reduced-motion을 반드시 존중하라

움직임에 민감한 사용자(전정기관 장애 등)에게 큰 화면 이동 애니메이션은 어지럼증·불편을 유발한다. 운영체제의 '동작 줄이기' 설정은 prefers-reduced-motion: reduce 미디어 쿼리로 감지되며, View Transitions는 이걸 자동으로 꺼주지 않으므로 개발자가 명시적으로 다뤄야 한다.

가장 안전한 방법은 CSS에서 전환 애니메이션을 무력화하는 것이다. 전환을 완전히 없애기보다, 위치 이동은 빼고 짧은 페이드 정도만 남기는 절충이 흔하다.

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

JS 쪽에서도 방어할 수 있다. 설정이 켜져 있으면 아예 전환으로 감싸지 않고 즉시 갱신한다.

const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!reduce && document.startViewTransition) {
  document.startViewTransition(() => render(next));
} else {
  render(next); // 전환 없이 바로
}

또 하나. 전환 중 스냅샷은 정지 이미지이므로, 그 사이 스크린리더의 포커스나 라이브 리전 읽기가 어긋나지 않도록 전환 시간은 짧게(대개 200~400ms) 유지하는 게 좋다. 화려함보다 '방해되지 않음'이 우선이다.

브라우저 지원·폴백, 그리고 남용 주의

2026년 현재 같은 문서 전환은 크로미엄 계열과 사파리에서 지원되고, 크로스 도큐먼트 전환은 크로미엄에서 안정, 사파리는 뒤따르는 상황, 파이어폭스는 구현이 진행 중이다. 다행히 이 API는 점진적 향상(progressive enhancement)에 완벽히 들어맞는다. 미지원 브라우저에서는 전환이 그냥 즉시 교체로 떨어질 뿐, 기능은 멀쩡히 동작한다. 그래서 폴백 코드는 앞서 본 if (!document.startViewTransition) 가드가 사실상 전부다.

마지막은 절제에 대한 이야기다. 전환이 쉬워졌다고 모든 인터랙션에 붙이면 오히려 앱이 느리고 산만해 보인다. 아래는 '쓰지 말아야 할 때'의 신호다.

  • 전환이 사용자 흐름을 지연시켜 체감 반응이 느려질 때 — 자주 반복되는 클릭엔 전환을 빼라.
  • 두 화면 사이에 논리적 연속성이 없을 때 — 관계없는 요소를 억지로 morph시키면 혼란만 준다.
  • 큰 영역이 매번 크게 움직여 시선을 강제로 끌 때 — 특히 자주 오가는 경로에서.
주의공유 요소 전환은 old·new 양쪽 스냅샷이 겹쳐 렌더링되므로, 매우 큰 이미지나 무거운 레이아웃을 대상으로 하면 전환 순간 프레임 드랍이 생길 수 있다. 대상은 명확한 히어로 요소 1~2개로 제한하고, 나머지는 기본 크로스페이드에 맡기는 편이 성능·주목도 모두에 낫다.

정리하면, View Transitions API는 '두 화면 사이에 하나의 진실만 존재하던' 웹의 근본 제약을 브라우저 레벨에서 풀어낸 표준이다. SPA에선 라우터 한 줄, MPA에선 CSS 한 줄로 시작하고, 공유 요소·접근성·절제라는 세 축만 지키면 프레임워크에 얽매이지 않고 네이티브에 가까운 전환을 오래 유지되는 방식으로 얻을 수 있다.

자주 묻는 질문

View Transitions API를 쓰면 무거운 애니메이션 라이브러리를 완전히 대체할 수 있나요?

화면 간 전환(라우팅, 상태 교체, 공유 요소 morph)에 한해서는 상당 부분 대체합니다. 브라우저가 스냅샷과 좌표 보간을 대신 해주므로 FLIP을 직접 구현할 일이 사라집니다. 다만 제스처 기반의 상호작용형 애니메이션, 물리 스프링, 복잡한 타임라인 시퀀스 등은 여전히 Motion 같은 전용 라이브러리나 Web Animations API가 더 적합합니다. 둘은 경쟁이 아니라 역할 분담에 가깝습니다.

view-transition-name이 중복되면 어떻게 되나요?

같은 시점에 동일한 이름이 문서 내 둘 이상 존재하면 브라우저는 해당 전환을 스킵하고 콘솔에 경고를 출력합니다. 그래서 리스트에서는 클릭된 항목 하나에만 전환 직전 이름을 부여하고 전환이 끝나면 회수하는 패턴을 씁니다. 이름은 CSS로 정적으로 주기보다 상황에 따라 JS로 동적으로 붙였다 떼는 경우가 많습니다.

크로스 도큐먼트(MPA) 전환은 외부 사이트로 이동할 때도 되나요?

아니요. 크로스 도큐먼트 전환은 동일 오리진 내비게이션에서만 동작합니다. 즉 같은 도메인(스킴·호스트·포트 일치) 안에서 페이지를 이동할 때만 전환이 적용되며, 외부 도메인으로 나갈 때는 적용되지 않습니다. 보안·프라이버시상의 제약입니다.

전환 중에 데이터를 fetch해도 되나요?

가능하지만 권장하지 않습니다. startViewTransition 콜백이 오래 걸리는 프로미스를 반환하면 그동안 old 스냅샷이 화면에 고정돼 멈춘 것처럼 보입니다. 데이터 로딩은 전환 시작 전에 끝내고, 콜백 안에서는 이미 준비된 데이터로 DOM만 즉시 교체하는 것이 원칙입니다. 로딩이 불가피하면 스켈레톤 상태로 먼저 전환한 뒤 데이터가 오면 갱신하세요.

접근성 때문에 전환을 아예 끄는 게 나을까요?

전부 끄기보다 prefers-reduced-motion: reduce일 때 위치 이동 같은 큰 움직임만 제거하고 짧은 페이드 정도로 낮추는 절충이 일반적입니다. 사용자가 OS에서 '동작 줄이기'를 켰다면 그 의사를 반드시 존중해야 하며, View Transitions는 이를 자동으로 처리하지 않으므로 CSS 미디어 쿼리나 JS 가드로 개발자가 직접 구현해야 합니다.

지원하지 않는 브라우저에서는 어떻게 처리하나요?

별도 폴백 UI가 거의 필요 없습니다. document.startViewTransition이 없으면 전환 없이 즉시 화면이 교체될 뿐 기능 자체는 정상 동작합니다. if (!document.startViewTransition) { render(); return; } 형태의 가드 한 줄이면 충분하며, 이것이 점진적 향상의 이상적인 사례입니다. MPA의 @view-transition 역시 미지원 브라우저는 규칙을 무시하고 평범하게 페이지를 넘깁니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록