λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
React

[React] 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜, IntersectionObserver + styled-components

by LasBe 2023. 8. 6.
λ°˜μ‘ν˜•

πŸ“’ React 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜ κ΅¬ν˜„ν•˜κΈ°


μ‚¬μš©μžμ˜ λˆˆκΈΈμ„ 끌기 μœ„ν•΄ λ‹€μ–‘ν•œ 방법듀이 μ‚¬μš© λ˜λŠ”λ°,
κ·Έ μ€‘μ—μ„œλ„ κ°€μž₯ λˆˆμ— λ„λ˜ 것이 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜μ΄μ—ˆμŠ΅λ‹ˆλ‹€.

 

μ‹€μ œ μ‚¬μ΄λ“œ ν”„λ‘œμ νŠΈμ—μ„œ κ΅¬ν˜„ν•œ λͺ¨μŠ΅μ€ λ‹€μŒ λ§ν¬μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

링λͺ¨(LINGMO) - λ‹Ήμ‹ μ˜ 링크 λͺ¨μŒ

κ°œμ„± μžˆλŠ” λ‚˜λ§Œμ˜ 링크 λͺ¨μŒμ„ λ§Œλ“€μ–΄λ³΄μ„Έμš”!

lingmo.me

 

직접 κ΅¬ν˜„ν•΄λ³΄λ‹ˆ μƒκ°ν–ˆλ˜ 만큼 어렡지도 μ•Šμ•˜λ˜ 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜μ„ React둜 κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•΄λ³Έ ν›„
hook으둜 뢄리해 μž¬μ‚¬μš©μ΄ μš©μ΄ν•˜λ„λ‘ κ°œμ„ ν•œ μ½”λ“œλ₯Ό μ†Œκ°œν•©λ‹ˆλ‹€.

 

πŸ“Œ 라이브러리둜 μ‰½κ²Œ κ΅¬ν˜„ν•˜κΈ°

[React] λ¦¬μ•‘νŠΈ 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜ 라이브러리 개발

슀크둀 μ• λ‹ˆλ©”μ΄μ…˜ κΈ°λŠ₯을 쉽고 λΉ λ₯΄κ²Œ κ΅¬ν˜„ν•˜κ³  μ‹ΆμœΌμ‹  뢄듀은 μœ„ 글을 μ°Έκ³ ν•΄μ£Όμ„Έμš”!

 

πŸ“Œ IntersectionObserver

IntersectionObserver은 JavaScript의 API 쀑 ν•˜λ‚˜λ‘œ,
μš”μ†Œκ°€ 뷰포트(ν™”λ©΄)에 λ‚˜νƒ€λ‚˜κ±°λ‚˜ μ‚¬λΌμ§ˆ λ•Œλ₯Ό κ°μ§€ν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€.

 

이λ₯Ό 톡해 슀크둀, λ ˆμ΄μ§€ λ‘œλ”©(lazy loading), λ¬΄ν•œ 슀크둀(infinite scrolling) λ“±
λ‹€μ–‘ν•œ μƒν™©μ—μ„œ μš”μ†Œλ“€μ„ κ°μ§€ν•˜κ³  νŠΈλ¦¬κ±°ν•˜λŠ”λ° μœ μš©ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

μ•„λž˜λŠ” κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•΄λ³Έ μ˜ˆμ œμž…λ‹ˆλ‹€.

 

 

πŸ“Œ styled-components둜 μ• λ‹ˆλ©”μ΄μ…˜ κ΅¬ν˜„

μš°μ„  IntersectionObserverλ₯Ό μ„€λͺ…ν•˜κΈ° μ•žμ„œ μ‹€μ§ˆμ μœΌλ‘œ μš”μ†Œμ— μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ μš©ν•˜λŠ” 뢀뢄을
styled-component둜 κ΅¬ν˜„ν•œ μ½”λ“œ λ¨Όμ € λ³΄κ² μŠ΅λ‹ˆλ‹€.

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;
  }
