View Transitions API를 사용한 원활하고 간단한 전환

Jake Archibald
Jake Archibald

브라우저 지원

  • 111
  • 111
  • x
  • x

소스

View Transition API를 사용하면 두 상태 간에 애니메이션 전환을 만드는 동시에 단일 단계에서 DOM을 쉽게 변경할 수 있습니다. Chrome 111 이상에서 사용할 수 있습니다.

View Transition API로 생성된 전환입니다. 데모 사이트 사용해 보기 – Chrome 111 이상이 필요합니다.

이 기능이 필요한 이유는 무엇인가요?

페이지 전환은 멋지게 보일 뿐만 아니라 흐름의 방향을 전달하며 페이지마다 어떤 요소가 관련되어 있는지 명확하게 보여줍니다. 데이터를 가져오는 동안에도 발생할 수 있기 때문에 성능을 보다 빠르게 인지할 수 있습니다.

하지만 CSS 전환, CSS 애니메이션, 웹 애니메이션 API와 같은 애니메이션 도구는 이미 웹에 있습니다. 그렇다면 요소를 옮기는 데 새로운 방법이 필요한 이유는 무엇일까요?

사실 상태 전환은 이미 있는 도구로도 어렵습니다.

간단한 크로스 페이드 같은 작업조차도 두 상태가 동시에 나타나게 됩니다. 이는 나가는 요소의 추가 상호작용을 처리하는 것과 같은 사용성 문제를 일으킵니다. 또한 보조 기기 사용자의 경우 이전 및 이후 상태가 동시에 DOM에 있는 기간이 있으며, 시각적으로는 보기 좋게 괜찮지만 읽기 위치와 포커스가 손실될 수 있습니다.

두 상태가 스크롤 위치가 다르면 상태 변경을 처리하기가 특히 어렵습니다. 요소가 한 컨테이너에서 다른 컨테이너로 이동하면 overflow: hidden 및 다른 형태의 클리핑에 문제가 발생할 수 있습니다. 즉, 원하는 효과를 얻으려면 CSS를 재구성해야 합니다.

불가능하지는 않고 정말 어려울 뿐입니다.

뷰 전환을 사용하면 상태가 겹치지 않고 DOM을 변경할 수 있으므로 더 쉬운 방법이 가능하지만 스냅샷으로 생성된 뷰를 사용하여 상태 간에 전환 애니메이션을 만들 수 있습니다.

또한 현재 구현에서는 단일 페이지 앱 (SPA)을 타겟팅하지만 전체 페이지 로드 간에 전환할 수 있도록 기능이 확장될 예정입니다. 이는 현재 불가능한 작업입니다.

표준화 상태

이 기능은 W3C CSS Working Group 내에서 초안 사양으로 개발 중입니다.

API 설계가 만족스러우면 이 기능을 안정화 버전으로 출시하는 데 필요한 프로세스와 검사를 시작할 것입니다.

개발자 의견은 매우 중요하므로 제안 및 질문을 통해 GitHub에서 문제를 제출해 주세요.

가장 간단한 전환: 크로스 페이드

기본 뷰 전환은 크로스 페이드이므로 API를 소개하기 좋습니다.

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

여기서 updateTheDOMSomehow는 DOM을 새 상태로 변경합니다. 요소 추가/삭제, 클래스 이름 변경, 스타일 변경 등 원하는 대로 수행할 수 있습니다.

이렇게 페이지가 크로스 페이드됩니다.

기본 크로스 페이드입니다. 최소 데모. 출처.

크로스 페이드는 그다지 인상적이지 않네요. 다행히 전환을 맞춤설정할 수 있습니다. 하지만 먼저 이 기본적인 크로스 페이드의 작동 방식을 이해해야 합니다.

전환의 작동 방식

위의 코드 샘플을 사용해 보겠습니다.

document.startViewTransition(() => updateTheDOMSomehow(data));

.startViewTransition()가 호출되면 API가 페이지의 현재 상태를 캡처합니다. 여기에는 스크린샷을 찍는 것도 포함됩니다.

완료되면 .startViewTransition()에 전달된 콜백이 호출됩니다. 여기서 DOM이 변경됩니다. 그런 다음 API가 페이지의 새로운 상태를 캡처합니다.

