๐ŸŽฎ ์Šคํฌ๋กค์— ์˜ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๐ŸŽฎ

๋ฐ•์ƒ์€ยท2022๋…„ 6์›” 15์ผ
0
post-thumbnail

๐Ÿค” Intersection Observer API

๋ฃจํŠธ ์š”์†Œ์™€ ํƒ€๊ฒŸ ์š”์†Œ์˜ ๊ต์ฐจ์ ์„ ๊ด€์ฐฐํ•˜๋Š” API์ž…๋‹ˆ๋‹ค.

๋ฃจํŠธ ์š”์†Œ๋ฅผ ๋ทฐํฌํŠธ๋กœ ํ•˜๊ณ  ํƒ€๊ฒŸ ์š”์†Œ๋ฅผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•  ํƒœ๊ทธ๋กœ ์ง€์ •ํ•œ๋‹ค๋ฉด ์Šคํฌ๋กค์— ์˜ํ•ด์„œ ๋ทฐํฌํŠธ์™€ ํƒœ๊ทธ๊ฐ€ ๊ต์ฐจํ•˜๋Š” ๊ฒฝ์šฐ์— ์›ํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ˜ฎ Intersection Observer API ์‚ฌ์šฉ๋ฒ•

  • ์˜ต์…˜๊ฐ’๋“ค
    1. root: ๋ฃจํŠธ ์š”์†Œ๋ฅผ ์ง€์ •ํ•˜๋Š” ์†์„ฑ ( ์ƒ๋žต ์‹œ ๋ทฐํฌํŠธ )
    2. rootMargin: ๋ฃจํŠธ ์š”์†Œ์˜ ๋ฒ”์œ„๋ฅผ ํ™•์žฅํ•˜๋Š” ์†์„ฑ ( ๋‹จ์œ„ ์ง€์ • )
    3. threshold: ๊ต์ฐจ์ ์˜ ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•˜๋Š” ์†์„ฑ 0.0~1.0 ( ๋ฐฐ์—ด๋„ ๊ฐ€๋Šฅ )
  • ์ฝœ๋ฐฑ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜
    1. boundingClientRect: ํƒ€๊ฒŸ ์š”์†Œ์˜ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜• ์ •๋ณด
    2. intersectionRatio: ๋ฃจํŠธ ์š”์†Œ์™€ ํƒ€๊ฒŸ ์š”์†Œ์˜ ๊ต์ฐจ ๋น„์œจ ( 0.0~1.0 )
    3. intersectionRect: ?
    4. rootBounds: ๋ฃจํŠธ ์š”์†Œ์˜ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜• ์ •๋ณด
    5. target: ํƒ€๊ฒŸ ์š”์†Œ
    6. time: ๊ต์ฐจ ์‹œ๊ฐ„
    7. isIntersecting: ํƒ€๊ฒŸ ์š”์†Œ์™€ ๋ฃจํŠธ ์š”์†Œ์˜ threshold๋งŒํผ์˜ ๊ต์ฐจ ์—ฌ๋ถ€

๐Ÿง Intersection Observer API ์˜ˆ์‹œ

  • ์˜ˆ์‹œ
const IO = new IntersectionObserver(callback, options);

IO.observe(element);
  • ์‚ฌ์šฉ ์˜ˆ์‹œ
const onScroll = useCallback((entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
  // ํŠน์ • ํƒœ๊ทธ๊ฐ€ ๋ทฐํฌํŠธ์˜ "threshold"๊ฐ’๋งŒํผ ๋ณด์—ฌ์ง€๋ฉด ํ˜„์žฌ ์ฝœ๋ฐฑํ•จ์ˆ˜ ์‹คํ–‰
}, [])

useEffect(() => {
  if(!elementRef.current) return;

  // ์ฝœ๋ฐฑํ•จ์ˆ˜์™€ ์˜ต์…˜๊ฐ’ ์ง€์ •
  let observer = new IntersectionObserver(onScroll, { threshold: 0.1 });
  // ํŠน์ • ์š”์†Œ ๊ฐ์‹œ ์‹œ์ž‘
  observer.observe(elementRef.current);

  // ๊ฐ์‹œ ์ข…๋ฃŒ
  return () => observer?.disconnect();
}, [elementRef]);
import { useRef, useEffect, useCallback } from "react";

type Direction = "up" | "down" | "left" | "right";

type Props = {
  direction?: Direction;
  duration?: number;
  delay?: number;
};
type ReturnType = {
  ref: React.MutableRefObject<any>;
  style: {
    opacity: number;
    transform: string | undefined;
  };
};

