본문으로 건너뛰기

© 2026 Molayo

CSS-T헤드라인2026. 05. 25. 22:56

문서 간 뷰 전환 (Cross-Document View Transitions): 수백 개의 요소로 확장하기

요약

문서 간 뷰 전환(Cross-Document View Transitions)을 수백 개의 요소로 확장할 때 발생하는 문제와 해결책을 다룹니다. 특히 CSS의 ident() 함수와 sibling-index()를 활용하여 대규모 목록에서도 고유한 식별자를 자동으로 생성하는 방법을 제안합니다.

핵심 포인트

  • 뷰 전환 시 발생하는 타임아웃 및 이미지 왜곡 문제 해결
  • ident() 함수를 통한 CSS 식별자 자동 생성 방식 소개
  • sibling-index()를 활용한 대규모 요소의 고유 이름 부여
  • 뷰 전환 외 scroll-timeline 등 다양한 CSS 속성 확장 가능성

Part 1에서 우리는 가장 먼저 맞닥뜨리게 되는 문제점들을 다루었습니다. 아무런 동작도 하지 않는 채 조용히 방치되는 지원 중단된 (deprecated) meta tag, 아무런 알림 없이 전환을 중단시켜 버리는 4초 타임아웃, 모든 종횡비(aspect ratio) 변화를 슬라임처럼 뭉개버리는 이미지 왜곡, 그리고 전환 라이프사이클(transition lifecycle)에 대한 훅(hook)을 제공하는 pagereveal/pageswap 이벤트 등이 그것입니다.

이 모든 과정을 거치면 "아무것도 작동하지 않는 상태"에서 "두 페이지 사이에서 하나의 요소가 멋지게 전환되는 상태"로 나아갈 수 있습니다. 기분이 아주 좋겠죠. 한 5분 정도는 말입니다. 그러다 각 카드가 상세 보기(detail view)로 변해야 하는 48개의 카드가 있는 상품 목록 페이지를 구축하려고 시도하는 순간, 튜토리얼들이 가장 어려운 부분을 빠뜨렸다는 사실을 깨닫게 됩니다.

이제 진짜 시작입니다. 이것을 확장해 봅시다.

문서 간 뷰 전환 (Cross-Document View Transitions) 시리즈

  1. 아무도 언급하지 않는 문제점들 (The Gotchas Nobody Mentions)
  2. 수백 개의 요소로 뷰 전환 확장하기 (Scaling View Transitions Across Hundreds of Elements) _(현재 위치!)

꿈의 구현: 한 줄의 코드로 무한한 이름 생성

완벽한 세상이라면, 여러분은 순수 CSS만으로 확장성 문제를 해결할 수 있을 것입니다. JavaScript도 필요 없고, 서버 측 루프(server-side loops)도 필요 없습니다. 그저 다음과 같이 작성하면 됩니다:

