React-intersection-observer ์‚ฌ์šฉ

์œ ๋Œ•ยท2019๋…„ 7์›” 11์ผ
0

ํšŒ์‚ฌ

๋ชฉ๋ก ๋ณด๊ธฐ
6/8

๐Ÿ’กย ๊ณต์‹๋ฌธ์„œ

react-intersection-observer

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

  • ๋ทฐํฌํŠธ(viewport)๋ž€ย ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์—ฌ์ง€๊ณ  ์žˆ๋Š” ๋‹ค๊ฐํ˜•(๋ณดํ†ต ์ง์‚ฌ๊ฐํ˜•)์˜ ์˜์—ญ์ž…๋‹ˆ๋‹ค. ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ํ˜„์žฌ ์ฐฝ์—์„œ ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„(์ „์ฒดํ™”๋ฉด์ด๋ผ๋ฉด ํ™”๋ฉด ์ „์ฒด)์„ ๋งํ•ฉ๋‹ˆ๋‹ค.ย ๋ทฐํฌํŠธ ๋ฐ”๊นฅ์˜ ์ฝ˜ํ…์ธ ๋Š” ์Šคํฌ๋กค ํ•˜๊ธฐ ์ „์—” ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค

๐Ÿ’กย ์‚ฌ์šฉ ์ด์œ 

  1. ํ˜ธ์ถœ ์ˆ˜ ์ œํ•œ ๋ฐฉ๋ฒ• debounce, throttle์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

debounce์™€ย throttle์€ ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ๋ถˆํ•„์š”ํ•œ ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ˆ˜๋ฅผ ์ปจํŠธ๋กคํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์ธ๋ฐ ์ด๋“ค์ด ํ•„์š”ํ•œ ์ด์œ ๋Š”

window.addEventListener('scroll', function() {
   return console.log('scroll!');
});

์œ„์˜ ์ฝ”๋“œ๋ฅผ ์ฝ˜์†” ์ฐฝ์— ์ž…๋ ฅํ•˜๊ณ  ์Šคํฌ๋กค์„ ์กฐ๊ธˆ๋งŒ ํ•ด๋ณด๋ฉด ๋‹จ๋ฒˆ์— ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค. ์œ„๋กœ ํ˜น์€ ์•„๋ž˜๋กœ ์Šคํฌ๋กค์„ ํ•  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ์ˆ˜๋„ ์—†์ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋Œ€์ฒด๋กœ ์ด๋Ÿฐ ํ˜„์ƒ์„ ๋ชฉ์ ์œผ๋กœ ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ๊ฑฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฑท์žก์„ ์ˆ˜ ์—†์ด ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋ฅผ debounce์™€ throttle์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปจํŠธ๋กค ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  1. reflow๋ฅผ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์Šคํฌ๋กค ์ด๋ฒคํŠธ์—์„œ๋Š” ํ˜„์žฌ์˜ ๋†’์ด ๊ฐ’์„ ์•Œ๊ธฐ ์œ„ํ•ดoffsetTop์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ •ํ™•ํ•œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ๋งค๋ฒˆ layout์„ ์ƒˆ๋กœ ๊ทธ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. layout์„ ์ƒˆ๋กœ ๊ทธ๋ฆฐ๋‹ค๋Š” ๊ฒƒ์€ ๋ Œ๋” ํŠธ๋ฆฌ๋ฅผ ์žฌ์ƒ์„ฑํ•œ๋‹ค๋Š” ๋œป์ธ๋ฐ, reflow๋ผ๊ณ ๋„ ๋ถˆ๋ฆฌ๋Š” ์ด ๊ณผ์ •์ด ๋ฐ˜๋ณต๋˜๋ฉด ๋‹น์—ฐํžˆ ๋ธŒ๋ผ์šฐ์ €์˜ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋˜๊ณ  ํ™”๋ฉด์˜ ๋ฒ„๋ฒ…๊ฑฐ๋ฆผ์ด ์ƒ๊ธธ ์ˆ˜ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์†์„ฑ

