[React] Intersection Observer API 활용해서 무한스크롤 기능 구현하기

김채운·2022년 10월 23일
0

React

목록 보기
12/26

무한스크롤을 구현하는 방법으로는 scroll이벤트인 onScroll을 사용하는 방법이 있고, Intersection Observer API를 활용하는 방법이 있다.
하지만 onScroll을 사용하면 scroll이 일어날때마다 이벤트가 실행돼서 성능 저하의 문제가 생기기 때문에 Intersection Observer API를 활용한 무한스크롤 기능을 구현했다.

Intersection Observer API

Intersection Observer API는 Javasctipt 내장 API로, 기본적으로 브라우저 뷰포트와 설정한 요소의 교차점을 관찰해서 target 요소가 root 요소와 교차가 일어나는지(뷰포트에 포함되는지 안 되는지)를 판단하여 콜백 함수를 실행할 수 있다. 따라서, target 요소를 적절한 위치에 배치를 하면 사용자가 스크롤을 이동하는 것에 따라 데이터 요청을 할 수 있다.
사용법은 객체 상태변화를 관찰하는 관찰자들(옵저버)을 등록하여 상태변화가 있을 때마다 각 옵저버에 알리는 방식이다.

➡️ 교차지점을 알려줄 요소를 지정한다.

   <Container>
            {
                list.map((p, i) => {
                    return <div key={i}>
                        <img src={p.image} alt="상품 이미지"/>
                        <p className='product-name'>{p.seller_store}</p>
                        <p className='product'>{p.product_name}</p>
                        <span className='product-price'>{p.price}</span>
                        <span></span>
                    </div>
                })
            }
            {moreData ? <div ref={target}></div> : null}
    </Container>
  • 배열이 일어나는 요소 다음에 ref를 사용해서 관찰하려는 엘리먼트를 지정해준다. 배열이 끝나고 교차점을 알려줘서 다음 데이터를 받아오게끔 한다.

➡️ observer 인스턴스 생성해주기

  useEffect(() => {
        let observer;
        if (target.current) {
            const handleInterSect = async ([entry], observer) => {
                if (entry.isIntersecting) {
                    observer.unobserve(entry.target); // 타겟요소에 대한 옵저버를 멈춘다. Lazy Loading이 시작되면 관찰을 멈추는 등의 용도가 있다.
                    await getData();
                    observer.observe(entry.target)
                }
            };
            observer = new IntersectionObserver(handleInterSect, { threshold: 0.6, });
            observer.observe(target.current) // 타겟요소에 대한 Intersection Observer 를 등록한다.
        }
        return () => observer && observer.disconnect(); //  다중 타겟요소들의 옵저빙을 동시에 멈추기 위해 사용되는 메서드다.
    }, [target, page])
  • 컴포넌트 렌더가 완료됐을 때 observer가 생성되어야 해서 useEffect를 활용해준다.
    또한 target이 생성되기 전에 observer를 시작할 수 없으므로 target을 가리키고 있을 때 실행되도록 조건문을 넣어줬다. 그리고 이 observer 인스턴스에는 감시할 target 요소와 타겟 요소가 들어왔을 때 실행할 함수를 등록해주면 된다.

Intersection Observer는 아래와 같은 문법으로 인스턴스를 생성한 뒤 사용한다.

 observer = new IntersectionObserver(callback, options);

callback : 교차시에 실행되는 함수이다. 로딩구현이나 패치 등의 함수가 통상 할당된다.
options : Intersection Observer에 관한 설정을 할 수 있는 부분이다.
root : 교차를 감지하는 root 요소. observe로 등록할 요소의 상위요소여야 한다. 기본값은 null(이 땐 브라우저 viewport)
rootMargin : root 요소의 마진값. 기본값은 0px.
threshold : 0.0 ~ 1.0 사이의 숫자들을 배열로 받는다. 이는 %로 치환되어, 해당 비율만큼 교차된 경우 콜백이 실행된다.