`;

styled-components의 keyframes둜 투λͺ…도와 μœ„μΉ˜κ°€ μ΄λ™ν•˜λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ μž‘μ„±ν•©λ‹ˆλ‹€.

 

κ·Έ ν›„ μ• λ‹ˆλ©”μ΄μ…˜μ΄ 적용될 div νƒœκ·Έμ— μ›ν•˜λŠ” layout을 μž‘μ•„μ£Όκ³ 
&.을 μ΄μš©ν•΄ frame-inμ΄λž€ className이 적용되면 μ• λ‹ˆλ©”μ΄μ…˜ νš¨κ³Όκ°€ μž‘λ™ν•˜κ²Œ ν•΄μ€λ‹ˆλ‹€.

 

animation μ†μ„±μ—λŠ” μœ„μ—μ„œ μž‘μ„±ν•œ
μ• λ‹ˆλ©”μ΄μ…˜ 이름(animation-name)κ³Ό λ”λΆˆμ–΄
μ• λ‹ˆλ©”μ΄μ…˜ μ‹œκ°„(animation-duration),
μ• λ‹ˆλ©”μ΄μ…˜ κ°’ 적용(animation-fill-mode)λ₯Ό λ„£μ–΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

 

이 μ€‘μ—μ„œλ„ animation-fill-mode λŠ” 속성은 μ• λ‹ˆλ©”μ΄μ…˜μ΄ λλ‚œ ν›„μ˜ μƒνƒœλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.

 

이 속성은 λ‹€μŒκ³Ό 같은 값을 κ°€μ§ˆ 수 μžˆλŠ”λ° forwardsλ₯Ό λΆ€μ—¬ν•΄ μ• λ‹ˆλ©”μ΄μ…˜μ˜ μ’…λ£Œλ  λ•Œμ˜ μƒνƒœκ°€ 계속 μ§€μ†λ˜λ„λ‘ ν•©λ‹ˆλ‹€.

  • none
    μ• λ‹ˆλ©”μ΄μ…˜μ΄ λλ‚œ ν›„ μƒνƒœλ₯Ό μ„€μ •ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
  • forwards
    μ• λ‹ˆλ©”μ΄μ…˜μ΄ λλ‚œ ν›„ κ·Έ 지점에 κ·ΈλŒ€λ‘œ μžˆμŠ΅λ‹ˆλ‹€.
  • backwards
    μ• λ‹ˆλ©”μ΄μ…˜μ΄ λλ‚œ ν›„ μ‹œμž‘μ μœΌλ‘œ λŒμ•„μ˜΅λ‹ˆλ‹€.
  • both
    μ• λ‹ˆλ©”μ΄μ…˜μ΄μ˜ μ•ž λ’€ κ²°κ³Όλ₯Ό μ‘°ν•©ν•˜μ—¬ μ„€μ •ν•©λ‹ˆλ‹€.
  • inherit
    μ• λ‹ˆλ©”μ΄μ…˜μ˜ μƒνƒœλ₯Ό μƒμœ„ μš”μ†Œν•œν…Œ μƒμ†λ°›μŠ΅λ‹ˆλ‹€.

 

πŸ“Œ React에 IntersectionObserver 적용

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>
    </>
  );
}

useEffectλ₯Ό μ΄μš©ν•΄ IntersectionObserver의 객체(observer)κ°€ refλ₯Ό κ°μ§€ν•΄μ„œ callback을 νŠΈλ¦¬κ±°ν•˜λŠ” μ½”λ“œμž…λ‹ˆλ‹€.

 

μž‘λ™ν•˜λŠ” μˆœμ„œλ₯Ό λ”°μ§€μžλ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

 

  1. observerκ°€ μŠ€ν¬λ‘€μ„ κ°μ§€ν•˜κ³  callback 호좜
  2. callback의 entryμ—μ„œ μ‚¬μš©μžμ˜ viewport에 refλ₯Ό μ μš©ν•œ Containerκ°€ μ§„μž…ν–ˆλŠ”μ§€ μ—¬λΆ€ νŒλ‹¨
  3. λ§Œμ•½ μ§„μž…ν•˜μ§€ μ•Šμ•˜λ‹€λ©΄ isInViewport μƒνƒœλ₯Ό false둜 set,
    μ§„μž…ν–ˆλ‹€λ©΄ true둜 set
  4. isInViewportκ°€ trueκ°€ 되면 Container의 className에 frame-in을 μ‚½μž…
  5. μ• λ‹ˆλ©”μ΄μ…˜ μž‘λ™

μœ„ μ½”λ“œμ—μ„œ useEffect λ‚΄ 슀크둀 감지λ₯Ό μœ„ν•œ μš”μ†Œλ“€μ˜ κΈ°λŠ₯κ³Ό μ˜λ―ΈλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

 

πŸ”Ž IntersectionObserverEntry

IntersectionObserverEntryλŠ” IntersectionObserver의 콜백 ν•¨μˆ˜ λ‚΄μ—μ„œ
각 κ΄€μ°° λŒ€μƒ μš”μ†Œμ˜ κ°€μ‹œμ„± 정보λ₯Ό λ‹΄κ³  μžˆλŠ” 객체이며 μ€‘μš”ν•œ 속성은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • isIntersecting
    κ΄€μ°° λŒ€μƒ μš”μ†Œκ°€ ν˜„μž¬ 뷰포트 내에 λ³΄μ΄λŠ”μ§€ μ—¬λΆ€λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€. Boolean κ°’μœΌλ‘œ λ°˜ν™˜λ©λ‹ˆλ‹€.
  • intersectionRatio
    κ΄€μ°° λŒ€μƒ μš”μ†Œμ™€ 뷰포트의 ꡐ차 μ˜μ—­μ˜ λΉ„μœ¨μ„ λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
    이 값은 0λΆ€ν„° 1κΉŒμ§€μ˜ λ²”μœ„λ₯Ό 가지며, 0은 ꡐ차 μ˜μ—­μ΄ μ—†μŒμ„, 1은 κ΄€μ°° λŒ€μƒ μš”μ†Œκ°€ μ™„μ „νžˆ 뷰포트 내에 μžˆμŒμ„ μ˜λ―Έν•©λ‹ˆλ‹€.

 

πŸ”Ž IntersectionObserver의 options

const options = { root: null, rootMargin: "0px", threshold: 0 };
const observer = new IntersectionObserver(callback, options);

IntersectionObserver의 객체λ₯Ό μƒμ„±ν•˜λŠ” μƒμ„±μžμ— λ“€μ–΄κ°€λŠ” options 인자의 값듀은 λ‹€μŒκ³Ό 같은 의미λ₯Ό μ§€λ‹ˆκ³  μžˆμŠ΅λ‹ˆλ‹€.

  1. root
    • 이 μ˜΅μ…˜μ€ 뷰포트λ₯Ό κΈ°μ€€μœΌλ‘œ κ΄€μ°°ν•  λŒ€μƒ μš”μ†Œλ“€μ„ μ„ νƒν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€.
    • 기본값은 null둜, 뷰포트 전체가 κ΄€μ°° λŒ€μƒμ΄ λ©λ‹ˆλ‹€.
    • λ‹€λ₯Έ DOM μš”μ†Œλ₯Ό μ§€μ •ν•˜μ—¬ ν•΄λ‹Ή μš”μ†Œ λ‚΄μ—μ„œμ˜ κ°€μ‹œμ„±μ„ κ°μ§€ν•˜λ„λ‘ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  2. rootMargin
    • 루트 μš”μ†Œμ™€ νƒ€κ²Ÿ μš”μ†Œ μ‚¬μ΄μ˜ μ—¬λ°±(λ§ˆμ§„)을 μ„€μ •ν•©λ‹ˆλ‹€.
    • 값은 CSS의 λ§ˆμ§„ κ°’κ³Ό λ™μΌν•œ ν˜•μ‹μœΌλ‘œ μ§€μ •ν•˜λ©°, "top right bottom left" μˆœμ„œμž…λ‹ˆλ‹€.
    • λ§ˆμ§„ 값을 톡해 μš”μ†Œκ°€ 뷰포트 내에 λ“€μ–΄μ˜¬ λ•Œ μ–Όλ§ˆλ‚˜ 이전에, λ˜λŠ” λ‚˜κ°ˆ λ•Œ μ–Όλ§ˆλ‚˜ λ‚˜κ°€κΈ° 전에 κ°€μ‹œμ„±μ„ 감지할지λ₯Ό μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  3. threshold
    • νƒ€κ²Ÿ μš”μ†Œμ˜ κ°€μ‹œμ„±μ„ νŒλ‹¨ν•˜λŠ” κΈ°μ€€μœΌλ‘œ, 값은 0λΆ€ν„° 1 μ‚¬μ΄μ˜ μ†Œμˆ˜μ  μˆ«μžμž…λ‹ˆλ‹€.
    • 기본값은 0으둜, νƒ€κ²Ÿ μš”μ†Œκ°€ λ·°ν¬νŠΈμ— μ‘°κΈˆμ΄λΌλ„ λ“€μ–΄μ˜€λ©΄ isIntersecting이 trueκ°€ λ©λ‹ˆλ‹€.
    • 값이 컀질수둝 νƒ€κ²Ÿ μš”μ†Œκ°€ 뷰포트 λ‚΄μ—μ„œ 더 λ§Žμ€ λΉ„μœ¨μ„ 차지해야 κ°€μ‹œμ„±μ΄ ν™•μΈλ©λ‹ˆλ‹€.

 

πŸ“Œ IntersectionObserverλ₯Ό μ‚¬μš©ν•  λ•Œ κ³ λ €ν•΄μ•Ό ν•  사항

정말 νŽΈλ¦¬ν•˜κ²Œ μš”μ†Œλ“€μ„ 감지할 수 μžˆλ„λ‘ κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” IntersectionObserverμ΄μ§€λ§Œ
λ‹€μŒκ³Ό 같은 사항듀을 κ³ λ €ν•˜λ©° μ μš©ν•΄λ΄μ•Ό ν•©λ‹ˆλ‹€.

  • λΈŒλΌμš°μ € 지원
    IntersectionObserverλŠ” λŒ€λΆ€λΆ„μ˜ ν˜„λŒ€ λΈŒλΌμš°μ €μ—μ„œ μ§€μ›λ˜μ§€λ§Œ, 였래된 λΈŒλΌμš°μ €μ—μ„œλŠ” μ§€μ›λ˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. 이 λ•Œλ¬Έμ— 크둜슀 λΈŒλΌμš°μ§•μ„ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€. ν•„μš”ν•˜λ‹€λ©΄ 폴리필(polyfill)을 μ‚¬μš©ν•˜μ—¬ 미지원 λΈŒλΌμš°μ €μ—μ„œλ„ μ‚¬μš©ν•  수 μžˆλ„λ‘ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • λ Œλ”λ§ μ„±λŠ₯
    IntersectionObserverλ₯Ό μ‚¬μš©ν•˜μ—¬ μš”μ†Œμ˜ κ°€μ‹œμ„±μ„ κ°μ§€ν•˜λ©΄μ„œ 자주 μ‹€ν–‰λ˜λŠ” 콜백 ν•¨μˆ˜λŠ” λ Œλ”λ§ μ„±λŠ₯에 영ν–₯을 쀄 수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ μ μ ˆν•œ threshold 값을 μ„€μ •ν•˜μ—¬ 콜백 ν•¨μˆ˜κ°€ λΆˆν•„μš”ν•˜κ²Œ 자주 μ‹€ν–‰λ˜μ§€ μ•Šλ„λ‘ μ‘°μ ˆν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • 콜백 ν•¨μˆ˜ λΉ„μš©
    IntersectionObserver의 콜백 ν•¨μˆ˜ λ‚΄μ—μ„œ μ‹€ν–‰λ˜λŠ” μ½”λ“œκ°€ 무거운 경우,
    뷰포트 λ‚΄ μš”μ†Œμ˜ κ°€μ‹œμ„± λ³€κ²½ μ‹œ μ„±λŠ₯ μ €ν•˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • λ©”λͺ¨λ¦¬ λˆ„μˆ˜
    IntersectionObserver의 콜백 ν•¨μˆ˜μ—μ„œ λ©”λͺ¨λ¦¬ λˆ„μˆ˜κ°€ λ°œμƒν•˜μ§€ μ•Šλ„λ‘ μ£Όμ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
    콜백 ν•¨μˆ˜ λ‚΄μ—μ„œ μ™ΈλΆ€ λ³€μˆ˜λ₯Ό μ‚¬μš©ν•  경우 ν΄λ‘œμ €(closure) λ¬Έμ œμ— μœ μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • 동적 λ³€κ²½: DOM μš”μ†Œκ°€ λ™μ μœΌλ‘œ μƒμ„±λ˜κ±°λ‚˜ 변경될 경우, ν•΄λ‹Ή μš”μ†Œμ˜ κ°€μ‹œμ„±μ„ μ˜¬λ°”λ₯΄κ²Œ 감지할 수 μžˆλ„λ‘ IntersectionObserverλ₯Ό μ—…λ°μ΄νŠΈν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μš”μ†Œ 크기
    κ°€μ‹œμ„±μ„ νŒλ‹¨ν•˜λŠ” 데 기쀀이 λ˜λŠ” μš”μ†Œμ˜ 크기가 μž‘κ±°λ‚˜ 없을 경우, μ •ν™•ν•œ κ°€μ‹œμ„±μ„ κ°μ§€ν•˜κΈ° μ–΄λ €μšΈ 수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§(SSR)
    μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§μ„ μ‚¬μš©ν•˜λŠ” 경우,
    IntersectionObserverλŠ” λΈŒλΌμš°μ € ν™˜κ²½μ—μ„œλ§Œ μž‘λ™ν•˜λ―€λ‘œ ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œμ—μ„œλ§Œ μ‚¬μš©ν•˜λ„λ‘ μ£Όμ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

 

πŸ“Œ hookκ³Ό μ»΄ν¬λ„ŒνŠΈλ‘œ λΆ„λ¦¬ν•˜κΈ°

μ•„λ¬΄λž˜λ„ μœ„ 예제 같은 λ°©λ²•μœΌλ‘œ μ‹€μ œ ν”„λ‘œμ νŠΈμ— μ μš©ν•˜κΈ°μ—λŠ” λͺ‡ 가지 λΆˆνŽΈν•œ 점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

  1. useEffect μ‚¬μš©μœΌλ‘œ μΈν•œ μ»΄ν¬λ„ŒνŠΈ μ½”λ“œ 라인 증가 및 가독성 μ €ν•˜
  2. μ• λ‹ˆλ©”μ΄μ…˜ 적용 μš”μ†Œλ§ˆλ‹€ λŠ˜μ–΄λ‚˜λŠ” ref와 useEffect

이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μœ„ 뢈편 사항듀을 κ°œμ„ ν•˜κ³  ScrollAnimation을 λ‹΄λ‹Ήν•΄μ„œ μ²˜λ¦¬ν•  hook을 λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

 

hookμ—μ„œ refλ₯Ό λ°°μ—΄λ‘œ λ§Œλ“€μ–΄ μ—¬λŸ¬ μš”μ†Œλ“€μ„ ν•œκΊΌλ²ˆμ— κ΄€λ¦¬ν•˜λŠ” 방법도 κ΅¬ν˜„ν•΄ λ΄€λŠ”λ°
λΆˆλ³€μ„± λ•Œλ¬Έμ— 둜직이 λΆˆν•„μš”ν•˜κ²Œ λ³΅μž‘ν•΄μ§€κ³  가독성도 쒋지 μ•Šμ•„μ Έ μ»΄ν¬λ„ŒνŠΈ λ°©μ‹μœΌλ‘œ λ§Œλ“€μ–΄λ΄€μŠ΅λ‹ˆλ‹€.

 

πŸ”Ž useScrollAnimation.ts

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

export const useScrollAnimation = () => {
  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 { isInViewport, ref };
};

μœ„μ—μ„œ 봀던 슀크둀 감지 λ‘œμ§μ„ κ·ΈλŒ€λ‘œ hook으둜 λΆ„λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.

hookμ—μ„œ ref와 animation 적용 μ—¬λΆ€λ₯Ό νŒλ‹¨ν•˜λŠ” 둜직과 μƒνƒœλ₯Ό μ„ μ–Έν•˜κ³  return ν•΄μ€λ‹ˆλ‹€.

 

πŸ”Ž ScrollAnimationContainer.tsx

import { Container } from "./styled";
import { useScrollAnimation } from "./useScrollAnimation";

type PropsType = {
  children: React.ReactNode;
};

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

κ·Έ λ‹€μŒ useScrollAnimation 훅을 μ΄μš©ν•΄ children을 κ°μ‹Έμ£ΌκΈ°λ§Œ ν•˜λ©΄ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λ˜λŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.

 

πŸ”Ž μ»΄ν¬λ„ŒνŠΈ μ‚¬μš©

import { ScrollAnimationContainer } from "./ScrollAnimationContainer";
import { Container } from "./styled";

export default function App() {
  return (
    <>
      <Container>
        <h1>μ•„λž˜λ‘œ 슀크둀 ν•˜μ„Έμš”</h1>
      </Container>

      <ScrollAnimationContainer>
        <h1>μ•ˆλ…•</h1>
      </ScrollAnimationContainer>

      <ScrollAnimationContainer>
        <h1>ν•˜μ„Έμš”</h1>
      </ScrollAnimationContainer>

      <ScrollAnimationContainer>
        <h1>λ°˜κ°‘μŠ΅λ‹ˆλ‹€</h1>
      </ScrollAnimationContainer>
    </>
  );
}

μœ„μ—μ„œ λͺ¨λ“  λ‘œμ§μ„ μ²˜λ¦¬ν•΄μ£ΌλŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό λ§Œλ“€μ–΄λ‘” 덕뢄에
μš”μ†Œλ“€μ„ κ°„νŽΈν•˜κ²Œ wrapping ν•˜κΈ°λ§Œ 해도 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λ˜λŠ” λͺ¨μŠ΅μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

그럼 μžμ„Έν•œ μ½”λ“œμ™€ κ΅¬μ‘°λŠ” μœ„μ— μ˜¬λ €λ‘” CodeSandBoxλ₯Ό μ°Έκ³ ν•΄μ£Όμ‹œκ³ 
슀크둀 μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•΄ 더 동적인 웹을 κ°œλ°œν•΄λ΄μ•Όκ² μŠ΅λ‹ˆλ‹€.

λ°˜μ‘ν˜•

λŒ“κΈ€


μ˜€ν”ˆ 채νŒ