정의
사용자가 OS에서 “움직임 줄이기”를 켰을 때, 자바스크립트로 돌리는 페이지 전환 애니메이션을 앱이 직접 끄는 기법이다.
prefers-reduced-motion: reduce 는 그 설정을 브라우저가 알려 주는 CSS 미디어 쿼리다. CSS transition/animation은 토큰으로 duration을 거의 0으로 줄일 수 있지만, JS 스프링·MutationObserver 기반 라이브러리는 그 CSS 변수를 읽지 않는다. 그래서 matchMedia로 구독하고, reduce면 전환 Provider를 마운트하지 않거나 transitions를 비운다.
왜 필요한가
접근성: 전정기관 민감·주의산만 사용자에게 강제 모션은 해롭다. 품질: 시각 회귀(VR)·axe 스모크는 종종 reduced motion을 강제하는데, JS 전환이 돌면 중간 프레임 스냅샷으로 flake가 난다. 성능: 가상 리스트처럼 DOM이 자주 바뀌는 페이지에서 전환용 Observer가 스크롤 중 mutation을 구독하면 핫패스 세금이 된다.
동작 원리
window.matchMedia('(prefers-reduced-motion: reduce)')구독.- React에서는
useSyncExternalStore로 SSR-safe하게 읽는다 — 서버/getServerSnapshot은 보통false(모션 허용), 클라이언트 하이드레이션 후 실제 값으로 보정. 설정은 애니메이션만 바꾸고 DOM 텍스트를 안 바꾸면 hydration mismatch가 없다. - reduce이면 (a) transitions 배열을 비우거나 (b) Provider 자체를 렌더하지 않는다 — (b)가 Observer 비용까지 제거한다.
- 페이지 전환 설정에서 scroll을 보존·조작하지 않음(
preserveScroll: false등). 스크롤 복원은 Next·가상 리스트 권한으로 남긴다.
실무 적용
- 사이트 크롬(헤더/푸터)은 전환 경계 밖에 두어 라우트마다 다시 애니메이트되지 않게 한다.
- Playwright VR/a11y 프로젝트에
reducedMotion: 'reduce'를 유지해 회귀를 고정한다. - 리스트↔상세에 기하학적 drill 전환을 넣을 때는 스크롤 복원 e2e를 먼저 통과시킨 뒤 교체한다.
- CSS View Transitions를 쓰는 경우에도 reduced motion에서 animation-duration을 0으로 두는 글로벌 규칙을 함께 둔다.
트레이드오프
| 선택 | 장점 | 비용 |
|---|---|---|
| Provider 미마운트 | Observer·스프링 비용 제거 | 분기 코드·테스트 경로 증가 |
| transitions만 빈 배열 | API 단순 | 라이브러리가 여전히 마운트될 수 있음 |
| CSS만 리셋 | 구현 쉬움 | JS 전환에 무효 |
사용하면 안 되는 경우
- 전환 라이브러리에게 scroll restoration까지 맡기고 가상 리스트와 동시에 쓰는 경우(권한 충돌).
- reduced motion 사용자를 위해 “대체 정보”가 필요한데 모션만 끄고 상태 변화를 안 알리는 경우(모션 ≠ 정보 전달).
흔한 실수
tokens.css에--duration: 1ms만 넣고 JS 라이브러리는 그대로 둠.- SSR에서
window.matchMedia를 직접 호출해 서버 크래시. - 전환 레이어
key={pathname}과 리스트 scroll 복원이 경쟁.
관련 개념
- usesyncexternalstore-snapshot-identity — 외부 스토어·미디어 구독의 스냅샷 규칙
- virtual-list-scroll-restore-authority — 스크롤 단일 권한
- css-view-timeline-entry-animation — CSS 스크롤/뷰 타임라인 모션