root

๋ทฐํฌํŠธ๋กœ ๊ฐ„์ฃผ๋˜๋Š” ๋Œ€์ƒ์ž…๋‹ˆ๋‹ค.

rootMargin

ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๊ด€์ฐฐํ•  ๋ทฐํฌํŠธ์ธ ๋ฃจํŠธ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ถ•์†Œํ•˜๊ธฐ๋‚˜ ๋Š˜๋ ค์ค๋‹ˆ๋‹ค.

์•„๋ฌด๋Ÿฐ ์„ค์ •์„ ํ•˜์ง€ ์•Š์œผ๋ฉด deafult 0px 0px 0px 0px์ž…๋‹ˆ๋‹ค.

threshold

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๊ด€์ฐฐํ•˜๋Š” ๊ต์ฐจ ์˜์—ญ์—์„œ์˜ ๋น„์œจ(์ž„๊ณ„๊ฐ’)์„ ๋งํ•ฉ๋‹ˆ๋‹ค.

0~1๊นŒ์ง€์˜ ์ˆซ์ž๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ์ตœ๋Œ€ 1(100%)๊นŒ์ง€ ์ง€์ • ๊ฐ€๋Šฅํ•˜๋ฉฐ, 0.7์ด๋ผ ํ•˜๋ฉด ๋Œ€์ƒ์ด ํ™”๋ฉด์— 70% ์ด์ƒ ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ•  ๋•Œ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์„œ๋“œ

disconnect()

IntersectionObserver๊ฐ€ ๋Œ€์ƒ์„ ๊ด€์ฐฐํ•˜๋Š” ๊ฒƒ์„ ์ค‘์ง€

observe()

IntersectionObserver๊ฐ€ ๊ด€์ฐฐํ•  ๋Œ€์ƒ์„ ๊ฐ€๋ฅด์ณ์คŒ

unobserve()

IntersectionObserver๊ฐ€ ํŠน์ •์š”์†Œ์˜ ๊ด€์ฐฐ์„ ์ค‘์ง€

takeRecords()

IntersectionObserver๊ด€์ฐฐ๋œ ๋ชจ๋“  ๋Œ€์ƒ์—๋Œ€ํ•œ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜

๐Ÿ’กย InView ํƒœ๊ทธ

  • ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•
import { InView } from 'react-intersection-observer';

const Component = () => (
  <InView>
    {({ inView, ref, entry }) => (
      <div ref={ref}>
        <h2>{`Header inside viewport ${inView}.`}</h2>
      </div>
    )}
  </InView>
);

๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ InViewํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์œ„์™€ ๊ฐ™์ด ์ปดํผ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๋ฉด ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ๊ฐ์‹ผ ์ปดํผ๋„ŒํŠธ์—์„œ์˜ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ์ด๋˜๋ฉด ์ƒˆ๋กœ์šด ๊ฐ’์ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

  • inView

ํ˜„์žฌ ๊ฐ€ ๊ด€์ฐฐ๋˜๊ณ  ์žˆ๋Š”์ง€์—๋Œ€ํ•œ boolean๊ฐ’์ž…๋‹ˆ๋‹ค.

  • ref

๊ฐ€ ๊ฐ์‹ธ๊ณ ์žˆ๋Š” children์š”์†Œ์—์„œ IntersectionObserver๊ฐ€ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์š”์†Œ๋ฅผ ์ •ํ•ด์ค๋‹ˆ๋‹ค.

  • entry

์œ„ ๋‘๊ฐœ๋ฅผ ์ œ์™ธํ•˜๊ณ  ์กฐ๊ธˆ๋” ์„ธ๋ฐ€ํ•œ ์„ค์ •์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„์˜ ์Šคํฌ๋ฆฐ์ƒท์€ IntersectionObserverEntry์˜ ์ฝ˜์†”๊ฐ’ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

  • isIntersecting