상태가 캡처되면 API는 다음과 같이 의사 요소 트리를 구성합니다.

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition는 페이지의 다른 모든 항목 위에 오버레이로 표시됩니다. 이 색상은 전환 배경색을 설정하려는 경우에 유용합니다.

::view-transition-old(root)는 이전 뷰의 스크린샷이고 ::view-transition-new(root)는 새 뷰의 실시간 표현입니다. 둘 다 CSS '대체 콘텐츠'로 렌더링됩니다(예: <img>).

이전 뷰는 opacity: 1에서 opacity: 0로 애니메이션되지만 새 뷰는 opacity: 0에서 opacity: 1로 애니메이션되어 크로스 페이드를 만듭니다.

모든 애니메이션은 CSS 애니메이션을 사용하여 실행되므로 CSS로 맞춤설정할 수 있습니다.

간단한 맞춤설정

위의 모든 의사 요소는 CSS로 타겟팅할 수 있으며, 애니메이션은 CSS를 사용하여 정의되므로 기존 CSS 애니메이션 속성을 사용하여 수정할 수 있습니다. 예를 들면 다음과 같습니다.

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

이렇게 한 가지만 변경하면 페이드가 정말 느리게 바뀝니다.

긴 크로스 페이드 최소 데모. 출처.

그래도 인상적이지는 않아요. 대신 Material Design의 공유 축 전환을 구현해 보겠습니다.

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

결과는 다음과 같습니다.

공유 축 전환. 최소 데모. 출처.

여러 요소 전환

이전 데모에서는 전체 페이지가 공유 축 전환에 관련되어 있습니다. 대부분의 페이지에서 작동하지만 다시 밀어넣기 위해 미끄러져 나오기 때문에 제목에는 적합하지 않은 것 같습니다.

이를 방지하려면 페이지의 나머지 부분에서 헤더를 추출하여 별도로 애니메이션을 적용할 수 있습니다. 요소에 view-transition-name를 할당하면 됩니다.

.main-header {
  view-transition-name: main-header;
}

view-transition-name의 값은 원하는 대로 지정할 수 있습니다 (전환 이름이 없는 none는 제외). 전환 과정에서 요소를 고유하게 식별하는 데 사용됩니다.

그 결과는 다음과 같습니다.

공유 축이 고정 헤더로 전환됩니다. 최소 데모. 출처.

이제 헤더가 그대로 유지되고 크로스 페이드됩니다.

이 CSS 선언으로 인해 유사 요소 트리가 변경되었습니다.

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

이제 전환 그룹이 두 개 있습니다. 하나는 헤더용, 다른 하나는 나머지용입니다. 이러한 개체는 CSS를 사용하여 독립적으로 타겟팅될 수 있고 다른 전환을 지정할 수 있습니다. 하지만 여기서는 main-header가 크로스 페이드인 기본 전환인 상태로 남아 있습니다.

기본 전환은 크로스 페이드만이 아닙니다. ::view-transition-group도 다음과 같이 전환됩니다.

  • 위치 및 변환 (transform를 통해)
  • 너비

헤더의 크기와 위치가 DOM 양쪽이 같으므로 지금까지는 문제가 되지 않았습니다. 그러나 헤더의 텍스트를 추출할 수도 있습니다.

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content는 요소가 나머지 너비까지 늘어나지 않고 텍스트의 크기가 되도록 사용됩니다. 이 속성이 없으면 뒤로 화살표를 사용하면 헤더 텍스트 요소의 크기가 줄어들지만 두 페이지에서 모두 동일한 크기로 설정하려고 합니다.

이제 다음 3가지 단계를 살펴보겠습니다.

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

하지만 이번에는 기본값을 사용합니다.

헤더 텍스트 슬라이딩. 최소 데모. 출처.

이제 제목 텍스트가 뒤로 버튼을 위한 공간을 만들기 위해 약간 만족스럽게 슬라이드합니다.

디버깅 전환

뷰 전환은 CSS 애니메이션을 기반으로 빌드되므로 Chrome DevTools의 Animations 패널은 전환 디버깅에 유용합니다.

Animations 패널을 사용하면 다음 애니메이션을 일시중지한 다음 애니메이션의 앞뒤로 스크러빙할 수 있습니다. 이 과정에서 전환 유사 요소는 요소 패널에서 찾을 수 있습니다.

Chrome 개발자 도구로 뷰 전환 디버깅

전환 요소는 동일한 DOM 요소일 필요가 없음