.card {
  /* card-1, card-2, card-3 등을 자동으로 생성합니다 */
  view-transition-name: ident("card-" sibling-index());
...

이것은 ident()입니다. 이는 Chrome에서 근무하는 Bramus가 CSS 작업 그룹(CSS Working Group)에 제안한 CSS 함수입니다. 이 함수는 문자열, 정수 또는 기타 식별자(identifiers)를 입력받아 이를 결합하여 유효한 CSS 이름을 출력합니다. 이를 요소의 형제 요소들 사이에서의 위치(1, 2, 3...)를 반환하는 sibling-index()와 결합하면, 목록 내 모든 요소에 대해 자동으로 생성된 고유한 이름을 얻을 수 있습니다. 단 하나의 규칙만으로 10개의 카드든 10,000개의 카드든 상관없이 작동합니다. CSS는 개의 많고 적음을 신경 쓰지 않습니다.

그리고 이것은 단지 뷰 전환 (view transitions)에만 국한되지 않습니다. 동일한 패턴은 scroll-timeline-name, container-name, view-timeline-name 등 대규모 환경에서 고유 식별자 (unique identifiers)가 필요한 모든 곳에 적용됩니다. 심지어 sibling-index() 대신 attr()을 사용하여 HTML 속성에서 이름을 가져와 ident("--item-" attr(id) "-tl")와 같은 식별자를 구성할 수도 있습니다. 이러한 유연성은 실질적입니다.

여기서 중요한 점은 이 방정식의 절반이 이미 존재한다는 것입니다. sibling-index()는 Chrome 138에 탑재되었으며, 현재 시차 애니메이션 (staggered animations)이나 계산된 스타일 (calculated styles) 등에 바로 사용할 수 있습니다. 부족한 조각은 ident()입니다. 2025년 5월의 Chrome Intent to Prototype이 있으므로, 이는 이미 고려 대상 (on the radar)에 올라와 있습니다. 하지만 "고려 대상"이라는 것과 "브라우저에 탑재되었다"는 것은 매우 다른 이야기입니다. 아직 어떤 브라우저도 ident()를 탑재하지 않았으며, 언제 출시될지에 대한 타임라인도 없습니다.

따라서 우리는 아직 이를 사용할 수 없습니다. 하지만 ident()가 출시되면 여러분이 곧 보게 될 복잡성의 거대한 부분이 그냥... 증발해 버리기 때문에 미리 알아둘 가치가 있습니다. 그때까지는 현재 브라우저에 실제로 존재하는 도구들을 사용하여 오늘날 이 문제를 효율적으로 해결하는 방법을 알아보겠습니다.

100개의 제품, 100개의 이름, 1개의 악몽

두 페이지 사이에서 하나의 히어로 이미지 (hero image)가 전환되는 모습을 보여주는 튜토리얼을 따라 한 뒤, 그 패턴을 그리드 (grid)에 적용하려고 할 때 어떤 일이 발생하는지 살펴보겠습니다:

/*  악몽 - 아이템당 하나의 규칙, 영원히 */
::view-transition-group(card-1),
::view-transition-group(card-2),
...

이것이 바로 이름이 지정된 요소가 하나 또는 두 개뿐인 튜토리얼을 따랐을 때 마주하게 되는 결과입니다. 그들은 하나의 이미지에 view-transition-name: hero를 할당하고 상황을 종료합니다. 멋지죠. 이제 제품 그리드를 구축해 보세요.

페이지 내의 모든 view-transition-name은 고유해야 합니다. 이는 엄격한 규칙입니다. 만약 두 요소가 동일한 이름을 공유하면, 브라우저는 다음 페이지의 어떤 요소가 어느 요소와 매핑되는지 알 수 없으므로 전체 전환(transition)을 취소해 버립니다. 48개의 제품이 있는 목록 페이지라면 48개의 고유한 이름이 필요합니다. 200개의 썸네일이 있는 사진 갤러리라면 200개가 필요합니다. 이름 자체가 문제는 아닙니다. 이름은 생성할 수 있으니까요. 문제는 CSS의 모든 의사 요소(pseudo-element) 선택자가 _특정 이름_을 대상으로 한다는 점입니다. 이로 인해 애니메이션 스타일이 관리할 수 없을 정도로 방대한 선택자의 벽으로 변해버립니다.

이 지점에서 여러분은 비슷해 보이지만 실제로는 완전히 다른 두 속성의 차이점을 이해해야 합니다.

Name vs. Class: 모든 것을 바꾸는 차이점

네, 여기서 명칭이 혼란스러운 것은 사실입니다. 솔직히 말씀드리면, 저도 처음 view-transition-nameview-transition-class를 나란히 보았을 때 서로 바꿔 쓸 수 있는 것이라고 생각했습니다. 하지만 그렇지 않으며

데이터베이스를 생각해보세요. name은 기본 키 (Primary Key)입니다. 고유하며, 특정 행 하나를 식별합니다. class는 카테고리 컬럼입니다. 행들을 그룹화하여 한 번에 모든 행에 대해 쿼리 (Query)를 실행할 수 있게 해줍니다.

실제 사례는 다음과 같습니다:

CodePen Embed Fallback

보시다시피, 6개의 카드와 6개의 고유한 이름이 있지만, 모든 애니메이션 동작을 처리하는 CSS 규칙은 정확히 3개뿐입니다. 카드가 60개여도, 600개여도 상관없습니다. CSS는 변하지 않습니다.

핵심 라인은 바로 이 선택자입니다: ::view-transition-group(*.card). 별표(*)는 이름에 대한 와일드카드 (Wildcard)이며, .cardview-transition-class와 일치합니다. 이는 "특정 이름이 무엇이든 관계없이, 요소가 view-transition-class: card를 가진 모든 뷰 전환 그룹 (View Transition Group)"이라고 읽힙니다.

문서 간 멀티 페이지 애플리케이션 (MPA) 전환의 경우, 패턴은 동일하지만 서버에서 이름을 생성합니다:

<!-- Page A -->
<div class="grid">

...
<!-- Page B -->
<div
  class="product-hero"
...
/* 모든 페이지에서 공유되는 단 하나의 스타일시트가 모든 제품을 처리합니다 */
@view-transition {
  navigation: auto;
...

이것이 수천 개의 제품을 가진 사이트를 위한 애니메이션 스타일시트의 전부입니다. 규칙은 단 3개입니다. 데이터베이스에 아이템이 아무리 많아도, 전환 (Transition) CSS를 한 줄도 더 추가할 필요가 없습니다.

view-transition-class가 존재하기 전에는 사람들이 끔찍한 일들을 저질렀습니다. JavaScript로 아이템을 루프 (Loop) 돌며 수백 개의 선택자가 포함된 <style> 블록을 생성하거나, CSS 프리프로세서 (Preprocessor)를 사용하여 빌드 타임 (Build time)에 가능한 모든 이름의 순열을 쏟아내곤 했습니다. 기술적으로는 작동했습니다. 마치 자동차 범퍼를 덕테이프로 붙여놓은 것이 기술적으로 작동하는 것과 마찬가지입니다.

view-transition-class는 기존 API가 확장성 (Scale)을 갖추지 못했다는 점을 명세 (Spec) 작성자들이 인정하고, 이를 올바른 방식으로 수정한 결과입니다.

한 가지 주의할 점(gotcha)은, 바로 이러한 확장성 (Scaling) 문제를 해결하기 위해 view-transition-class가 나중에 명세 (Spec)에 추가되었다는 것입니다. 이 속성은 Chrome 125에 도입되었으며, 현재 Chrome, Edge, 그리고 Safari 18.2+ 버전에서 지원됩니다. 이전 버전의 Chromium 및 Firefox는 아직 이를 인식하지 못합니다. 이 경우 전환 (Transitions) 기능 자체는 여전히 _작동_하지만, 사용자가 설정한 커스텀 타이밍 대신 기본 페이드 애니메이션 (Default fade animation)을 사용하게 됩니다. 아주 나쁜 폴백 (Fallback)은 아닙니다.

또한 일반적인 CSS 클래스처럼 단일 요소에 여러 클래스를 할당할 수도 있습니다. view-transition-class: card featured와 같은 방식은 유효하며, ::view-transition-group(*.card) 또는 ::view-transition-group(*.featured) 중 하나로 타겟팅할 수 있습니다. 대부분의 제품은 동일한 방식으로 전환되길 원하지만, 몇몇 제품은 다른 애니메이션 스타일로 눈에 띄게 만들고 싶을 때 매우 유용합니다.

모든 것에 미리 이름을 붙이지 마세요

지금까지 살펴본 모든 사례는 페이지가 로드되는 순간부터 HTML이나 CSS에 view-transition-name이 바로 자리 잡고 있었습니다. 그렇게 해도 작동은 합니다. 하지만 실제 규모 (Scale)의 환경에 도달하기 전까지는 명확히 드러나지 않는 비용이 따릅니다.

CodePen 임베드 폴백 (CodePen Embed Fallback)

두 페이지의 CSS를 살펴보세요. view-transition-name 선언이 전혀 없습니다. 단 하나도 없습니다. 그리드 내의 모든 카드는 사용자가 하나를 클릭하는 바로 그 순간까지 익명 (Anonymous) 상태입니다.

이것이 왜 중요한지 설명하겠습니다. 스타일시트의 요소에 view-transition-name을 지정하면 — 즉, 페이지 로드 시점부터 CSS에 그대로 할당해 두면 — 브라우저에게 "이 요소는 이 페이지에서 발생하는 모든 전환에 참여한다"라고 말하는 것과 같습니다. 모든 내비게이션 (Navigation)마다 말이죠. 브라우저는 해당 요소를 스냅샷 (Snapshot) 찍고, 위치를 계산하며, 이를 위한 의사 요소 트리 (Pseudo-element tree)를 설정해야 합니다. 히어로 이미지 (Hero image) 하나라면 상관없습니다. 하지만 48개의 제품 카드가 있는 그리드라면, 사용자가 단 _하나_를 클릭했을 뿐인데 48개의 요소가 개별적으로 캡처되고, 차이점(diff)이 계산되며, 애니메이션이 적용됩니다. 나머지 47개의 스냅샷은 순전한 낭비입니다.

성능이 좋은 기기에서는 눈치채지 못할 수도 있습니다. 하지만 LTE 환경에서 제품 이미지 그리드를 불러오는 중급 사양의 Android 폰이라면 어떨까요? 성능 저하를 체감하게 될 것입니다. 전환 애니메이션이 끊기거나, 브라우저가 모든 설정을 충분히 빠르게 완료하지 못해 애니메이션을 아예 건너뛰어 버릴 수도 있습니다.

해결책은 view-transition-name을 '적시(just-in-time)' 방식으로 처리하는 것입니다. 페이지 로드 시점이 아니라, 상호작용이 일어나는 순간에 이름을 할당하세요.

생명주기(lifecycle)는 다음과 같이 진행됩니다:

  1. 사용자가 목록 페이지의 카드를 클릭합니다.
  2. 브라우저가 탐색(navigating)을 시작하며, 이전 페이지에서 pageswap 이벤트가 발생합니다.
  3. pageswap 핸들러가 event.activation.entry.url을 확인하여 사용자가 어디로 가는지 파악한 뒤, 클릭된 카드를 찾아 view-transition-name: product-42를 부여합니다.
  4. 브라우저가 이름이 지정된 해당 요소(및 기본 root 전환)를 스냅샷(snapshot)으로 찍습니다.
  5. 탐색이 수행되고 새 페이지가 로드됩니다.
  6. 들어오는 페이지에서 pagereveal 이벤트가 발생합니다.
  7. pagereveal 핸들러가 URL을 읽고, 히어로 요소(hero element)를 찾아 일치하는 view-transition-name: product-42를 할당합니다.
  8. 브라우저가 이전 스냅샷과 새 스냅샷에서 일치하는 이름을 확인하고, 두 요소 사이를 모핑(morph)합니다.
  9. 전환이 완료되면 .finished 프로미스(promise)가 해결(resolve)되고, 할당했던 이름들을 제거합니다.

이것이 전부입니다. 하나의 요소에 이름을 지정하고, 하나의 요소가 전환되며, 낭비는 전혀 없습니다.

여기서 event.activation 객체는 여러분의 가장 친한 친구가 되어줄 것입니다. 나가는 페이지(outgoing page)에서는 event.activation.entry.url이 탐색이 향하는 목적지를 알려줍니다. 들어오는 페이지(incoming page)에서는 단순히 window.location을 읽으면 됩니다. 이 두 가지를 활용하면 전역 상태(global state)나 sessionStorage를 이용한 트릭, 혹은 앱에서 이미 사용 중인 것 이상의 복잡한 쿼리 파라미터(query parameter) 조작 없이도 어떤 요소에 이름을 붙여야 할지 파악하는 데 필요한 모든 정보를 얻을 수 있습니다.

그리고 그 정리(cleanup) 단계, 즉 .finished 리졸브(resolves) 이후에 이름을 제거하는 것에 대해 말하자면, 이는 단순히 깔끔함을 위한 것이 아닙니다. 만약 사용자가 목록 페이지로 돌아가서 다른 카드를 클릭한다면, 이전 전환(transition)에서 사용했던 이름이 이전 카드에 여전히 남아있는 것을 원치 않을 것입니다. 오래된 이름(Stale names)은 중복 이름 충돌(duplicate-name conflicts, 즉 즉각적인 전환 실패)을 일으키거나 잘못된 요소 매칭(wrong-element matching, 즉 새 페이지가 잘못된 카드로부터 모핑되는 현상)을 유발합니다. 뒷정리를 확실히 하세요.

이 패턴은 기본적으로 Astro의 transition:name 디렉티브(directive)가 내부적으로 수행하는 방식과 같습니다. Nuxt의 뷰 전환 지원도 마찬가지입니다. 이들은 내비게이션 생명주기(navigation lifecycle)에 맞춰 이름을 동적으로 할당하고 제거합니다. 프레임워크들은 단지 컴포넌트 속성(attribute) 뒤로 pageswap/pagereveal 연결 로직을 숨겨두었을 뿐입니다. 여러분도 동일한 작업을 하고 있는 것이며, 단지 추상화 계층(abstraction layer)이 없을 뿐입니다. 움직이는 부품은 더 적지만, 결과는 같습니다.

실제 콘텐츠를 위한 실용적인 패턴

제품 그리드 예제는 가장 흔한 사례를 다루지만, 실제 현장에서 마주하게 될 몇 가지 다른 패턴들을 살펴보겠습니다.

다양한 종횡비를 가진 사진 갤러리

갤러리는 까다롭습니다. 모든 썸네일(thumbnail)이 서로 다른 종횡비(aspect ratio)를 가질 수 있고, 전체 크기 뷰는 확실히 다를 것이기 때문입니다. 파트 1 기사에서 다룬 taffy 수정 방식이 여기서 필수적이지만, 전환(transition)이 혼란스럽기보다는 의도된 것처럼 느껴지게 만드는 것도 중요합니다.

/* 갤러리 아이템은 타겟팅된 애니메이션을 위해 자체 클래스를 가집니다 */
::view-transition-group(*.gallery-item) {
  animation-duration: 0.5s;
...

갤러리의 핵심 비결은 주변의 카드나 컨테이너가 아닌, <img> 태그 자체에 view-transition-name을 할당하는 것입니다. 브라우저가 카드의 배경, 패딩(padding), 캡션(caption)과 함께 모핑되는 것이 아니라, 이미지만을 썸네일 크기에서 라이트박스(lightbox) 크기로 모핑하도록 만들어야 합니다. 이미지에 이름을 붙이세요. 카드는 스타일링하세요. 이 둘을 분리해서 유지하세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 CSS-Tricks의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0