intersectionObserver๊ฐ€ ๊ด€์ฐฐํ•˜๋Š” Root๋ฅผ ๊ต์ฐจํ• ๋•Œ์˜ ์ƒํƒœ๊ฐ’ ๋ณ€ํ™”๋ฅผ boolean๊ฐ’์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

  • time

๊ต์ฐจ๊ฐ’์ด ๋ณ€ํ™”ํ–ˆ์„๋•Œ์˜ timestamp๋ฅผ ๊ธฐ๋กํ•ด์ค๋‹ˆ๋‹ค.

์‰ฝ๊ฒŒ ์ƒ๊ฐํ•˜๋ฉด ์œ„์˜ isIntersecting์˜ ์ƒํƒœ๊ฐ’ ๋ณ€ํ™”ํ•œ ์ˆœ๊ฐ„์˜ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.

๐Ÿ‘‰ย ์‚ฌ์šฉ ์˜ˆ์‹œ

// components>featured>ItemList.tsx

import { InView } from 'react-intersection-observer'; // 1

const onTrackingViewItem = (inView: boolean, itemIdx: number, entry: any): any => {
    if (inView) {
      //ํ•„์š”ํ•œ ๋กœ์ง
    }
  };

				<InView key={index}> // 2
            {({ inView, ref, entry }) => ( // 3
              <S.ItemContainer>
                  <S.ItemSection
                    ref={ref} // 4
                    onChange={onTrackingViewItem(inView, itemIdx, entry)} //5
                    onClick={() => onItemClick(item)}>
                    <img src={item.media.imageUrl} className="itemImg" alt="์ƒํ’ˆ ์ด๋ฏธ์ง€" />
                    <S.ItemTitle>{item.title}</S.ItemTitle>
                    <S.ItemPrice>{item.property.price.text}</S.ItemPrice>
                    <SellState sellState={sellState} alt="์ƒํ’ˆ ์ƒํƒœ ์ด๋ฏธ์ง€" />
                  </S.ItemSection>
              </S.ItemContainer>
            )}
          </InView>

itemlist๊ฐ€ ๋ฟŒ๋ ค์ง€๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€ ๋งˆ๋‹ค ๊ด€์ฐฐ์ด ํ•„์š”ํ–ˆ๋‹ค.

์œ ์ €๊ฐ€ ์–ด๋–ค ์ƒํ’ˆ์„ ๋ณด์•˜๋Š”์ง€ ์–ด๋–ค์ƒํ’ˆ์„ ํด๋ฆญํ•˜์˜€๋Š”์ง€ ๋“ฑ๋“ฑ

  • ์ฃผ์„1๋ฒˆ

ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ ํ•„์š”ํ•œ ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ์ปดํผ๋„ŒํŠธ ํ˜ธ์ถœ์„ ํ•ด์ค๋‹ˆ๋‹ค.

  • ์ฃผ์„2๋ฒˆ

importํ•ด์˜จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ children์œผ๋กœ ํ•„์š”ํ•œ ์š”์†Œ๋“ค์™ธ๋ถ€์—์„œ ๊ฐ์‹ธ์ค๋‹ˆ๋‹ค.

  • ์ฃผ์„3๋ฒˆ

ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ปดํผ๋„ŒํŠธ์—์„œ ํ•„์š”ํ•˜๋Š” state๊ฐ’์„ ํ˜ธ์ถœํ•˜์—ฌ children์™ธ๋ถ€์—์„œ ๊ฐ์‹ธ์ค๋‹ˆ๋‹ค.

  • ์ฃผ์„4๋ฒˆ

intersectionObserver๊ฐ€ ๊ด€์ฐฐํ•  ๋ถ€๋ถ„์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.

  • ์ฃผ์„5๋ฒˆ

๋‚ด๋ถ€ ํ”„๋กœ์ ํŠธ์—์„œ ํ•ด๋‹น ์ƒํƒœ๋ณ€ํ™”์— ๋”ฐ๋ผ ํ•„์š”๋กœํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’กย InfinitieScroll Hooks