지금까지는 view-transition-name를 사용하여 헤더와 헤더의 텍스트를 위한 별도의 전환 요소를 만들었습니다. 이들은 DOM 변경 전후에 개념적으로 동일한 요소이지만, 그렇지 않은 경우에도 전환을 만들 수 있습니다.

예를 들어 기본 동영상 삽입에 다음과 같이 view-transition-name를 지정할 수 있습니다.

.full-embed {
  view-transition-name: full-embed;
}

그런 다음 썸네일을 클릭하면 전환되는 동안에만 동일한 view-transition-name를 받을 수 있습니다.

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

그러면 결과는 다음과 같습니다.

한 요소가 다른 요소로 전환됩니다. 최소 데모. 출처.

이제 썸네일이 기본 이미지로 전환됩니다. 개념적으로는 말 그대로 다른 요소이지만 전환 API는 이러한 요소를 동일한 view-transition-name를 공유하므로 동일한 요소로 취급합니다.

이를 위한 실제 코드는 미리보기 이미지 페이지로의 전환도 처리하므로 위의 간단한 예보다 조금 더 복잡합니다. 전체 구현은 소스를 참조하세요.

맞춤 시작 및 종료 전환

다음 예를 살펴보세요.

사이드바에 들어가거나 나갑니다. 최소 데모. 출처.

사이드바는 전환의 일부입니다.

.sidebar {
  view-transition-name: sidebar;
}

그러나 이전 예의 헤더와 달리 사이드바는 일부 페이지에만 표시됩니다. 두 상태에 모두 사이드바가 있는 경우 전환 의사 요소는 다음과 같습니다.

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

그러나 사이드바가 새 페이지에만 있는 경우 ::view-transition-old(sidebar) 유사 요소는 표시되지 않습니다. 사이드바에 '이전' 이미지가 없으므로 이미지 쌍에는 ::view-transition-new(sidebar)만 있습니다. 마찬가지로 사이드바가 이전 페이지에만 있는 경우 이미지 쌍에는 ::view-transition-old(sidebar)만 있습니다.

위의 데모에서는 사이드바가 진입하는지, 나가는지 또는 두 상태에 있는지에 따라 다르게 전환됩니다. 오른쪽으로 슬라이드하여 페이드 인하여 들어가고 오른쪽으로 슬라이드했다가 페이드 아웃하여 나가고 두 상태 모두에 있을 때는 그대로 유지됩니다.

특정 시작 및 종료 전환을 만들려면 :only-child 유사 클래스를 사용하여 이전/새 유사 요소가 이미지 쌍의 유일한 하위 요소일 때 이 요소를 타겟팅합니다.

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

이 경우 기본값이 완벽하므로 사이드바가 두 상태에 모두 있는 경우 특별한 전환은 없습니다.

비동기 DOM 업데이트 및 콘텐츠 대기

.startViewTransition()에 전달된 콜백은 프로미스를 반환하여 비동기 DOM 업데이트를 허용하고 중요한 콘텐츠가 준비되기를 기다립니다.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

Promise가 실행될 때까지 전환이 시작되지 않습니다. 이 시간 동안에는 페이지가 정지되므로 지연이 최소한으로 유지되어야 합니다. 특히 네트워크 가져오기는 .startViewTransition() 콜백의 일부로 수행하는 것이 아니라 .startViewTransition()를 호출하기 전에 실행되어야 하며, 페이지의 상호작용은 여전히 완벽합니다.

이미지나 글꼴이 준비될 때까지 기다리려면 적극적인 제한 시간을 사용해야 합니다.

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

하지만 경우에 따라 지연을 완전히 없애고 이미 보유한 콘텐츠를 사용하는 것이 더 나을 수 있습니다.

이미 보유한 콘텐츠를 최대한 활용하기

썸네일이 더 큰 이미지로 전환되는 경우:

기본 전환은 크로스 페이드입니다. 즉, 미리보기 이미지가 아직 로드되지 않은 전체 이미지와 크로스 페이드될 수 있습니다.

이 문제를 처리하는 한 가지 방법은 전환을 시작하기 전에 전체 이미지가 로드될 때까지 기다리는 것입니다. 이 작업은 .startViewTransition()를 호출하기 전에 실행하는 것이 이상적입니다. 그러면 페이지의 상호작용이 유지되고 스피너가 표시되어 사용자에게 항목이 로드 중임을 나타낼 수 있습니다. 하지만 이 경우에는 더 좋은 방법이 있습니다.

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

