๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ—‚ WIL/๐Ÿ“ React

Scroll ์ด๋ฒคํŠธ ๋Œ€์‹  Intersection Observer API ์ด์šฉํ•˜์ž!

by nalong 2022. 8. 24.

๐Ÿ“… ๋ณธ ๊ธ€์€ 2022๋…„ 7์›” 23์ผ ๊ฐœ์ธ github์— ์ž‘์„ฑ๋œ ๊ธ€์ž…๋‹ˆ๋‹ค.

๐Ÿš€ Scroll ์ด๋ฒคํŠธ ๋Œ€์‹  Intersection Observer API ์ด์šฉํ•˜์ž!

 3์ฐจ ํ”„๋กœ์ ํŠธ ๋ฆฌํŒฉํ† ๋ง! 

๐Ÿ–ฑ Scroll ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค!

์ „์ฒด ์ผ๊ธฐ ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€๋ฅผ ์ปค์„œ ๊ธฐ๋ฐ˜ ๋ฌดํ•œ ์Šคํฌ๋กค๋กœ ๊ตฌํ˜„ํ•˜์˜€๋Š”๋ฐ, ์ด๋•Œ window ๊ฐ์ฒด์— scroll ์ด๋ฒคํŠธ๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ํŠน์ • ์ง€์ ์— ์Šคํฌ๋กค์ด ๋˜์—ˆ์„ ๋•Œ, ์„œ๋ฒ„์— ๋‹ค์Œ ์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ์š”์ฒญํ•˜๊ณ  ์ด๋•Œ ํŒŒ๋ผ๋ฏธํ„ฐ์— cursor ๊ฐ’์„ ๋ณด๋‚ด๋Š” ํ˜•์‹์œผ๋กœ ํ•˜์˜€๋‹ค!

// handleScroll.js
export const handleScroll = () => {
  const scrollHeight = document.documentElement.scrollHeight;
  const scrollTop = document.documentElement.scrollTop;
  const clientHeight = document.documentElement.clientHeight;
  if (scrollTop + clientHeight >= scrollHeight) {
    return true; // true ๊ฐ’ ๋ฐ˜ํ™˜
  }
};
//  EmotionList.js
  ...
  const [isLoaded, setIsLoaded] = useState(true); 
  const [stop, setStop] = useState(false);   
  useEffect(() => {
    if (isLoaded && !stop) { 
      getList();
    }
  }, [isLoaded]);

  useEffect(() => {
    window.addEventListener(
      'scroll',
      function (event) {
        const res = handleScroll(event);
        if (res === true) { // true ๊ฐ’์ด ๋ฐ˜ํ™˜๋  ๋•Œ 
          setIsLoaded(true); // setIsLoaded(true)๋กœ ์ƒํƒœ ๋ณ€๊ฒฝ
        }
      },
      false
    );
  }, []);

    const getList = async () => {
    if (isLoaded === true) {
      try {
        const res = await Api.get(`diary/list/?cursor=${cursor}`);
        const length = res.data.length;
        const sliceData = res.data.slice(0, length - 1);
        setCursor(res.data.slice(-1)[0].cursor);
        setDiaryList((data) => [...data, ...sliceData]);
        setIsLoaded(false);
        if (length < 10) {
          setStop(true);
        }
      } catch (err) {
        snackBar('info', '๋” ์ด์ƒ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ');
      }
    }
  };

๐Ÿ”ฅ ๋ฌธ์ œ

  1. ์š”์ฒญํ•  ์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ๋Š” ๋” ์ด์ƒ ์—†์ง€๋งŒ, ์Šคํฌ๋กค๋กœ ์ธํ•ด ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ ๋ฐœ์ƒ.
  2. ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ด๋ฒคํŠธ๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์„ฑ๋Šฅ์— ์ข‹์ง€ ์•Š์Œ.

๐Ÿ’ก ๋ฌธ์ œ -1 ํ•ด๊ฒฐ 

stop ์ด๋ผ๋Š” ์ƒํƒœ๊ฐ’ ์„ค์ •