ํฌ๊ฒŒ ๊ด€์ฐฐ์ž(observer) ์™€ ๊ด€์ฐฐ ๋Œ€์ƒ(entry), ์˜ต์…˜(์กฐ๊ฑด) ๊ทธ๋ฆฌ๊ณ  ์ฝœ๋ฐฑํ•จ์ˆ˜(๋กœ์ง)๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

  1. ๊ด€์ฐฐ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ด€์ฐฐ ๋Œ€์ƒ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  3. ๊ด€์ฐฐ์ž๋Š” ๊ด€์ฐฐ ๋Œ€์ƒ์„ ๊ด€์ฐฐํ•ฉ๋‹ˆ๋‹ค
  4. ๊ด€์ฐฐ ๋Œ€์ƒ์ด ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ƒํƒœ์— ๋†“์ด๊ฒŒ ๋œ๋‹ค๋ฉด ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
// hooks/useInfiniteScroll.tsx

const useInfiniteScroll = (props: UseInfiniteScrollProps) => {
  const { bottom, moreItemList } = props;
  const [page, setPage] = useState(1); 

  useEffect(() => {
    const options = {
      rootMargin: '30px', 
      threshold: 1 
    };
    // ๊ด€์ฐฐ์ž๋ฅผ ์ƒ์„ฑ
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        setPage((prev) => prev + 1);
      }
    }, options);
    observer.observe(bottom.current);
  }, []);

export default useInfiniteScroll;

๐Ÿ‘‰ย ์‚ฌ์šฉ ์˜ˆ์‹œ

// components/featured/ItemList.tsx
import React, { useState, useRef } from 'react';
import useInfiniteScroll from 'hooks/useInfiniteScroll';

// ๊ด€์ธก ๋Œ€์ƒ ์ƒ์„ฑ
const bottom = useRef(null);

// ๋ฌดํ•œ ์Šคํฌ๋กค ํ›…์Šค ์‚ฌ์šฉ
const page = useInfiniteScroll({ bottom, moreItemList });

// ์•„์ดํ…œ ๋ชฉ๋ก ์ƒˆ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  const moreItemList = () => {
    const data = {
      page: page,
      limit: 50
    };
    const query = querystring.stringify(data);
    fetcher
      .get(`${ApiPath.featured.featured}/${featuredIdx}?${query}`)
      .then((res) => {
        const newItemList = res.data.list;
        setTotalItemList((totalItemList) => totalItemList.concat(newItemList));
      })
      .catch((error) => {
        console.error(error.message);
      });
  };

return (
// ๊ด€์ธก ๋Œ€์ƒ ์„ ์–ธ
	<div>
	blah blah...
	<div ref={bottom} />
)
  1. Intersection Observer ์˜ ์กฐ๊ฑด์œผ๋กœ ๋ฌด์—‡์„ ๋„ฃ์–ด์ค„ ๊ฒƒ์ธ๊ฐ€?
    โ†’ ์Šคํฌ๋กค๋ฐ”๊ฐ€ ๋ฐ”๋‹ฅ์— ๋‹ฟ์œผ๋ฉด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ๋งŒ๋“ค์ž.
  2. ๊ด€์ฐฐ ๋Œ€์ƒ ์€ ๋ฌด์—‡์ธ๊ฐ€?
    โ†’ ๊ด€์ฐฐ ๋Œ€์ƒ์€ ๋ฆฌ์ŠคํŠธ์˜ ๋งจ ์•„๋ž˜์— div ํƒœ๊ทธ๋กœ ์„ ์–ธํ•œ๋‹ค.
  3. ์–ด๋–ค ๋กœ์ง(์ฝœ๋ฐฑํ•จ์ˆ˜) ๋ฅผ ๋„ฃ์–ด์ค„ ๊ฒƒ์ธ๊ฐ€?
    โ†’ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋กœ์ง(moreItemList)์„ ๋„ฃ๋Š”๋‹ค.

๐Ÿ˜ฑ velog ์‹œ์ž‘ ์ „ ๋ธ”๋กœ๊ทธ(tistory) ๋งํฌ

https://yoohyeon.tistory.com/

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