문제
웹사이트에서는 객체의 여러 뷰를 제공하는 경우가 많습니다. 예를 들어 제품 목록을 제공한 다음 각 제품의 세부정보 페이지를 제공합니다. 두 뷰 사이를 탐색할 때 연결이 끊어진 것처럼 느껴지는 경우가 많습니다. 사용자가 제품 썸네일을 클릭하여 세부정보를 확인하면 썸네일이 사라지고 화면의 다른 위치에 더 큰 새 이미지가 즉시 표시됩니다. 이러한 연속성 부족으로 인해 사용자가 요소 간의 관계를 추적하기가 더 어려워집니다.
해결 방법
View Transitions API를 사용하면 전환 전후에 서로 다른 상태로 존재하는 요소 쌍을 지정할 수 있습니다. 단일 페이지 탐색 (SPA)에서 document.startViewTransition()를 사용하여 전환을 트리거하면 브라우저는 공유된 고유한 view-transition-name를 통해 이러한 공유 요소를 식별합니다. 그런 다음 위치, 크기, 스타일의 차이를 자동으로 계산하고 이전 상태에서 새 상태로 부드럽게 애니메이션을 적용합니다. 이 전환은 z-index 값이 높은 요소보다도 위에 있는 최상위 레이어에서 발생합니다.
구현 가이드
1단계: startViewTransition에서 상태 변경 래핑
싱글 페이지 애플리케이션 (SPA) 또는 간단한 상태 변경의 경우 DOM을 업데이트하는 로직을 document.startViewTransition로 래핑합니다. 브라우저는 현재 상태의 스냅샷을 캡처하고 업데이트를 실행한 다음 새 상태를 캡처합니다.
function navigate(view) {
// MANDATORY: Wrap the update in startViewTransition
document.startViewTransition(() => updateDOM(view));
}
2단계: 공유 전환 이름 할당
view-transition-name CSS 속성을 사용하여 브라우저에 변형할 요소를 알려줍니다. 이름은 none를 제외하고 무엇이든 가능합니다. 필수: 지정된 view-transition-name 앞뒤에 요소가 1개 이하여야 합니다. 지정된 view-transition-name가 있는 요소가 2개 이상인 경우 DOM은 전환 없이 즉시 새 상태로 업데이트됩니다.
여러 view-transition-name를 사용하여 여러 요소 쌍을 변형할 수 있습니다. 예를 들어 제품 이미지와 제목을 별도의 전환으로 전환할 수 있습니다.
목록 보기에는 여러 항목이 있으므로 모든 항목에 동일한 view-transition-name를 지정할 수 없습니다. 이 문제는 SPA에서 두 가지 방법으로 해결할 수 있습니다.
동적 세부정보 페이지: 목록 페이지의 각 항목에 고유한
view-transition-name를 할당한 다음, 목록 항목을 선택할 때 세부정보 페이지의 일치하는 요소에 해당 이름을 동적으로 적용합니다(여기에 표시된 대로)./* In the list view, give each */ #product-1 { view-transition-name: p1 } #product-2 { view-transition-name: p2 } #product-3 { view-transition-name: p3 }function updateDOM(clickedTransitionName){ const hero = document.getElementById("hero"); hero.style.viewTransitionName = clickedTransitionName; }동적 목록 항목: 세부정보 페이지의 요소에
view-transition-name를 할당하고, 목록 페이지의 항목이 선택되면 해당 이름을 적용합니다. 목록 페이지로 돌아갈 때 목록 페이지의 항목에서view-transition-name를 삭제합니다.
세부정보 페이지의 #hero 요소와 목록 페이지의 선택된 .thumbnail 요소는 view-transition-name를 공유합니다.
#hero{
view-transition-name: hero;
}
.thumbnail.selected {
view-transition-name: hero;
}
썸네일을 클릭하면 .selected 클래스 선택기를 사용하여 view-transition-name를 할당하고 전환을 시작하기 전에 DOM을 변경하여 목록 보기를 준비해야 합니다.
그런 다음 document.startViewTransition()를 호출하고 변경사항을 적용하여 페이지를 세부정보 보기에서 목록 보기로 전환할 수 있습니다.
목록 보기로 다시 이동한 후에는 다음 탐색에서 오류가 발생하지 않도록 뷰 전환 클래스를 정리해야 합니다. 전환의 finished 프로미스가 해결된 후 이 정리를 실행할 수 있습니다.
// Function called when a thumbnail is clicked
function goFromListToDetail(e){
e.currentTarget.classList.add("selected");
const hero = document.getElementById("hero");
const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
hero.style.background = bgColor;
// Trigger the transition, checking for support
if (!document.startViewTransition) {
document.body.classList.add("detail");
// MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
document.getElementById("detail-heading")?.focus();
return; // MANDATORY: End function execution if view transitions are not supported.
}
const transition = document.startViewTransition(() => {
document.body.classList.add("detail");
});
// MANDATORY Accessibility Routing: Route focus after the view transition resolves
transition.finished.finally(() => {
document.getElementById("detail-heading")?.focus();
});
}
// Function called when navigating from detail back to list view
function goFromDetailToList() {
if (!document.startViewTransition) {
document.body.classList.remove("detail");
document.getElementById("list-heading")?.focus();
return;
}
const transition = document.startViewTransition(() => {
document.body.classList.remove("detail");
});
// Clean up the list view and route focus
transition.finished.finally(() => {
// Route focus back to list view
document.getElementById("list-heading")?.focus();
// Remove selected classList to remove view-transition-names
document.querySelectorAll(".selected").forEach(
(element) => {
element.classList.remove("selected");
},
);
});
}
선택하는 방법은 사용 사례에 따라 다릅니다. 동적 목록 항목은 반복되는 CSS가 적지만 수동 JavaScript 정리가 더 많이 필요합니다.
3단계: 가로세로 비율 '늘이기' 수정
기본적으로 브라우저는 두 스냅샷에 맞게 확장되는 그룹 내에서 이전 스냅샷과 새 스냅샷을 크로스페이드합니다. 텍스트를 전환하는 경우 전환된 요소의 가로세로 비율이 안정적이도록 이전 뷰와 새 뷰 모두에서 텍스트 요소의 너비를 fit-content로 설정합니다.
#list-page .title {
width: fit-content;
}
#detail-page #title {
width: fit-content;
}
가로세로 비율이 변경되는 요소를 전환하는 경우 이전 및 새 가상 요소의 높이를 ::view-transition-pair() 가상 요소의 100% 로 설정해야 할 수 있습니다.
::view-transition-old(hero),
::view-transition-new(hero){
height: 100%;
}
의사 요소는 라이브 요소의 스냅샷이므로 object-fit 및 object-position 선언을 사용하여 전환 효과를 더 세밀하게 제어할 수도 있습니다.
권장사항
- 전환을 너무 많이 지정하지 마세요. 사용자가 적극적으로 추적하는 기본 콘텐츠 (예: 히어로 이미지, 제목)에만 공유 요소를 사용합니다.
- 권장사항: 전환이 완료된 후 임시
view-transition-name값을 삭제하여 향후 전환에 부작용이 발생하지 않도록 합니다. - 활성 애니메이션이 있는 요소는 전환하지 마세요. 뷰 전환은 스냅샷에서 작동하므로 뷰 전환 중에 애니메이션이 일시중지된 것처럼 보입니다.
prefers-reduced-motion미디어 쿼리를 사용하여 모션 감소에 대한 사용자 환경설정을 존중해야 합니다(DO).- 필수 접근성 라우팅: 뷰 전환은 페이지 레이아웃을 동적으로 변형하지만 프로그래매틱 포커스를 관리하지는 않습니다. 전환 중에 숨겨지거나 삭제된 요소에 포커스가 남아 있으면 포커스가 사라져 키보드 및 보조 기술 사용자에게 컨텍스트가 제공되지 않습니다. DOM 업데이트 직후 또는 뷰 전환의
finished프로미스가 해결될 때 업데이트된 페이지 제목 또는 뷰 컨테이너로 프로그래매틱 방식으로 포커스를 이동합니다(tabindex="-1"사용).
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
대체 전략
View Transitions API는 점진적 개선을 위해 설계되었습니다. 이를 지원하지 않는 브라우저는 애니메이션 없이 DOM 업데이트를 즉시 실행합니다.
function navigate(){
if (!document.startViewTransition) {
// Fallback: Just update the DOM
updateDOM();
} else {
document.startViewTransition(() => updateDOM());
}
}