์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ›์•„์˜จ ์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ๊ฐ€ 10๊ฐœ ๋ฏธ๋งŒ์ด๋ผ๋ฉด(์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ 10๊ฐœ์”ฉ ๋ฐ›์•„์˜ด.)
๋” ์ด์ƒ ์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†๋‹ค๋Š” ๋œป์ด๊ธฐ ๋•Œ๋ฌธ์— stop ์ƒํƒœ๊ฐ’์„ true๋กœ ๋ณ€๊ฒฝ!
์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ๋„, stop ๊ฐ’์ด true ์ด๊ธฐ๋•Œ๋ฌธ์— getList ํ•จ์ˆ˜ ํ˜ธ์ถœ X.

 const [stop, setStop] = useState(false); 
  
  useEffect(() => {
    if (isLoaded && !stop) {  // stop ๊ฐ’์ด false ์ผ๋•Œ๋งŒ ํ˜ธ์ถœ.
      getList();
    }
  }, [isLoaded]);

  useEffect(() => {
    window.addEventListener(
      'scroll',
      function (event) {
        const res = handleScroll(event);
        if (res === true) { // true ๊ฐ’์ด ๋ฐ˜ํ™˜๋  ๋•Œ 
          setIsLoaded(true); // setIsLoaded(true)๋กœ ์ƒํƒœ ๋ณ€๊ฒฝ
        }
      },
      false
    );
  }, []);

    const getList = async () => {
    if (isLoaded === true) {
      try {
        const res = await Api.get(`diary/list/?cursor=${cursor}`);
        const length = res.data.length;
        const sliceData = res.data.slice(0, length - 1);
        setCursor(res.data.slice(-1)[0].cursor);
        setDiaryList((data) => [...data, ...sliceData]);
        setIsLoaded(false);
        if (length < 10) {
          setStop(true);
        } 
     // ๋งŒ์•ฝ ์„œ๋ฒ„์—์„œ ์ „๋‹ฌ๋ฐ›์€ ์ผ๊ธฐ๋ฆฌ์ŠคํŠธ๊ฐ€ 10๊ฐœ ๋ฏธ๋งŒ์ด๋ผ๋ฉด
     // ์ผ๊ธฐ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋” ์ด์ƒ์—†๋‹ค๋Š” ๋œป -> stop ์ƒํƒœ๊ฐ’ true ๋กœ ๋ณ€๊ฒฝ. 
      } catch (err) {
        snackBar('info', '๋” ์ด์ƒ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ');
      }
    }
  };

๋ฌธ์ œ 1์ด ์™„๋ฒฝํ•˜๊ฒŒ ํ•ด๊ฒฐํ•˜์ง„ ๋ชปํ–ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž์˜ ์ผ๊ธฐ๊ฐ€ 30๊ฐœ๋ผ๋ฉด? 10 -> 10 -> 10
๊ธธ์ด๋Š” 10๊ฐœ ์ด๊ธฐ๋•Œ๋ฌธ์— stop ์ƒํƒœ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š”๋‹ค.

๐Ÿค” ๋ฌธ์ œ -2 ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์€ ?

๊ณ ๋ฏผ ๋์— Throttle ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์ ํ™” ์ž‘์—…์„ ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค.

๐Ÿ“ Throttle ?

์ด๋ฒคํŠธ์— ์˜ํ•œ ์ฝœ๋ฐฑ์„ ์ผ์ • ์‹œ๊ฐ„ ๋’ค์— ํ˜ธ์ถœ.
์ด๋Ÿฐ ์„ฑ์งˆ์„ ์ด์šฉํ•ด์„œ, scroll ์ด๋ฒคํŠธ์— ์ ์šฉํ–ˆ์„ ๋•Œ ์„ค์ •ํ•œ ์‹œ๊ฐ„๋™์•ˆ์€ ์Šคํฌ๋กค์„ ํ•˜๋”๋ผ๋„ ์ฝœ๋ฐฑ์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.

โญ๏ธ Intersection Observer API โญ๏ธ

Throttel ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋˜ ์ค‘ Intersection Observer API ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค!

โœ”๏ธ ๊ต์ฐจ ๊ด€์ฐฐ์ž API(Intersection Observer API)๋Š” ์ƒ์œ„์š”์†Œ ๋˜๋Š” ์ตœ์ƒ์œ„ ๋ฌธ์„œ์˜ ๋ทฐํฌํŠธ ์™€
๋Œ€์ƒ ์š”์†Œ์˜ ๊ต์ฐจ์ ์—์„œ ๋ณ€ํ™”๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•œ๋‹ค.

์‰ฝ๊ฒŒ ๋งํ•ด, scroll ์ด๋ฒคํŠธ์™€ ๋‹ฌ๋ฆฌ ํƒ€๊ฒŸ ์š”์†Œ๊ฐ€ ๋‹ค๋ฅธ ์š”์†Œ์— ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜ ๋‚˜๊ฐˆ ๋•Œ or
์ง€์ •ํ•œ ๋งŒํผ ๋‘ ์š”์†Œ์˜ ๊ต์ฐจ ๋ถ€๋ถ„์ด ๋ณ€๊ฒฝ๋ ๋•Œ๋งˆ๋‹ค ๋“ฑ๋กํ•œ ์ฝœ๋ฐฑํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค!
=> ๋ฌธ์ œ 2 ํ•ด๊ฒฐ!

โš™๏ธ ์ˆ˜์ •ํ•œ ์ฝ”๋“œ

  useEffect(() => {
    let observer;
    if (target) {
      observer = new IntersectionObserver(getList, {
        threshold: 0.7,
      });
      observer.observe(target);
    }
    return () => observer && observer.disconnect();
  }, [target]);

  const getList = async ([entry]) => {
    if (entry.isIntersecting && !stop) {
      try {
        setLoading(true);
        const res = await Api.get(`diary/list/?cursor=${cursor}`);
        if (res.data.length === 0) {
          setStop(true);
          setLoading(false);
        } else {
          const length = res.data.length;
          const sliceData = res.data.slice(0, length - 1);
          setCursor(res.data.slice(-1)[0].cursor);
          setDiaryList((data) => [...data, ...sliceData]);
          setLoading(false);
        }
      } catch (err) {
        snackBar('info', '๋” ์ด์ƒ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ');
      }
    }
  };

๐Ÿ–‡ Reference

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API