React에서 IntersectionObserver으로 애니메이션 넣기

MunGyun·2024년 8월 24일
0

React

목록 보기
5/5
post-thumbnail

들어가며

다음 주부터 진행할 프로젝트에서 스크롤 시 발생하는 애니메이션을 처음으로 사용해볼 예정이므로 IntersectionObserver에 대한 개념을 정리하고 간단한 예제 코드를 작성하며 이것에 익숙해지기 위해 포스팅을 진행했습니다.

개념 정리

생성자

IntersectionObserver는 생성자 함수를 통해 인스턴스를 생성합니다. 생성자 함수는 두 개의 인자를 받습니다.
callback: 요소가 교차할 때 호출될 함수
options: 교차 관찰의 세부 설정을 포함하는 객체


const observer = new IntersectionObserver(callback, options);

콜백 함수

callback 함수는 관찰 중인 요소가 교차할 때 호출됩니다. 이 함수는 IntersectionObserverEntry 객체의 배열을 인자로 받습니다.
IntersectionObserverEntry 객체는 요소의 교차 상태에 대한 정보를 담고 있습니다.

IntersectionObserverEntry는 관찰 대상 요소가 교차 영역과 상호작용할 때 발생하는 정보를 담고 있습니다. 이 객체는 콜백 함수에 전달되며, 요소의 교차 상태를 이해하는 데 필요한 다양한 속성을 제공합니다. 예시에서는 entries가 이것에 해당합니다.


const callback = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 요소가 뷰포트와 교차할 때 실행할 코드
    }
  });
};

옵션 객체

options 객체는 다음과 같은 설정을 포함할 수 있습니다.
root: 교차를 관찰할 부모 요소 (기본값은 null, 즉 뷰포트).
rootMargin: 뷰포트의 마진을 지정하는 문자열.
threshold: 교차 비율을 지정하는 값 (0에서 1 사이의 숫자).

예를 들어, 요소의 10%가 교차 영역에 들어오면 intersectionRatio는 0.1가 됩니다. threshold를 설정함으로써, 요소가 지정한 비율만큼 교차했을 때 콜백이 실행됩니다.

const options = {
  root: null, // 기본값은 뷰포트
  rootMargin: '0px',
  threshold: 0.1
};

관찰 및 중지

observe(target): 특정 요소를 관찰.
unobserve(target): 특정 요소의 관찰을 중지.
disconnect(): 모든 관찰을 중지.


observer.observe(targetElement);
observer.unobserve(targetElement);
observer.disconnect();

예시 코드 만들기

우선 각 div에 사용할 컴포넌트를 만들었습니다.

const divRefs = useRef([]);

const Item = ({ text, index, setRef }) => (
  <div ref={(el) => setRef(index, el)}>
    <span>{text}</span>
  </div>
);

const setRef = (index, element) => {
    divRefs.current[index] = element;
  };

setRef 함수를 통해 렌더링된 DOM요소를 divRef에 넣습니다.
이제 애니메이션에 관한 코드를 작성하겠습니다.

useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add("fade-in");
          } else {
            entry.target.classList.remove("fade-in");
          }
        });
      },
      { threshold: 0.1 }
    );

    divRefs.current.forEach((div) => {
      if (div) observer.observe(div);
    });

    return () => {
      divRefs.current.forEach((div) => {
        if (div) observer.unobserve(div);
      });
    };
  }, []);

IntersectionObserver를 사용하기 위해 useEffect를 사용하는 것은 일반적인 패턴입니다. useEffect를 사용하면 컴포넌트의 렌더링이 완료된 후에 IntersectionObserver를 설정하고, 컴포넌트가 언마운트될 때 관찰을 해제하는 작업을 처리할 수 있습니다.

isIntersecting 속성을 통해 entries의 특정 요소가 화면에 보이는지를 감지하고, 해당 요소에 "fade-in" 클래스를 추가합니다. "fade-in" 클래스는 CSS에서 div의 opacity를 1로 변경하여 요소가 서서히 나타나도록 하는 애니메이션을 적용합니다.

options 객체의 threshold를 0.1로 설정하면, 특정 요소가 화면에 10%만큼 보일 때 애니메이션이 실행됩니다. 이는 요소의 교차 비율이 10% 이상일 때 IntersectionObserver가 콜백 함수를 호출하게 됩니다.

이후, observe 메서드를 통해 각 요소를 관찰하며, IntersectionObserver 인스턴스가 요소의 교차 상태를 추적하고 애니메이션을 트리거할 수 있도록 합니다.

마지막으로 리턴 값과 CSS속성에 대해 보여드리겠습니다.

return (
    <StyledApp>
      {items.map((text, index) => (
        <Item key={index} text={text} index={index} setRef={setRef} />
      ))}
    </StyledApp>
  );
};

const StyledApp = styled.div`
  color: #fff;
  display: flex;
  gap: 300px;
  flex-direction: column;
  justify-items: center;
  align-items: center;

  div {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 400px;
    text-align: center;
    font-size: 70px;
    font-weight: 700;
    opacity: 0;
    transition: opacity 1s ease-in-out;
  }

  .fade-in {
    opacity: 1;
  }
`;

전체 코드를 업로드하며 이번 포스팅을 마무리하겠습니다.

import styled from "styled-components";
import { useEffect, useRef } from "react";

// Item 컴포넌트 정의
const Item = ({ text, index, setRef }) => (
  <div ref={(el) => setRef(index, el)}>
    <span>{text}</span>
  </div>
);

const App = () => {
  const divRefs = useRef([]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add("fade-in");
          } else {
            entry.target.classList.remove("fade-in");
          }
        });
      },
      { threshold: 0.1 }
    );

    divRefs.current.forEach((div) => {
      if (div) observer.observe(div);
    });

    return () => {
      divRefs.current.forEach((div) => {
        if (div) observer.unobserve(div);
      });
    };
  }, []);

  // divRefs를 설정하는 함수
  const setRef = (index, element) => {
    divRefs.current[index] = element;
  };

  // 데이터 정의
  const items = [
    "안녕하세요",
    "감사해요",
    "잘있어요",
    "다시만나요",
    "사랑해요",
  ];

  return (
    <StyledApp>
      {items.map((text, index) => (
        <Item key={index} text={text} index={index} setRef={setRef} />
      ))}
    </StyledApp>
  );
};

export default App;

const StyledApp = styled.div`
  color: #fff;
  display: flex;
  gap: 300px;
  flex-direction: column;
  justify-items: center;
  align-items: center;

  div {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 400px;
    text-align: center;
    font-size: 70px;
    font-weight: 700;
    opacity: 0;
    transition: opacity 1s ease-in-out;
  }

  .fade-in {
    opacity: 1;
  }
`

참고자료

MDN

profile
Hi! I'm Mun Gyun :)

0개의 댓글