๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
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๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ณ 
์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•ด ๋” ๋™์ ์ธ ์›น์„ ๊ฐœ๋ฐœํ•ด๋ด์•ผ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

์˜คํ”ˆ ์ฑ„ํŒ