-
Framer Motion에서 whileInView + hover 충돌 해결하기개발 여정/FrontEnd 2025. 4. 9. 23:27
들어가기 앞서, Motion 라이브러리에 대해
Motion 라이브러리는 리액트에서 애니메이션 효과를 구현하기 위한 라이브러리다. 화면 요소가 등장할 때 위아래로 튀어오르거나, 클릭했을 때 커졌다 작아지는 등의 다양한 시각적인 효과를 구현할 수 있다. 이전에는 이런 애니메이션 효과를 적용하려면 css에서 @keyframs를 일일이 정의해야 했지만, 모션 라이브러리를 통해 간단하게 JS 코드 내에서 구현할 수 있게 되었다.
📦 기본 설치
npm run motion을 통해 간단하게 설치할 수 있다.
npm run motion사용 예시
사용하고자 하는 html 요소에 motion.을 붙이고 각종 속성을 주면 된다.
<motion.div initial={{ opacity: 0 }} // 처음 마운트될 때 불투명도가 0으로, 화면에 보이지 않는다. whileInView={{ opacity: 1 }} // 화면이 뷰포트에 잡히면 불투명도가 1로 div 요소가 나타난다. transition={{ duration: 1 }} // div 요소가 나타나는 과정이 1초로 서서히 나타난다. />Motion에서
whileInView+hover충돌 해결기구현하고자 했던 효과
내가 박스 요소에 구현하고자 하는 효과는 다음과 같았다.

요소 3개가 등장하는데 차례대로 위에서 아래로 스르륵 나타남