➡️ 콜백함수 생성

  • 위의 코드의 handleInterSect 부분으로 교차상태가 변화했을 때, 교차된 target인 [entry]의 속성중 하나인 intersecting(관찰대상자의 교차상태)을 활용해서 교차상태가 true일 때, 데이터를 요청할 수 있도록 함수를 생성했다.
    해당 콜백함수는 entries와 observer를 인자로 받는데, entries는 IntersectionObserverEntry 인스턴스의 배열로
    관찰하는 요소의 정보와 루트 요소의 정보가 들어있다. 루트요소는 지정하지 않으면 default로 html이 된다. entry는 위의 intersecting처럼 여러가지 속성을 가지고 있다.
    여기서 unobserve는 사용자의 데이터 요청이 완료되기 전에 교차 상태를 여러 번 변화시키는 상황이 발생하지 않도록 하기 위해 관찰을 중단하기 위해 사용한 것이고 데이터 요청이 완료 됐을 때 다시 관찰하도록 하기 위해 observe를 다시 지정해주었다.

👇참고 entry 요소

boundingClientRect : 관찰 대상의 사각형 정보(DOMRectReadOnly)
intersectionRect : 관찰 대상의 교차한 영역 정보(DOMRectReadOnly)
intersectionRatio : 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)
isIntersecting : 관찰 대상의 교차 상태(Boolean)
rootBounds : 지정한 루트 요소의 사각형 정보(DOMRectReadOnly)
target : 관찰 대상 요소(Element)
time : 변경이 발생한 시간 정보(DOMHighResTimeStamp)

➡️ Data 요청

 const getData = async () => {
        await api.get(`/products/?page=${page}`).then((res) => {
            setList((prev) => prev.concat(res.data.results))//리스트 추가
            setPage(prev => prev + 1)
        }).catch((error) => {
            setMoreData(false)
            return;
        })
    }
  • api get 요청을 통해서 요청하고 데이터를 받아옴에 따라 page도 +1을 해줘서 다음 api요청때 다음의 페이지로 데이터 요청을 보낼 수 있게 한다. 또한, 데이터를 이전 데이터 배열에 concat으로 합쳐줌으로서 배열이 업데이트 되고, 그에따라 렌더가 다시 이루어진다.
    error 부분의 setMoreData는 더이상 불러올 페이지가 없어서 데이터를 불러오지 못할 경우의 조건을 만들기 위해 사용했다.
    그래서 이 state는 아래처럼 교차점 요소를 지정할 때 사용해서 불러올 데이터(moreData)가 없으면(false) target 요소가 생기지 않도록 조건을 걸어놨다.
 {moreData ? <div ref={target}></div> : null}

➡️ 전체코드

function MainGrid() {

    const [page, setPage] = useState(1)
    const [list, setList] = useState([])
    const [moreData, setMoreData] = useState(true)
    const target = useRef(null);

    const getData = async () => {
        await api.get(`/products/?page=${page}`).then((res) => {
            setList((prev) => prev.concat(res.data.results))
            setPage(prev => prev + 1)
        }).catch((error) => {
            setMoreData(false)
            return;
        })
    }

    useEffect(() => {
        let observer;
        if (target.current) {
            const handleInterSect = async ([entry], observer) => {
                if (entry.isIntersecting) {
                    observer.unobserve(entry.target);
                    await getData();
                    observer.observe(entry.target)
                }
            };
            observer = new IntersectionObserver(handleInterSect, { threshold: 0.6, });
            observer.observe(target.current)
        }
        return () => observer && observer.disconnect();
    }, [target, page])

    return (
        <Container>
            {
                list.map((p, i) => {
                    return <div key={i}>
                        <img src={p.image} alt="상품 이미지" />
                        <p className='product-name'>{p.seller_store}</p>
                        <p className='product'>{p.product_name}</p>
                        <span className='product-price'>{p.price}</span>
                        <span></span>
                    </div>
                })
            }
            {moreData ? <div ref={target}></div> : null}
        </Container>
    )
}

참고 👇

0개의 댓글