이번 편의 결과물: projects.html에서 전체·웹·앱 필터 버튼을 누르면 카드가 부드럽게 사라졌다 나타난다. document.startViewTransition을 지원하지 않으면 애니메이션 없이 즉시 바뀐다 · 다루는 개념: same-document 뷰 전환, document.startViewTransition, view-transition-name, @supports를 이용한 지원 여부 분기
이 편에서 만드는 파일
portfolio/
├── projects.html ~ 필터 버튼·카드에 data 속성 추가
├── css/
│ └── styles.css ~ 뷰 전환 관련 스타일 추가
└── js/
└── main.js ~ 필터 클릭 시 뷰 전환 로직 추가개념 정리
뷰 전환 API는 화면 상태가 바뀔 때 이전 모습을 캡처해 새 모습과 자연스럽게 섞어 주는 브라우저 기능입니다. 두 갈래가 있습니다.
| 종류 | 설명 | 2026-09 상태 |
|---|---|---|
| same-document 전환 | 같은 문서 안에서 DOM이 바뀔 때(document.startViewTransition) | 최신 브라우저 기준 널리 확산(2025-10 Baseline newly available, Firefox 144 합류) |
cross-document 전환(@view-transition) | 서로 다른 문서(페이지)로 이동할 때 | 제한적 지원(Firefox·구형 Safari 미구현) |
이 포트폴리오는 정적 3페이지라 실제 페이지 이동에는 cross-document 전환이 맞지만, 아직 Baseline에 못 미쳐 이 실습에서는 다루지 않습니다. 대신 projects.html 안에서 필터 버튼으로 보이는 카드가 바뀌는 상황에 same-document 전환을 적용합니다. 같은 문서 안에서 상태가 바뀐다는 점에서 “페이지가 바뀌는 것처럼 보이는” 효과를 그대로 재현할 수 있습니다.
document.startViewTransition 동작 순서
document.startViewTransition(콜백)을 호출한다.- 브라우저가 현재 화면을 스크린샷처럼 캡처한다.
- 콜백 안에서 실제 DOM을 바꾼다(카드 숨기기/보이기).
- 브라우저가 바뀐 후 화면을 다시 캡처한다.
- 두 캡처 사이를 기본적으로 크로스페이드로 이어 준다.
view-transition-name으로 요소 지정
기본 크로스페이드만으로도 효과가 있지만, 특정 요소가 “같은 것이 움직이며 바뀐다”는 느낌을 주려면 그 요소에 고유한 view-transition-name을 줍니다. 이름이 같은 요소는 이전 모습에서 새 모습으로 자연스럽게 보간됩니다.
.project-card {
view-transition-name: var(--card-transition-name, none);
}이 실습에서는 카드 전체 그리드에 하나의 전환만 걸어 복잡도를 낮추고, 개별 카드 보간은 다루지 않습니다.
기능 감지 두 단계
| 확인 대상 | 방법 |
|---|---|
| JS API 자체 | typeof document.startViewTransition === 'function' |
| CSS 쪽 스타일링 여지 | @supports (view-transition-name: none) |
두 확인은 목적이 다릅니다. JS 확인이 없으면 미지원 브라우저에서 함수 호출 자체가 에러를 냅니다. CSS @supports는 전환 중간 상태를 꾸미는 ::view-transition-* 관련 스타일을 지원 브라우저에만 적용해 불필요한 스타일이 낭비되지 않게 합니다.
실습
1. projects.html에 필터 버튼과 카드 속성 추가
카드 그리드 위에 필터 버튼 3개를 추가하고, 각 카드에 data-category를 붙입니다.
<!-- portfolio/projects.html (project-grid 위) -->
<div class="project-filter" role="group" aria-label="프로젝트 필터">
<button type="button" data-filter="all" aria-pressed="true">전체</button>
<button type="button" data-filter="web" aria-pressed="false">웹</button>
<button type="button" data-filter="app" aria-pressed="false">앱</button>
</div>
<div class="project-grid">
<article class="project-card" data-category="web">오늘의 날씨</article>
<article class="project-card" data-category="app">독서 기록 bookshelf</article>
<article class="project-card" data-category="app">메모 앱 notes</article>
<article class="project-card" data-category="web">접근성 체크리스트</article>
<article class="project-card" data-category="web">디자인 토큰 미리보기</article>
<article class="project-card" data-category="app">todo-cli</article>
</div>카드 내부(썸네일·제목·설명·태그·버튼)는 앞선 편에서 만든 마크업을 그대로 유지하고, 최상위 <article>에 data-category만 추가합니다.
2. main.js에 필터 로직 작성
// portfolio/js/main.js
const filterButtons = document.querySelectorAll('.project-filter button')
const cards = document.querySelectorAll('.project-card')
function applyFilter(filter) {
cards.forEach((card) => {
const match = filter === 'all' || card.dataset.category === filter
card.hidden = !match
})
filterButtons.forEach((btn) => {
btn.setAttribute('aria-pressed', String(btn.dataset.filter === filter))
})
}
filterButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const filter = btn.dataset.filter
if (typeof document.startViewTransition === 'function') {
document.startViewTransition(() => applyFilter(filter))
} else {
applyFilter(filter)
}
})
})startViewTransition을 지원하는 브라우저에서는 콜백 안에서 applyFilter가 실행되며 전환이 걸리고, 지원하지 않으면 곧바로 applyFilter만 실행되어 즉시 바뀝니다.
3. 전환 스타일과 폴백 지정
/* portfolio/css/styles.css (@layer components 안) */
.project-filter {
display: flex;
gap: var(--space-3);
margin-block-end: var(--space-6);
}
.project-filter button[aria-pressed='true'] {
background: var(--color-primary);
color: white;
}
@supports (view-transition-name: none) {
.project-grid {
view-transition-name: project-grid;
}
::view-transition-old(project-grid),
::view-transition-new(project-grid) {
animation-duration: 250ms;
}
}@supports (view-transition-name: none) 블록 안에만 전환 관련 이름·의사 요소 스타일을 넣었습니다. 이 속성 자체를 모르는 브라우저는 이 블록을 통째로 무시하므로 별도의 not 분기 없이도 안전합니다.
4. 실행
npx serve portfolioprojects.html을 열고 필터 버튼을 클릭합니다.
5. 확인
확인 1. 최신 Chrome·Edge에서 필터를 누르면 카드 그리드가 부드럽게 크로스페이드된다
확인 2. 개발자 도구 콘솔에서 typeof document.startViewTransition을 입력해 function이 나오는지 확인
확인 3. Firefox 144 미만 등 미지원 브라우저(또는 콘솔에서 document.startViewTransition = undefined로 강제)에서도 필터는 정상 동작하고 즉시 바뀐다
확인 4. 필터 버튼의 aria-pressed가 선택된 버튼에서만 true로 바뀐다직접 해보기
- 필터 전환 애니메이션 지속 시간을 250ms에서 400ms로 늘려 차이를 비교해 봅니다.
prefers-reduced-motion: reduce인 사용자에게는 애니메이션 지속 시간을0.01ms로 사실상 끄는 규칙을 추가해 봅니다.
정답 보기
/* portfolio/css/styles.css */
@media (prefers-reduced-motion: reduce) {
::view-transition-old(project-grid),
::view-transition-new(project-grid) {
animation-duration: 0.01ms;
}
}지속 시간을 0으로 두면 일부 브라우저에서 전환 자체가 건너뛰어질 수 있어, 관례적으로 0.01ms처럼 매우 짧은 값을 씁니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 구형 브라우저에서 필터 클릭 시 에러 발생 | document.startViewTransition을 감지 없이 바로 호출 | typeof document.startViewTransition === 'function' 확인 후 분기 |
| 전환이 걸려도 카드 내용이 즉시 바뀌어 보임 | DOM 변경 코드를 콜백 밖에서 먼저 실행 | applyFilter를 반드시 startViewTransition의 콜백 안에서 호출 |
::view-transition-old 스타일이 미지원 브라우저에서도 파싱 에러로 보임 | @supports 없이 최상위에 바로 작성 | @supports (view-transition-name: none) 블록 안으로 이동 |
| 필터 버튼 상태가 스크린 리더에 전달되지 않음 | aria-pressed를 갱신하지 않음 | applyFilter 안에서 모든 버튼의 aria-pressed를 다시 계산 |