Bendd 웹앱에는 다양한 애니메이션이 적용되어 있습니다. 애니메이션을 개발할 때 절대 잊지 말아야 할 사실이 있는데요. 일부 사용자에게 멀미를 유발할 수 있는 x/y 애니메이션을 opacity로 바꾸거나, 배경 동영상의 자동 재생을 비활성화하거나, 시차 모션을 끄는 등의 애니메이션을 제어할 수 있도록 제공해야 합니다.
그럼 무엇을 기준으로 애니메이션을 비활성화해야 할까요? 다행히 모든 주요 OS에서는 접근성 설정에서 “모션 감소” 설정을 사용할 수 있습니다. 이 설정을 활성화하면 모든 애니메이션(ex: MacOS에서 요술램프 지니 최소화 효과)을 비활성화합니다. Apple은 미디어 쿼리인 prefers-reduced-motion 을 사용해 브라우저에 해당 설정을 노출하기 시작했습니다. 이렇게 하면 웹사이트에서 이 미디어 쿼리를 이용해 명시적으로 애니메이션을 비활성화할 수 있습니다.
보통 전역 스타일로 아래와 같이 구성할 수 있습니다:
이 코드만으로 모든 애니메이션의 동작이 줄어들면 좋겠지만, 아쉽게도 애니메이션이 전적으로 CSS로 동작(eg. transitions, keyframe animations)할 때만 잘 실행됩니다. 커서 좌표에 따른 애니메이션 등 CSS를 통해서만 애니메이션을 만들 수 없는 유형은 JS에서 애니메이션을 실행하는데 이 경우엔 의도한 대로 동작하지 않습니다.
이 경우, 사용자의 “모션 감소” 설정 여부를 파악해서 직접 로직에서 애니메이션이 실행되지 않도록 해야 합니다. 그래서 사용자가 OS에서 “모션 감소” 체크박스를 토글하면 콜백 함수를 실행해 React 라이프사이클에 연결하는 훅을 구현했습니다:
사용자의 기기에서 모션 감소 설정이 활성화된 경우 true를 반환하는 훅입니다. 이 훅으로 JS로 애니메이션을 적용한 요소도 쉽게 제어할 수 있습니다:
여전히 usePrefersReducedMotion 훅을 통해 반환된 값을 이용해 애니메이션 관련 속성에 삼항 연산자를 이용해 값을 조정하는 과정이 필요합니다. framer-motion의 motion컴포넌트를 확장해서 usePrefersReducedMotion 훅을 사용해 애니메이션을 선택적으로 비활성화 해 이 과정을 간소화해보겠습니다:
Proxy를 사용해서 motion.div의 속성 액세스를 가로채고, prefersReducedMotion: true일 때 애니메이션 관련 속성을 undefined로 설정합니다. Reflect 클래스의 도움으로 motion.div의 원래 동작은 유지하면서 필요에 따라 애니메이션을 비활성화할 수 있습니다. (Promise 기반 브라우저 익스텐션 API를 사용하는 확장 프로그램을 개발할 때 프록시를 사용했었기에 큰 어려움은 없었습니다.)
현재 구현에 한 가지 불편한 점이 있습니다. div 요소가 아닌 다른 요소를 렌더링할 수 없다는 점인데요. 이를 보완하겠습니다. 여기엔 motion 컴포넌트를 확장하는 등 여러가지 방법이 있습니다. 그러나 반드시 모든 애니메이션을 비활성화 할 필요는 없다는 점을 기억해야 합니다. 대부분 화면이 반짝이는 플래시 효과나 x/y 애니메이션을 비활성화하고 배경색이 천천히 움직이는 애니메이션은 활성화해도 무방합니다.
따라서 저는 애니메이션 비활성화 선택지를 줄 수 있게 motion 컴포넌트를 자식으로 받아, usePrefersReducedMotion 훅을 사용해 사용자의 ‘동작 감소’ 선호 여부를 확인하고, 이에 따라 자식 컴포넌트의 애니메이션 속성을 제거하여 반환하는 래퍼 함수로 구현해보겠습니다.
MotionSlot 컴포넌트는 아래와 같은 과정을 거칩니다:
1 / 5
동작 감소 선호 여부 확인
`usePrefersReducedMotion` 훅을 통해 사용자가 동작 감소 옵션의 선호 여부를 확인합니다.
비하인드 스토리로, 원래 motion 컴포넌트 여부를 판별할 때 브라우저 콘솔에 출력된 자식 요소의 심볼 값을 바탕으로 판별했는데요:
그러나 이 구현은 해당 패키지의 관리자가 값을 변경하면 의도한 대로 동작하지 않게 됩니다. 이러한 위험을 방지하고자 framer-motion 코드를 훑기 시작했습니다. 그리고 생성된 Symbol 값을 바탕으로 isMotionComponent 함수를 제공한다는 것을 알아냈고, 안전한 조건문을 구현할 수 있었습니다.
이제 아래와 같이 div 뿐만 아니라, 다양한 요소를 렌더링할 수 있게 되었습니다:
그리고 사용자의 동작 감소 선호 여부를 매번 확인하고 로직을 별도로 작성하지 않아도 됩니다. 아래 영상을 통해 사용자 선호 감소 옵션이 활성화되면 자동으로 애니메이션이 비활성화되는 것을 확인할 수 있습니다.
아래에서 MotionSlot 컴포넌트를 사용할 때와 사용하지 않을 때의 코드를 비교해보세요: