[React.js] 무한스크롤 feat. Intersection Observer API

apro_xo·2022년 6월 2일
4
post-thumbnail

적게 된 이유

개인적으로 뉴스 뷰어를 만들게 되었는데, 무한스크롤 구현에서 삽질을 너무 많이 해서 힘들었다. 결론적으로 구현 초기에는 Scroll Event를 이용하여 구현을 시도하였으나, 여러 문제에 봉착하게 되었다.😢
어떠한 문제가 있었냐면,,,,(물론 내가 구현을 잘못해서 문제가 발생한 것이다...)

  • 스크롤이 바닥에 닿았을 때, 닿았다는 인식은 잘한다.

  • 하지만 닿고 나서 axios로 서버에서 데이터를 가져올 때, 그 가져오는데 걸리는 시간 동안 스크롤은 계속 닿아있기 때문에 page를 저장하는 state를 1 씩 증가하길 원했으나 엄청나게 급격하게 증가하는 경우 발생.

다른 방법을 알아본 결과 Intersection Observer API를 알게 되었고, 공부하여 무한스크롤을 구현하게 되었다. 잊어버리지 않기 위해 기록한다.

1. Intersection Observer API

1-1. 이게 도대체 뭐야?

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.

MDN에서는 위와 같이 말하고 있다.

Intersection Observer API로 관찰하는 타겟 요소가 자신의 상위 요소와 교차되는지 관찰하고 있다가 교차되면 어떠한 동작을 취할 수 있는 기능을 제공해준다고 나는 이해를 했다.

1-2. 사용 케이스

  1. 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩

  2. 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현.

  3. 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.

  4. 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정.

MDN에서 나열한 사용 케이스들인데, 무한스크롤 뿐만 아니라 지연 로딩을 구현하는데도 사용할 수 있다니 좀 유용한 것 같다. 일단 나는 무한스크롤이 더 중요하다.

2. 무한스크롤 구현

위의 그림을 설명하면 반복되는 List가 있고, 마지막 List는 스크롤이 아직 끝까지 되지 않아 뷰포트 밖에 있다. 이 마지막 List가 Target 즉, 관찰 대상이다.🔥

Target이 뷰포트 밑에 닿았을 때, 새로운 데이터를 불러와 더하여 무한스크롤을 구현하는 아이디어다.

👉 또한, 무한스크롤되어 새로운 데이터가 기존 데이터에 합쳐지면 마지막 List가 바뀌기 때문에 유동적으로 Target을 변경해야한다.

3. Intersection Observer API 사용하기

3-1. callback 설명

let observer = new IntersectionObserver(callback, option)

IntersectionObserver 생성자는 callback함수와 option을 파라미터로 받는다.

3-1-1. callback함수는 어떻게 생겨먹었나?

callback(entries, observer)

callback함수는 target과 root가 교차 되었을 때 실행할 함수를 뜻한다.
root가 무엇인가❓ option 안에 있다. option은 3-2. 에서 설명하겠다.

entries는 IntersectionObserverEntry 인스턴스의 배열이다.
이 배열은 읽기 전용의 다음 속성들을 포함한다.

  • boundingClientRect : 관찰 대상의 사각형 정보

  • intersectionRect : 관찰 대상의 교차한 영역 정보

  • intersectionRatio : 관찰 대상의 교차한 영역 백분율

  • isIntersecting : 관찰 대상의 교차 상태 boolean타입

  • time : 변경이 발생한 시간 정보

  • target : 관찰 대상 요소

  • rootBounds : 지정한 루트 요소의 사각형 정보

3-2. option 설명

option은 객체인데, observer 콜백이 호출되는 상황을 조정할 수 있다.
option은 root, rootMargin, threshold 이렇게 세 개의 값을 가진다.

3-2-1. root 🔥

대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.

MDN에서 이렇게 말하고 있다.
즉, 관찰 대상(target)과 기준이 되는 대상(root)가 교차되는 것을 관찰하는게 Intersection Observer API이다.

3-2-2. rootMargin

root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.

3-2-3. threshold

observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.


threshold는 위 그림과 같이 이해하면 될 것 같다.

4. React(+Typescript) 적용

4-1. 코드 (NewsItem.tsx)

const NewsItem: React.FC<NewsItemProps> = ({ item, setPage, index, length }) => {
    const [target, setTarget] = useState<HTMLDivElement | null>(null);

    const onIntersect = ([entry]: IntersectionObserverEntry[], observer: IntersectionObserver)=>{
        if (entry.isIntersecting) {
            setPage(page => page + 1)
            // 데이터를 불러오고 화면에 렌더링 되기 전까지 계속 요건을 충족하므로
            // page가 무한대로 증가하는 경우 방지
            // 기존의 target을 unobserve함으로써 page가 한 번만 증가하게끔 함
            observer.unobserve(entry.target);
        }
    }

    useEffect(() => {
        let observer: IntersectionObserver;
        if (target) {
            observer = new IntersectionObserver(onIntersect, { threshold: 0.3 });
            observer.observe(target);
        }

        return (() => {
            observer && observer.disconnect();
        })
    }, [target])
  
  //...(생략)
  
  return (
        <>
            <div className='news-item-wrapper' onClick={clickHandler}
            ref={index === length - 1 ? setTarget : null}>
                <img src={item.urlToImage} alt="NewsThumbnailImage" />
                <div className="news-item-body">
                    <div className='article-info'>
                        <h1>{item.title}</h1>
                        <p className='news-item-content'>{item.content}</p>
                    </div>
                    <div className='news-info'>
                        <p>Author : {authorRename(item.author)}</p>
                        <p>Published : {slicePublishedAt(item.publishedAt)}</p>
                        <p>Source : {item.source.name}</p>
                    </div>

                </div>
                <div className='star-area'>
                    <p className='star'></p>
                </div>
            </div>
        </>
    )

4-2. 코드 설명

target과 root가 겹쳐지면 실행할 콜백함수 onIntersect에서 entry 중 isIntersecting이 true값이면 page를 1 증가시키고 관찰을 잠시 취소한다(unobserve). 취소하는 이유는 위 코드에 주석으로 설명을 해놓았다.

page를 1 증가하는 이유는 위 코드에서는 생략했지만 내가 사용한 API에서 한 페이지당 20개의 데이터를 불러오는데, 그 페이지를 증가시켜 다른 페이지의 데이터를 불러오기 위해 증가시킨다.

target이 변경되면 useEffect가 실행되는데, target이 존재하면 IntersectionObserver 객체를 만들고 원하는 target을 등록한다.(observe) 그리고 컴포넌트가 언마운트 될 때는 반드시 disconnect를 해주어야 한다.

관찰 하는 대상을 유동적으로 변경해주어야하기 때문에

<div className='news-item-wrapper' onClick={clickHandler}
            ref={index === length - 1 ? setTarget : null}>

아래와 같은 코드를 작성하여 target을 동적으로 변경할 수 있도록 했다.

profile
유능한 프론트엔드 개발자가 되고픈 사람😀

0개의 댓글