ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 충돌 해결기

     

    구현하고자 했던 효과 

     

    내가 박스 요소에 구현하고자 하는 효과는 다음과 같았다.

     

     

    mount

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

     

     

     

     

    hover


    요소에 커서를 댔을 때 부드럽게 위로 올라갔다가 마우스를 떼면 다시 제 자리로 돌아옴

     

     

     

    처음 작성한 코드

     

    이 효과를 위해 다음과 같이 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_bug

     

    커서를 대고 뗐을 때(=hover를 해제했을 때) 박스가 무겁게 내려옴.
    특히 박스 순서 1->2->3으로 갈 수록 더 무겁게 내려옴.

     

     

     

    문제 원인

     

    화면의 초기 상태부터 풀어서 설명하면 코드는 이렇게 적용된다.

     

    1. 초기 상태 (initial)에서 시작
    2. 화면에 보이면 (whileInView) 등장 애니메이션 실행
    3. hover 시 (whileHover) 적용
    4. 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에 배열을 사용해서 참조하는 법을 익히게 돼서 유익했다. 그리고 결과적으로 원하는 움직임 효과를 얻게 돼서 뿌듯하다!

     

    final

     

Designed by Tistory.