현대의 웹사이트들은 점점 더 웅장해지는 비주얼로 사용자의 주의를 끌기 위해 경쟁합니다. 패럴랙스 히어로(Parallax heroes) 섹션은 뷰포트 깊숙이 펼쳐집니다. 캐러셀(Carousels)은 끝없이 스크롤됩니다. 배경 비디오는 가입 양식 뒤에서 자동으로 재생됩니다. 디자인 포트폴리오에서는 이러한 패턴이 세련되고 매력적으로 보입니다. 하지만 사용자 중 일부에게 이것은 신체적 자극(trigger)이 됩니다.
전정 기관 장애(vestibular disorders)를 앓고 있는 사람들은 지속적인 움직임이 큰 영역에 노출될 때 메스꺼움, 어지러움 또는 편두통을 경험합니다. 이들을 보호하기 위해, 이제 모든 주요 운영 체제에는 "동작 줄이기(Reduce Motion)" 접근성 설정이 포함되어 있습니다. 웹 브라우저는 prefers-reduced-motion 미디어 쿼리를 통해 해당 설정을 노출합니다. 이를 깔끔하게 반영하고자 하는 React 개발자는 @reactuses/core의 useReducedMotion 훅을 사용할 수 있습니다. 이 훅은 OS 설정을 불리언(boolean) 값으로 읽어오고, 사용자가 설정을 변경하면 실시간으로 업데이트하며, 서버 사이드 렌더링(SSR) 시에도 오류 없이 작동합니다.
직접 만든 해결책이 실패하는 이유
window.matchMedia('(prefers-reduced-motion: reduce)')를 사용하여 직접 설정을 조회할 수도 있습니다. 많은 개발자가 그렇게 하지만, 그 과정에서 미묘한 버그가 발생하곤 합니다.
첫째, 변경 사항을 감지하는 리스너(listener)를 쉽게 잊어버릴 수 있습니다. 초기 쿼리는 컴포넌트가 마운트될 때 한 번 실행됩니다. 만약 사용자가 앱을 열었다가, 캐러셀 때문에 속이 좋지 않아 시스템 설정에서 '동작 줄이기'를 켰다면, 여러분의 컴포넌트는 그 업데이트를 전혀 감지하지 못합니다. 미디어 쿼리 객체는 addEventListener를 지원하지만, 이를 올바르게 연결하고, 언마운트(unmount) 시 제거하며, 레거시 addListener 구문을 폴리필(polyfill)하는 작업은 복사-붙여넣기 실수를 유발하기 쉬운 번거로운 보일러플레이트(boilerplate) 코드입니다.
둘째, SSR 중에는 window가 존재하지 않습니다. 앱이 서버에서 렌더링되는 경우, 단순하게 matchMedia를 호출하면 참조 오류(reference error)가 발생하여 렌더링이 중단됩니다. 결국 typeof window !== 'undefined' 가드로 체크 로직을 감싸고, 기본값을 추측하며, 클라이언트 하이드레이션(hydration)이 일치하기만을 바라게 됩니다.
셋째, 모든 애니메이션 컴포넌트에서 해당 로직을 반복하는 것은 유지보수 부채가 됩니다. 어떤 팀원은 리스너를 한 방식으로 작성하고, 다른 팀원은 아예 생략하며, 또 다른 팀원은 움직임을 선호하는 기본값을 하드코딩할 수 있습니다. 훅(hook)을 사용하면 이러한 혼란을 중앙 집중화할 수 있습니다.
useReducedMotion은 이 세 가지 문제를 모두 해결합니다. 단순한 불리언 값을 반환하며, 서버에서는 안전하게 아무 작업도 수행하지 않습니다(no-ops). 또한 리스너를 자동으로 연결하고 정리합니다.
활용하는 세 가지 방법
불리언 값을 얻었다면, 이를 바탕으로 동작할 전략이 필요합니다. 단일 컴포넌트부터 전체 애플리케이션까지 확장 가능한 세 가지 패턴을 소개합니다.
1. 불필요한 작업을 피하기 위해 CSS 클래스 제어하기
가장 직접적인 접근 방식은 무거운 JavaScript 애니메이션 루프가 시작조차 되지 않도록 방지하는 것입니다. 평소 스크롤 이벤트를 감지하고 requestAnimationFrame 루프 내에서 레이어를 이동시키는 패럴랙스 이미지 컴포넌트가 있다고 가정해 봅시다. 해당 로직을 실행한 뒤 시각적 결과만 억제하는 대신, 먼저 useReducedMotion을 확인하십시오.
훅이 true를 반환하면, 정적 변형(static variant) 클래스로 컴포넌트를 렌더링하고 스크롤 리스너를 바인딩하는 useEffect를 건너뜁니다. 브라우저의 작업량이 줄어듭니다. 움직임에 민감한 사용자는 고정된 이미지를 보게 되고, 그 외의 사용자들은 움직이는 레이어를 보게 됩니다. 애니메이션 코드가 아예 초기화되지 않으므로 CPU와 배터리도 절약할 수 있습니다.
2. 애니메이션 라이브러리에 직접 전달하기
Framer Motion과 같은 라이브러리를 사용한다면, 이 훅을 프롭(prop) 정의에 바로 연결할 수 있습니다. Motion 컴포넌트는 변형(variants), 전환(transitions), 제스처(gestures)를 위한 설정 객체를 허용합니다. useReducedMotion에서 얻은 불리언 값을 사용하여 전환 시 조건부로 duration: 0을 설정하거나,