요소에 커서를 댔을 때 부드럽게 위로 올라갔다가 마우스를 떼면 다시 제 자리로 돌아옴처음 작성한 코드
이 효과를 위해 다음과 같이 initial, whileInView, whileHover 속성을 설정했다.
{infoList.map( (infoBox: any, index: number) => ( <motion.li ref={index} initial={{ y: 30, opacity: 0 }} // 처음 마운트될 때는 아래에서부터 등장하기 위해 y축을 30으로 설정, 불투명했다가 나타나는 효과를 위해 opacity를 0으로 설정 whileInView={{ y: 0 opacity: 1, transition: { delay: (index + 1) * 0.4, duration: 0.5 }, }} // 마운트될 때 3개의 요소가 차례대로 등장하기 위해 delay 속도를 index별로 다르게 설정, 모습을 서서히 드러내는 시간은 0.5초로 줌 whileHover={{ y: -8, transition: { duration: 0.5 }, // 마우스 hover 시 위로 올라가도록 y축 -8로 설정, 올라가는 시간을 0.5초로 설정 }} /> ) )}문제 발생
하지만, 위와 같이 적용해보니 결과가 뭔가 이상했다.

커서를 대고 뗐을 때(=hover를 해제했을 때) 박스가 무겁게 내려옴.
특히 박스 순서 1->2->3으로 갈 수록 더 무겁게 내려옴.문제 원인
화면의 초기 상태부터 풀어서 설명하면 코드는 이렇게 적용된다.
- 초기 상태 (
initial)에서 시작 - 화면에 보이면 (
whileInView) 등장 애니메이션 실행 - hover 시 (
whileHover) 적용 - hover 해제 시, 마지막 상태인
whileInView로 돌아가며 그 상태의 transition 시간을 적용함
코드를 살펴보면 아무래도 whileInView의 delay 시간이 hover를 해제하는 순간에도 적용되는 것 같았다.
뒤에 있는 박스일 수록 더 무겁게 박스가 내려오는 것도 motion.li에 차례대로 증가하는 delay 속도를 줬기 때문인 것 같았다.문제의 whileInView, whileHover를 다시 보면 이렇다.
whileInView={{ y: 0 opacity: 1, transition: { delay: (index + 1) * 0.4, duration: 0.5 }, <- 여기의 delay가 범인 }} whileHover={{ y: -8, transition: { duration: 0.5 }, }}hover 해제 시에도
whileInView.transition.delay가 적용되기 때문에,y: -8에서y: 0으로 돌아올 때 0.4초, 0.8초, 1.2초의 딜레이가 부과됐던 것이다.해결 방법:
onHoverStart/onHoverEnd로 대체whileHover를 쓰지 않고, 더 디테일하게 Hover 움직임을 제어할 수 있는onHoverStart,onHoverEnd로 대체했다.whileHover과는 달리onHoverStart,onHoverEnd에는 다음과 같이 콜백 함수를 넣어줘야 한다.<motion.a onHoverStart={event => {}} onHoverEnd={event => {}} />첫 시도 코드:
onHoverStart,onHoverEnd로 대체하고,
그 안의 콜백함수가 마우스 event의 currentTarget가 null이 아닐 때 호출되도록 다음과 같이 작성했다.onHoverStart={(event) => { if (event.currentTarget != null) { const target = event.currentTarget as HTMLElement; target.stylestyle.transform = "translateY(-8px)"; target.style.style.transition = "transform 0.5s"; } }} onHoverEnd={(event) => { if (event.currentTarget != null) { const target = event.currentTarget as HTMLElement; target.stylestyle.transform = "translateY(0px)"; target.style.style.transition = "transform 0.5s"; } }}또 다른 문제 발생: event.currentTarget이 자꾸 null이 됨
저렇게 하니 이번에는 박스에 커서를 댔을 때 아예 움직임이 나타나지 않았다.
console.log로 콜백함수가 실행되나 봤더니, 실행되지 않는 걸로 보아
event.currentTarget != null<- 이 조건문을 통과하지 못한 것 같았다.문제 원인: TypeScript의 너무 엄격한 타입 추론
찾아보니 타입 스크립트의 경우,
event.currentTarget를 DOM 요소가 아닌EventTarget로 보기 때문에 null로 볼 수 있다고 했다.
그렇다면EventTarget은 뭐지?EventTarget이란?
· 브라우저에서 이벤트를 받을 수 있는 모든 객체의 가장 상위 타입. · HTMLElement, Window, Document 등은 전부 EventTarget을 상속함. · 하지만 EventTarget 자체는 너무 추상적이라서 실제로는 속성이 거의 없음. 👉 style이나 classList 같은 DOM 조작 속성은 없음.문제 해결 2: event.currentTarget 버리고 useRef로 직접 요소 참조하기
event.currentTarget👈 얘가 문제였기 때문에 null이 될 수 없는 직접 참조 방법으로 바꿔야 했다.
그래서 motion.li의 요소를 모두 직접 참조하도록 useRef를 사용했다.그리고 li 요소가 여러 개이기 때문에, 처음 useRef를 선언할 땐 빈 배열로 선언하고, map문을 돌면서 index에 따라 다른 이름을 가지도록 처리했다. (헥헥💦💦)
코드_찐_최종_최최종
그렇게 완성된 코드에 적용된 해결방안들을 보면 다음과 같다.
1️⃣
whileHover👉onHoverStart,onHoverEnd로 변경
2️⃣onHoverStart,onHoverEnd의 참조 요소를useRef를 통해 직접 참조함
3️⃣ 적용되는 li 요소가 여러 개이기 때문에 ref를 배열로 설정const listRefs = useRef<(HTMLLIElement | null)[]>([]); {infoList.map( (infoBox: any, index: number) => ( <motion.li ref={(el) => { listRefs.current[index] = el; }} initial={{ y: 30, opacity: 0 }} whileInView={{ y: 0 opacity: 1, transition: { delay: (index + 1) * 0.4, duration: 0.5 }, }} onHoverStart={(event) => { if (listRefs.current[index] != null) { listRefs.current[index].style.transform = "translateY(-8px)"; listRefs.current[index].style.transition = "transform 0.5s"; } }} onHoverEnd={(event) => { if (listRefs.current[index] != null) { listRefs.current[index].style.transform = "translateY(0px)"; listRefs.current[index].style.transition = "transform 0.5s"; } }} /> ) )}
느낀 점
Motion이 사용할 땐 재미있는데, 정교한 움직임을 구현하다 보면 속성끼리 부딪히는 경우가 생기는 것 같다. 그래도 몰랐던
EventTarget에 대한 개념이나,useRef에 배열을 사용해서 참조하는 법을 익히게 돼서 유익했다. 그리고 결과적으로 원하는 움직임 효과를 얻게 돼서 뿌듯하다!
'개발 여정 > FrontEnd' 카테고리의 다른 글
Tailwind css로 컴포넌트 디자인 시스템 구축하기 (0) 2025.04.17 Framer Motion에서 event.currentTarget 사용 시 null이 되는 이유 (0) 2025.04.15 [Next.js 13] ESLint: TypeError: this.libOptions.parse is not a function 해결법 (0) 2023.08.02 [Next.js] Hydration이란? (0) 2023.07.01 [React] setInterval을 통해 React Hooks 알아보기 (useEffect, useState, useRef) (0) 2023.05.26 - 초기 상태 (