이제 미리보기 이미지가 사라지지 않고 전체 이미지 아래에 표시됩니다. 즉, 새 뷰가 로드되지 않았다면 전환하는 동안 썸네일이 표시됩니다. 즉, 전환이 즉시 시작되고 전체 이미지가 자체 시간에 로드될 수 있습니다.

새 보기에 투명도가 표시되면 이 방식이 작동하지 않지만, 이 경우에는 그렇지 않다는 것을 알기 때문에 최적화를 수행할 수 있습니다.

가로세로 비율 변경 처리

지금까지는 모든 전환이 가로세로 비율이 동일한 요소로 이루어졌지만, 항상 그런 것은 아닙니다. 썸네일이 1:1이고 기본 이미지가 16:9이면 어떻게 되나요?

한 요소가 다른 요소로 전환되고 가로세로 비율이 변경됩니다. 최소 데모. 출처.

기본 전환에서는 그룹이 이전 크기에서 이후 크기로 애니메이션 처리됩니다. 이전 뷰와 새 뷰는 그룹의 너비가 100% 이며 자동 높이가 적용되어 그룹 크기와 관계없이 가로세로 비율이 유지됩니다.

이는 좋은 기본값이지만 이 경우에는 아닙니다. 따라서

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

즉, 너비가 확장될 때 썸네일이 요소 중앙에 머무르지만 1:1에서 16:9로 전환되면 전체 이미지가 '잘리지 않습니다'.

기기 상태에 따라 전환 변경

모바일과 데스크톱에서 서로 다른 전환을 사용할 수 있습니다. 예를 들어 모바일에서는 전체 슬라이드를 실행하지만 데스크톱에서는 좀 더 미세한 슬라이드입니다.

한 요소가 다른 요소로 전환됩니다. 최소 데모. 출처.

일반 미디어 쿼리를 사용하면 됩니다.

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

일치하는 미디어 쿼리에 따라 view-transition-name에 할당하는 요소를 변경하는 것이 좋습니다.

'움직임 감소' 환경설정에 따른 반응

사용자는 운영체제를 통해 모션이 줄어든 것을 선호하며 이러한 선호도는 CSS를 통해 노출됩니다.

이러한 사용자의 전환을 방지할 수 있습니다.

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

하지만 '움직임 감소'가 선호된다고 해서 사용자가 움직임이 없다는 뜻은 아닙니다. 위의 방법 대신 좀 더 섬세한 애니메이션을 선택할 수도 있지만, 이와 동시에 요소와 데이터 흐름 간의 관계를 표현하는 애니메이션을 선택할 수도 있습니다.

탐색 유형에 따라 전환 변경

특정 페이지 유형에서 다른 페이지로 이동할 때 맞춤 전환을 사용해야 하는 경우가 있습니다. 또는 '뒤로' 탐색은 '앞으로' 탐색과 달라야 합니다.

'뒤로' 이동할 때 전환이 발생합니다. 최소 데모. 출처.

이러한 사례를 처리하는 가장 좋은 방법은 <html>에 클래스 이름(문서 요소라고도 함)을 설정하는 것입니다.

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

이 예에서는 전환이 최종 상태에 도달하면 확인되는 프로미스인 transition.finished를 사용합니다. 이 객체의 다른 속성은 API 참조에서 다룹니다.

이제 CSS에서 이 클래스 이름을 사용하여 전환을 변경할 수 있습니다.

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

미디어 쿼리와 마찬가지로 이러한 클래스의 존재를 사용하여 view-transition-name를 가져올 요소를 변경할 수도 있습니다.

다른 애니메이션을 정지하지 않고 전환

동영상 전환 위치에 대한 다음 데모를 살펴보세요.

동영상 전환. 최소 데모. 출처.

잘못된 점이 있었나요? 가입하지 않았더라도 걱정하지 마세요. 여기서는 속도가 느려집니다.

동영상 전환이 느려집니다. 최소 데모. 출처.

전환하는 동안 동영상이 정지되는 것처럼 보이다가 재생 중인 버전의 동영상이 페이드 인됩니다. ::view-transition-old(video)는 이전 뷰의 스크린샷이고 ::view-transition-new(video)는 새 뷰의 라이브 이미지이기 때문입니다.

