본문 바로가기
REACT/롤 전적 사이트

[5] 무한 스크롤 구현하기

by 쪼꼬에몽 2025. 8. 21.

카테캠에서 사용한 무한 스크롤을 여기에서도 한 번 사용해볼 것이다.

매치 정보가 5개씩 뜨는데 더보기를 누르기 불편하다.

따라서 5개까지 내리면 새로 데이터를 받는 식으로 무한 스크롤을 나타낼 것이다.

import { useEffect, useRef } from 'react';

interface InfiniteScrollObserverProps {
  onIntersect: () => void;
  enabled?: boolean;
  rootMargin?: string;
  threshold?: number;
}

export default function InfiniteScrollObserver({
  onIntersect,
  enabled = true,
  rootMargin = '0px',
  threshold = 1.0,
}: InfiniteScrollObserverProps) {
  const targetRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!enabled || !targetRef.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          onIntersect();
        }
      },
      { rootMargin, threshold }
    );

    observer.observe(targetRef.current);

    return () => {
      if (targetRef.current) {
        observer.unobserve(targetRef.current);
      }
    };
  }, [enabled, onIntersect, rootMargin, threshold]);

  return <div ref={targetRef} style={{ height: '1px' }} />;
}

 

무한 스크롤을 나타내는 공통 컴포넌트이다.

페이지 맨 아래에 보이지 않는 div를 세워두고, 이 div가 화면에 보이면 부모에게 알려 다음 페이지의 데이터를 불러오게 한다.

interface InfiniteScrollObserverProps {
  onIntersect: () => void;
  enabled?: boolean;
  rootMargin?: string;
  threshold?: number;
}

onIntersect

  • div가 화면에 보였을 때 할 행동
  • 여기서는 보여줄 아이템 개수 5개 늘리기

enabled

  • true 일 때만 감시를 시작
  • 없으면 컴포넌트가 처음 렌더링되자마자 div가 보여서 데이터가 연쇄 로드 됨.
  • 불러올 데이터가 없으면 false로 바꿔 자원 절약함

rootMaring

  • 감지 영역을 확장하거나 축소하는 CSS margin 값
  • 실제 화면에 보이기 100px 전부터 보인 것으로 간주
  • 페이지 맨 끝에 도달하기 직전에 미리 데이터를 로드함

threshold

  • 감시 대상이 몇 % 보여야 onIntersect를 호출할지 결정
const targetRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
  // 감시 시점 확인
  if (!enabled || !targetRef.current) return;

  // observer는 특정 요소가 화면에 나타나는지 감시 
  const observer = new IntersectionObserver(
    // 감시 상태에 변화가 생길 때마다 호출되는 콜백 함수 
    (entries) => {
      // 감시하던 요소(entries[0])가 현재 화면에 보이는지 확인 
      if (entries[0].isIntersecting) {
        onIntersect(); // 보이면 onIntersect 함수 실행 
      }
    },
    { rootMargin, threshold }
  );

  // 생성된 obsever에게 이제부터 targetRef가 연결된 실제 div 요소를 감시 시작해라고 명령함 
  observer.observe(targetRef.current);

  // useEffect가 재실행 또는 화면에서 사라질 때 호출됨 
  return () => {
    if (targetRef.current) {
      // 이전 observer는 감시를 중단해라고 명령함 
      // 검사 중복 방지 
      observer.unobserve(targetRef.current);
    }
  };
}, [enabled, onIntersect, rootMargin, threshold]);

targetRef

  • 실제 브라우저의 HTML div 요소에 직접 접근할 수 있는 연결고리 생성
  • div를 감시병으로 사용 

useEffect

  • React의 렌더링 사이클 외부에서 브라우자 API(IntersectionObserver)와 상호작용하기 위해 사용
<InfiniteScrollObserver
  onIntersect={() => setVisibleMatchId((prevCount) => prevCount + 5)}
  enabled={visibleMatchId < (matchId?.length ?? 0)}
  rootMargin="100px"
/>

 

무한 스크롤 컴포넌트를 적용한 것이다.

이제 실행을 해보았다.

문제가 발생했다.

한 번에 25개의 데이터가 화면에 보여졌다.

5개 씩 보이도록 했는데 25개가 처음에 한 번에 보여졌다.

아마 처음 렌더링할 때 div가 계속 감지되어 반복적으로 실행된 것으로 보인다.

 

찾아보니,

1. 초기 렌더링

visibleMatchId의 초기값은 5.

Home 컴포넌트는 5개의 MatchInfo 컴포넌트와 그 바로 아래에 1px Observer div 렌더링함.

2. Observer 즉시 감지

모니터는 5개의 match 카드보다 길다.

페이지 맨 아래에 있는 Obsever div가 렌더링되자마자 isIntersecting 상태가 됨.

3. 1차 연쇄 호출

감지되자마자 onIntersect 함수가 실행됨.

setVisibleMatchId가 호출되어 visibleMatchId 상태가 5 -> 10이 됨.

4. 2차 연쇄 호출

상태가 변경되어 리렌더링이 발생함.

10개의 MatchInfo가 렌더링됨.

10개로도 화면이 다 차지 않으면, Observer div는 여전히 화면에 보이는 상태.

이 과정이 <MatchInfo> 컴포넌트들이 쌓여 화면을 가득 채우고 Observer div를 화면 밖으로 밀어낼 때까지 번개처럼 빠르게 반복됨.

25개가 되었을 때 Observer가 화면 밖으로 밀려 호출이 멈춤.

 

이를 해결하기 위해서는 맨 처음 렌더링 시 Observer를 비활성화했다가, 스크롤 준비가 되었을 때 활성화해야 한다.

useEffect(() => {
  if (!matchIdIsLoading && visibleMatchIds.length > 0) {
    const timer = setTimeout(() => {
      setObserverEnabled(true);
    }, 1000);

    return () => clearTimeout(timer);
  }
}, [matchIdIsLoading, visibleMatchIds.length]);

데이터 로딩이 완료된 후 Observer를 활성화 한다.

1초 뒤에 Observer를 활성화하여 초기 렌더링 시의 연쇄 호출을 방지한다.

컴포넌트 언마운트 시에는 타이머를 제거한다.

<InfiniteScrollObserver
  onIntersect={() => setVisibleMatchId((prevCount) => prevCount + 5)}
  enabled={observerEnabled && visibleMatchId < (matchId?.length ?? 0)}
  rootMargin="50px"
/>

observerEnabled가 true일 때만 무한 스크롤이 발생하도록 한다.

무한 스크롤은 작동 방식은 간단해 보여도 실제로 적용하려니 어렵다.