라이브러리/framer-motion

[트러블 슈팅 / framer-motion] onViewportEnter 뷰포트 감지 버그 해결

순코딩 2024. 6. 9. 22:52

문제

height가 100vh인 컴포넌트를 여러개 렌더링하여 onViewportEnter로 뷰포트 감지를 하였는데 마운트 시에 한 개만 뷰포트에 감지되는 것이 아닌 두 개의 컴포넌트가 뷰포트에 감지됨으로써 뷰포트에 감지되었을 때 애니메이션을 작동하려는 의도와는 다르게 코드가 동작함

 

해결방안

react-intersection-observer 라이브러리를 사용하여 뷰포트 진입을 보다 정확하게 감지하여 해결했다.

간단히 요약하자면 해당 라이브러리를 사용해 컴포넌트가  뷰포트 50%이상 진입 시 뷰포트 감지로 간주하여 문제를 해결했다

 

초기 렌더링 시 0번째와 1번째 페이지의 애니메이션이 모두 실행되는 이유는, 두 컴포넌트가 뷰포트에 동시에 노출되기 때문일 수 있습니다. 특히, 스크롤 스냅이 적용된 경우 뷰포트의 크기와 스크롤 위치에 따라 여러 컴포넌트가 동시에 뷰포트에 노출될 수 있습니다.

이 문제를 해결하기 위해 `react-intersection-observer` 라이브러리를 사용하여 뷰포트에 진입하는 요소를 보다 정확하게 감지할 수 있습니다. 이 라이브러리는 Intersection Observer API를 사용하여 요소가 뷰포트에 진입하거나 이탈할 때 콜백을 트리거합니다.

먼저, `react-intersection-observer`를 설치합니다:

```bash
npm install react-intersection-observer
```

그리고, 페이지 컴포넌트에서 이 라이브러리를 사용하여 뷰포트 진입을 감지합니다:

### 수정된 코드 예제

```jsx
import { motion } from "framer-motion";
import { useState } from "react";
import styled from "styled-components";
import { useInView } from "react-intersection-observer";

export default function Content() {
  const pageList = ["페이지 1", "페이지 2", "페이지 3", "페이지 4", "페이지 5"];
  return (
    <>
      <Container>
        {pageList.map((item, index) => (
          <Page key={index} pageNum={item} pageIndex={index} />
        ))}
      </Container>
    </>
  );
}

const Container = styled.div`
  background-color: red;
  width: 100vw;
  height: 100vh;
  overflow-y: scroll; // 스크롤이 가능하도록 설정
  scroll-snap-type: y mandatory;
`;

function Page({ pageNum, pageIndex }) {
  const itemList = [`아이템1`, `아이템2`, `아이템3`, `아이템4`, `아이템5`];
  const [itemAnimation, setItemAnimation] = useState("stop");

  const { ref, inView } = useInView({
    triggerOnce: true, // 한 번만 트리거
    threshold: 0.5, // 뷰포트에 50% 이상 보일 때 트리거
  });

  if (inView && itemAnimation === "stop") {
    setItemAnimation("start");
  }

  return (
    <PageWrap ref={ref}>
      <Contents>
        <h1>{pageNum}</h1>
        {itemList.map((item, index) => (
          <motion.div
            key={index}
            initial={{ x: "100vw" }}
            animate={itemAnimation === "start" ? { x: 0 } : {}}
            transition={{
              type: "spring",
              stiffness: 30,
              delay: 0.5 * index, // 애니메이션 시작 전에 0.5초 딜레이
            }}
          >
            {item}
          </motion.div>
        ))}
      </Contents>
    </PageWrap>
  );
}

const PageWrap = styled(motion.div)`
  width: 100vw; // 화면 전체를 채운다
  height: 100vh; // 화면 전체를 채운다
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
`;

const Contents = styled.div`
  width: 300px;
  height: 400px;
  background-color: white;
  position: relative;
  scroll-snap-align: center; // parallax 효과
`;
```

### 설명

1. **`useInView`**: `react-intersection-observer`에서 제공하는 훅을 사용하여 요소의 뷰포트 진입 상태를 감지합니다.
2. **`ref`**: 감지할 요소에 연결되는 참조입니다.
3. **`inView`**: 요소가 뷰포트에 진입했는지 여부를 나타내는 불리언 값입니다.
4. **`triggerOnce`**: 요소가 한 번 뷰포트에 진입할 때만 트리거되도록 설정합니다.
5. **`threshold`**: 요소가 뷰포트에 50% 이상 보일 때 트리거되도록 설정합니다.
6. **`ref` 속성**: `PageWrap` 컴포넌트에 `ref`를 전달하여 뷰포트 진입 상태를 감지합니다.
7. **조건부 애니메이션 트리거**: `inView`가 `true`이고 `itemAnimation`이 `stop` 상태일 때 `setItemAnimation("start")`를 호출하여 애니메이션을 시작합니다.

이렇게 하면 뷰포트에 진입한 페이지에 대해서만 애니메이션이 실행되며, 0번째와 1번째 페이지가 동시에 애니메이션되는 문제를 해결할 수 있습니다.