카테캠에서 사용한 무한 스크롤을 여기에서도 한 번 사용해볼 것이다.
매치 정보가 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일 때만 무한 스크롤이 발생하도록 한다.
무한 스크롤은 작동 방식은 간단해 보여도 실제로 적용하려니 어렵다.
'REACT > 롤 전적 사이트' 카테고리의 다른 글
| [4] 스타일 꾸미기 (7) | 2025.08.19 |
|---|---|
| [3] 매치 정보 가져오기 (9) | 2025.08.13 |
| [2] puuid를 이용하여 소환사 정보 받아오기 (1) | 2025.08.06 |
| [1] useQuery를 이용하여 puuid 받기 (4) | 2025.08.06 |