해결할 수 있지만 먼저 해결할 가치가 있는지 자문해 보세요. 전환이 정상 속도로 재생될 때 '문제'가 나타나지 않았다면 그것을 변경하지 않겠습니다.

정말로 수정하려면 ::view-transition-old(video)를 표시하지 않고 ::view-transition-new(video)로 바로 전환하세요. 이렇게 하려면 기본 스타일과 애니메이션을 재정의하면 됩니다.

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

이상입니다

동영상 전환이 느려집니다. 최소 데모. 출처.

이제 전환이 진행되는 동안 동영상이 재생됩니다.

JavaScript로 애니메이션 적용

지금까지는 모든 전환을 CSS를 사용하여 정의했지만, CSS만으로는 충분하지 않은 경우도 있습니다.

원 전환. 최소 데모. 출처.

이러한 전환의 몇 가지 부분은 CSS만으로는 달성할 수 없습니다.

  • 애니메이션은 클릭 위치에서 시작됩니다.
  • 애니메이션은 가장 먼 모서리까지 반경이 있는 원으로 끝납니다. 하지만 향후 CSS에서도 이 방식이 가능하기를 바랍니다.

다행히 Web Animation API를 사용하여 전환을 만들 수 있습니다.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

이 예에서는 전환 유사 요소가 성공적으로 생성되면 확인되는 프로미스인 transition.ready를 사용합니다. 이 객체의 다른 속성은 API 참조에서 다룹니다.

개선으로 사용되는 전환

View Transition API는 DOM 변경사항을 '래핑'하고 이에 대한 전환을 만들도록 설계되었습니다. 그러나 DOM은 성공했지만 전환이 실패하는 경우, 앱이 '오류' 상태로 전환되지 않아야 하기 때문입니다. 전환이 실패하지 않는 것이 이상적이지만, 실패하더라도 나머지 사용자 환경에 지장을 주지는 않아야 합니다.

전환을 향상으로 취급하려면, 전환이 실패할 경우 앱에서 뭔가를 발생시킬 수 있는 방식으로 전환 프로미스를 사용하지 않도록 주의해야 합니다.

금지사항
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

이 예의 문제는 전환이 ready 상태에 도달할 수 없으면 switchView()가 거부하지만 뷰를 전환하는 데 실패했음을 의미하지는 않습니다. DOM이 업데이트되었지만 view-transition-name가 중복되어 전환을 건너뛰었습니다.

다음을 수행합니다.

의견을 제시하지
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

이 예에서는 transition.updateCallbackDone를 사용하여 DOM 업데이트를 기다리고 업데이트가 실패하면 거부합니다. switchView는 전환에 실패하면 더 이상 거부하지 않고 DOM 업데이트가 완료될 때 확인되고 실패하면 거부됩니다.

새 뷰가 '정착'되었을 때(예: 애니메이션 전환이 완료되거나 끝으로 건너뛴 경우) switchView를 확인하려면 transition.updateCallbackDonetransition.finished로 바꿉니다.

폴리필은 아니지만...

이 기능이 유용한 방식으로 폴리필할 수 있다고 생각하지 않지만 틀린 것으로 입증되어 다행입니다.

그러나 이 도우미 함수를 사용하면 뷰 전환을 지원하지 않는 브라우저에서 훨씬 더 쉽게 작업할 수 있습니다.

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

다음과 같이 사용할 수 있습니다.

