[React] 스크롤 애니메이션, IntersectionObserver + styled-components

piper ·2024년 2월 27일
0

React

목록 보기
14/22

목표: 리액트에서 사용할 수 있는 스크롤 애니메이션을 만들어보고
이를 customized hook으로 만들어본다.

과정:

IntersectionObserver:
IntersectionObserver은 JavaScript의 API 중 하나로, 요소가 뷰포트(화면)에 나타나거나 사라질 때를 감지하는 역할을 합니다.
이를 통해 스크롤, 레이지 로딩(lazy loading), 무한 스크롤(infinite scrolling) 등 다양한 상황에서 요소들을 감지하고 트리거하는데 유용하게 사용할 수 있습니다.

styled-components로 애니메이션 구현:

import styled, { keyframes } from "styled-components";

const frameInAnimation = keyframes`
  0% {
    opacity: 0;
    transform: translateX(-100%);
  }

  100%{
    opacity: 1;
    transform: translateX(0%);
  }
`;

export const Container = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;

  &.frame-in {
    animation: ${frameInAnimation} 2s forwards;
  }
`;

React에 IntersectionObserver 적용:
useEffect를 이용해 IntersectionObserver의 객체(observer)가 ref를 감지해서 callback을 트리거하는 코드입니다.

작동하는 순서를 따지자면 다음과 같습니다.

1.observer가 스크롤을 감지하고 callback 호출
2.callback의 entry에서 사용자의 viewport에 ref를 적용한 Container가 진입했는지 여부 판단
3.만약 진입하지 않았다면 isInViewport 상태를 false로 set,
진입했다면 true로 setisInViewport가 true가 되면 Container의 className에 frame-in을 삽입 애니메이션 작동

import { useState, useRef, useEffect } from "react";
import { Container } from "./styled";

export default function App() {
  const [isInViewport, setIsInViewport] = useState(false);
  const ref = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!ref.current) return; // 요소가 아직 준비되지 않은 경우 중단

    const callback = (entries: IntersectionObserverEntry[]) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // 요소가 뷰포트에 나타났을 경우
          setIsInViewport(true);
        } else {
          // 요소가 뷰포트를 벗어난 경우
          setIsInViewport(false);
        }
      });
    };

    const options = { root: null, rootMargin: "0px", threshold: 0 };

    const observer = new IntersectionObserver(callback, options);
    observer.observe(ref.current); // 요소 관찰 시작

    return () => {
      observer.disconnect(); // 컴포넌트 언마운트 시 관찰 중단
    };
  }, []);

  return (
    <>
      <Container>
        <h1>아래로 스크롤 하세요</h1>
      </Container>
      <Container className={isInViewport ? "frame-in" : ""} ref={ref}>
        <h1>안녕하세요</h1>
      </Container>
    </>
  );
}

이제 이 것을 Hook으로 만들어 본다.
이는 2단계의 과정으로 나뉘어 진다.
1. useState, useEffect, useRef가 있는 스크롤을 감지해주는 커스텀 훅 로직
2. component를 {children} props으로 받아주는 컨테이너.

import { useEffect, useRef, useState } from "react";

export const UseScrollAnimation = () => {
  const [isInViewport, setIsInViewport] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current) return; // 요소가 아직 준비되지 않은 경우 중단

    const callback = (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // 요소가 뷰포트에 나타났을 경우
          setIsInViewport(true);
        } else {
          // 요소가 뷰포트를 벗어난 경우
          setIsInViewport(false);
        }
      });
    };

    const options = { root: null, rootMargin: "0px", threshold: 0 };

    const observer = new IntersectionObserver(callback, options);
    observer.observe(ref.current); // 요소 관찰 시작

    return () => {
      observer.disconnect(); // 컴포넌트 언마운트 시 관찰 중단
    };
  }, []);

  return { isInViewport, ref };
};
import { AnimatedHeading } from "../styles/AnimatedHeading";
import { UseScrollAnimation } from "./useScrollAnimation";

export const ScrollAnimationContainer = ({ children }) => {
  const { ref, isInViewport } = UseScrollAnimation();
  return (
    <AnimatedHeading ref={ref} className={isInViewport ? "frame-in" : ""}>
      {children}
    </AnimatedHeading>
  );
};

출처: https://lasbe.tistory.com/178#%F0%9F%93%8C_IntersectionObserver%EB%A5%BC_%EC%82%AC%EC%9A%A9%ED%95%A0_%EB%95%8C_%EA%B3%A0%EB%A0%A4%ED%95%B4%EC%95%BC_%ED%95%A0_%EC%82%AC%ED%95%AD

profile
연습일지

0개의 댓글