const useScrollFadeIn = ({
  direction = "up",
  duration = 1,
  delay = 0,
}: Props): ReturnType => {
  // 2022/06/14 - ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•  ํƒœ๊ทธRef - by 1-blue
  const elementRef = useRef<HTMLElement | null>(null);

  // 2022/06/14 - ์ง€์ •ํ•œ ๋ฐฉํ–ฅ์— ๋”ฐ๋ฅธ ํŠธ๋žœ์ง€์…˜ ๋ฐ˜ํ™˜ - by 1-blue
  const handleDirection = useCallback((dir: Direction) => {
    switch (dir) {
      case "up":
        return "translate3d(0, 50%, 0)";
      case "down":
        return "translate3d(0, -50%, 0)";
      case "left":
        return "translate3d(50%, 0, 0)";
      case "right":
        return "translate3d(-50%, 0, 0)";
      default:
        return "";
    }
  }, []);

  // 2022/06/14 - IntersectionObserver์— ๋“ฑ๋กํ•  ์ฝœ๋ฐฑํ•จ์ˆ˜ - by 1-blue
  const onScroll = useCallback(
    ([{ isIntersecting }]: IntersectionObserverEntry[]) => {
      const { current } = elementRef;
      if (!current) return;

      // ์ง€์ •ํ•œ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ "threshold"๋งŒํผ์„ ์ œ์™ธํ•˜๊ณ  ๋ทฐํฌํŠธ์— ๋“ค์–ด์™”๋‹ค๋ฉด ์‹คํ–‰
      if (isIntersecting) {
        current.style.transitionProperty = "all";
        current.style.transitionDuration = `${duration}s`;
        current.style.transitionTimingFunction = "cubic-bezier(0, 0, 0.2, 1)";
        current.style.transitionDelay = `${delay}s`;
        current.style.opacity = "1";
        current.style.transform = "translate3d(0, 0, 0)";
      }
      // ์ง€์ •ํ•œ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ "threshold"๋งŒํผ์„ ์ œ์™ธํ•˜๊ณ  ๋ทฐํฌํŠธ ๋ฐ–์— ์กด์žฌํ•œ๋‹ค๋ฉด ์‹คํ–‰
      // ์•„๋ž˜ ๋ถ€๋ถ„์„ ๋„ฃ์–ด์ฃผ๋ฉด ๊ต์ฐจ๋ฒ”์œ„ ์ดํ•˜์ผ ๊ฒฝ์šฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐ˜๋Œ€๋กœ ์‹คํ–‰๋จ
      else {
        current.style.transitionProperty = "all";
        current.style.transitionDuration = `${duration}s`;
        current.style.transitionTimingFunction = "cubic-bezier(0, 0, 0.2, 1)";
        current.style.transitionDelay = `${delay}s`;
        current.style.opacity = "0";
        current.style.transform = handleDirection(direction);
      }
    },
    [delay, duration, handleDirection, direction]
  );

  // 2022/06/14 - IntersectionObserver ๋“ฑ๋ก - by 1-blue
  useEffect(() => {
    if (!elementRef.current) return;

    // ์ขŒ์šฐ๋กœ ์›€์ง์ด๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์„ ๊ฒฝ์šฐ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์ „์ฒด ํฌ๊ธฐ์˜ 50%๋ฅผ ์ด๋™ํ•ด์„œ ๋ทฐํฌํŠธ ๋ฐ–์œผ๋กœ ๋งŽ์ด ๋‚˜๊ฐ€๋ฒ„๋ฆฌ๋ฉด
    // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰์ด ์•ˆ ๋  ์ˆ˜ ์žˆ๋‹ค.
    // ์™œ๋ƒํ•˜๋ฉด ์ด๋ฏธ ๋ทฐํฌํŠธ ๋ฐ–์œผ๋กœ ์ง€์ •ํ•œ ๊ต์ฐจ๋ฒ”์œ„ ์ด์ƒ์„ ๋‚˜๊ฐ€๋ฒ„๋ ธ๊ธฐ ๋•Œ๋ฌธ์— ์Šคํฌ๋กค๊ณผ ๊ด€๊ณ„์—†์ด ์‹คํ–‰๋˜์ง€ ์•Š์Œ
    // ๊ทธ๋Ÿด ๊ฒฝ์šฐ "threshold" ๊ฐ’์„ ์ค„์ด๊ฑฐ๋‚˜, "rootMargin"์„ ๋Š˜๋ฆฌ๋ฉด ๋œ๋‹ค.
    let observer = new IntersectionObserver(onScroll, { threshold: 0.1, rootMargin: "20px" });
    observer.observe(elementRef.current);

    return () => observer?.disconnect();
  }, [onScroll]);

  return {
    ref: elementRef,
    style: { opacity: 0, transform: handleDirection(direction) },
  };
};

export default useScrollFadeIn;

// <article {...useScrollFadeIn({ direction: "right", duration: 1.6 })}> ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉ

0๊ฐœ์˜ ๋Œ“๊ธ€