function spaNavigate(data) {
  const classNames = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

뷰 전환을 지원하지 않는 브라우저에서는 updateDOM가 계속 호출되지만 애니메이션 전환은 제공되지 않습니다.

전환 중에 <html>에 추가할 classNames를 제공할 수도 있으므로 탐색 유형에 따라 전환을 더 쉽게 변경할 수 있습니다.

애니메이션을 원하지 않는 경우 뷰 전환을 지원하는 브라우저에서도 trueskipTransition에 전달할 수 있습니다. 이 기능은 사이트에 전환을 사용 중지하려는 사용자 환경설정이 있는 경우 유용합니다.

프레임워크 작업

DOM 변경을 추상화하는 라이브러리나 프레임워크로 작업하는 경우, 까다로운 부분은 DOM 변경이 완료된 시점을 파악하는 것입니다. 다음은 다양한 프레임워크에서 위의 도우미를 사용한 일련의 예입니다.

  • 반응: 여기서 키는 flushSync이며 일련의 상태 변경사항을 동기식으로 적용합니다. 예, 이 API 사용에 대해 큰 경고가 표시되지만 댄 아브라모프는 이 경우에 적절하다고 확신합니다. React 및 비동기 코드의 평상시처럼 startViewTransition에서 반환된 다양한 프로미스를 사용할 때는 코드가 올바른 상태로 실행되고 있는지 주의해야 합니다.
  • Vue.js: 여기서 키는 nextTick이며 DOM이 업데이트되면 처리됩니다.
  • Svelte: Vue와 매우 비슷하지만 다음 변경을 기다리는 메서드는 tick입니다.
  • Lit: 여기서 핵심은 구성요소 내의 this.updateComplete 프로미스로, DOM이 업데이트되면 처리됩니다.
  • Angular: 여기서 키는 applicationRef.tick이며 대기 중인 DOM 변경사항을 플러시합니다. Angular 버전 17부터 @angular/router와 함께 제공되는 withViewTransitions를 사용할 수 있습니다.

API 참조

const viewTransition = document.startViewTransition(updateCallback)

새로운 ViewTransition를 시작합니다.

문서의 현재 상태가 캡처되면 updateCallback가 호출됩니다.

그런 다음 updateCallback에서 반환된 프로미스가 충족되면 다음 프레임에서 전환이 시작됩니다. updateCallback에서 반환된 프로미스가 거부되면 전환이 취소됩니다.

ViewTransition의 인스턴스 멤버:

viewTransition.updateCallbackDone

updateCallback에서 반환된 프로미스가 처리될 때 처리되고 거부될 때 거부되는 프로미스입니다.

View Transition API는 DOM 변경사항을 래핑하고 전환을 만듭니다. 그러나 때로는 전환 애니메이션의 성공/실패에는 신경 쓰지 않고 DOM 변경 발생 여부와 시점만 알고 싶을 때가 있습니다. updateCallbackDone는 이러한 사용 사례에 사용됩니다.

viewTransition.ready

전환을 위한 유사 요소가 생성되고 애니메이션이 곧 시작되면 충족되는 프로미스입니다.

전환을 시작할 수 없으면 거부됩니다. 이는 중복된 view-transition-name와 같은 잘못된 구성이거나 updateCallback가 거부된 프로미스를 반환하는 경우입니다.

이는 자바스크립트로 전환 의사 요소에 애니메이션을 적용하는 데 유용합니다.

viewTransition.finished

최종 상태가 완전히 표시되고 사용자에게 상호작용이 가능해지면 처리되는 promise입니다.

최종 상태가 생성되지 않았음을 나타내므로 updateCallback가 거부된 프로미스를 반환하는 경우에만 거부됩니다.

그렇지 않고 전환이 시작되지 않거나 전환 중에 건너뛰는 경우에도 종료 상태에 도달하여 finished이 처리됩니다.

viewTransition.skipTransition()

전환에서 애니메이션 부분을 건너뜁니다.

DOM 변경이 전환과 별개이므로 updateCallback 호출을 건너뛰지 않습니다.

기본 스타일 및 전환 참조

::view-transition
표시 영역을 채우고 각 ::view-transition-group을 포함하는 루트 의사 요소입니다.
::view-transition-group

위치를 확실히 지정했습니다.

'이전'과 '이후' 상태 간에 widthheight를 전환합니다.

'이전'과 '뒤' 표시 영역 공간 쿼드 간에 transform를 전환합니다.

::view-transition-image-pair

그룹을 채우기 위한 절대적 위치입니다.

plus-lighter 혼합 모드가 이전 뷰와 새 뷰에 미치는 영향을 제한하는 isolation: isolate가 있습니다.

::view-transition-new::view-transition-old

래퍼의 왼쪽 상단에 절대적으로 배치됩니다.

그룹 너비의 100% 를 채우지만 자동 높이를 사용하므로 그룹을 채우지 않고 가로 세로 비율을 유지합니다.

실제 크로스 페이드를 허용하는 mix-blend-mode: plus-lighter가 있습니다.

이전 뷰가 opacity: 1에서 opacity: 0로 전환됩니다. 새 뷰가 opacity: 0에서 opacity: 1로 전환됩니다.

의견

이 단계에서는 개발자의 의견이 매우 중요하므로 제안 및 질문을 포함하여 GitHub에서 문제를 제출해 주세요.