
https://react-query-v3.tanstack.com/guides/infinite-queries#_top
정리하기
https://blog.openreplay.com/infinite-scrolling-with-react-query/
이 블로그에 리액트 쿼리 useInfiniteQuery랑 무한 스크롤 구현하는 법 다 나와있음...!! 따라 쓰면서 무작정 적용은 했는데 다 영어라 모르겠어서 대충 번역해서 이해해야겠다
React Query는 무한 스크롤을 위한 useInfiniteQuery 훅을 제공함. useQuery와 비슷하며 몇 가지 차이점은:
data.pageParams를 사용한 pageParams 액세스. 다른 하나는 가져온 페이지를 포함하는 배열인 data.pages를 사용한 pages accessgetNextPageParam 및 getPreviousPageParam이 있습니다.fetchNextPage와 fetchPreviousPage 함수는 각각 다음 페이지와 이전 페이지를 가져오기 위한 반환 속성으로 포함되어 있습니다.hasNextPage 및 hasPreviousPage 부울 속성이 반환되어 다음 또는 이전 페이지가 있는지 확인합니다.isFetchingNextPage 및 isFetchingPreviousPage boolean 속성이 반환되어 다음 페이지 또는 이전 페이지를 가져올 때 확인합니다.무한 스크롤을 구현하려면 위의 몇 가지 options/properties만 사용하면 됩니다. 먼저 useQuery 후크를 useInfiniteQuery로 교체하고 starter app에서 데이터를 가져오는 데 사용되는 함수도 업데이트합니다. 이렇게 하려면 App.js 파일로 이동하여 다음 import를 추가하고 App 컴포넌트에서 fetchRepositories 및 useQuery를 다음 코드로 바꿉니다.
// App.js
import { useInfiniteQuery } from "react-query"
function App() {
const LIMIT = 10
const fetchRepositories = async (page) => {
const response = await fetch(`https://api.github.com/search/repositories?q=topic:react&per_page=${LIMIT}&page=${page}`)
return response.json()
}
const {data, isSuccess, hasNextPage, fetchNextPage, isFetchingNextPage} = useInfiniteQuery(
'repos',
({pageParam = 1}) => fetchRepositories(pageParam),
{
getNextPageParam: (lastPage, allPages) => {
const nextPage = allPages.length + 1
return nextPage
}
}
)
return (
<div className="app">
{isSuccess && data.pages.map(page =>
page.items.map((comment) => (
<div className='result' key={comment.id}>
<span>{comment.name}</span>
<p>{comment.description}</p>
</div>
))
)}
</div>
);
}
이제 가져온 결과가 표시되어야 합니다. 무한 스크롤이 작동하려면 페이지 맨 아래로 스크롤할 때마다 fetchNextPage 함수를 호출해야 합니다. 페이지 맨 아래에 도달하는 시점을 확인하기 위해 브라우저 Scroll 이벤트 또는 Intersection Observer API를 사용하여 이를 수행할 수 있습니다. 다음 섹션에서 이 두 가지를 모두 다룰 것입니다.
// App.js
import { useEffect } from 'react';
useEffect(() => {
let fetching = false;
const handleScroll = async (e) => {
const {scrollHeight, scrollTop, clientHeight} = e.target.scrollingElement;
if(!fetching && scrollHeight - scrollTop <= clientHeight * 1.2) {
fetching = true
if(hasNextPage) await fetchNextPage()
fetching = false
}
}
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [fetchNextPage, hasNextPage])
위의 코드에서, document에 스크롤 이벤트 리스너를 추가했습니다. 이 리스너는 실행될 때 handleScroll 함수를 호출합니다. 여기에서 scrollingEvent 속성을 사용하여 페이지 맨 아래에 도달했을 때를 감지 한 다음 hasNextPage가 true면 fetchNextPage를 호출하여 다음 페이지를 가져옵니다.
지금은 getNextPageParam 옵션에서 다음 페이지를 가져오기 위한 값인 매개변수를 반환하기 때문에 hasNextPage는 항상 true입니다. hasNextPage가 false가 되려면 getNextPageParam에서 undefined 또는 다른 거짓 값을 반환해야 합니다. 나중에 이 작업을 수행하여 맨 아래로 스크롤한 후 fetching data events를 중지하는 기능을 만들 것입니다.
이를 통해 앱을 시작하고 페이지 하단으로 스크롤할 때 새 데이터를 가져옵니다.
Intersection Observer API는 포함하는 root element 또는 viewport를 기준으로 DOM 요소의 가시성과 위치를 관찰하는 방법을 제공합니다. 간단히 말해서 관찰된 요소가 표시되거나 미리 정의된 위치에 도달할 때 모니터링하고 제공된 콜백 함수를 실행합니다. 이 API를 사용하여 무한 스크롤 기능을 구현하려면 먼저 가져온 데이터의 맨 아래에 관찰된 요소가 될 요소를 만듭니다. 그런 다음 이 요소가 표시되면 fetchNextPage 함수를 호출합니다.
// App.js
import {useRef, useCallback} from 'react'
<div className="app">
...
<div className='loader' ref={observerElem}>
{isFetchingNextPage && hasNextPage ? 'Loading...' : 'No search left'}
</div>
</div>
위에서 Intersection Observers를 사용하여 관찰하려는 div 요소를 만들었습니다. 직접 액세스할 수 있도록 ref 속성을 추가했습니다. 위의 div는 isFetchingNextPage 및 hasNextPage boolean 값에 따라 Loading… 또는 No search left를 표시합니다.
다음으로 App 컴포넌트 상단에 다음 코드 라인을 추가합니다.
// App.js
function App() {
const observerElem = useRef(null)
...
여기에서 ref attribute에 전달된 observerElem 변수를 만들었습니다. 이를 통해 DOM이 로드될 때 위에서 만든 div 요소에 액세스할 수 있습니다. 우리는 코드에서 Intersection Observer로 div element를 전달하기 위해 이 작업을 수행합니다. 다음으로 useInfiniteQuery hook 뒤에 다음 코드 줄을 추가합니다.
// App.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting) {
fetchNextPage()
}
}, [fetchNextPage, hasNextPage])
useEffect(() => {
const element = observerElem.current
const option = { threshold: 0 }
const observer = new IntersectionObserver(handleObserver, option);
observer.observe(element)
return () => observer.unobserve(element)
}, [fetchNextPage, hasNextPage, handleObserver])
위에서, IntersectionObserver에 전달되는 콜백인 handleObserver 함수를 만들었습니다. observer.observe(element)로 지정된 대상 요소가 뷰포트에 진입하면 fetchNextPage를 호출합니다.
이를 통해 앱에서 페이지 맨 아래로 스크롤할 때 새 데이터를 가져옵니다.
지금 당장은 가져올 데이터가 없고 앱에서 페이지 맨 아래로 스크롤하더라도 fetchNextPage가 계속 호출되어 더 많은 데이터를 가져오기 위해 API에 요청을 보냅니다. 이를 방지하려면 데이터가 남아 있지 않을 때 getNextPageParam에 false 값(undefined, 0 null, false)을 반환해야 합니다. 이렇게 하면 반환된 hasNextPage 속성은 데이터가 남아 있지 않을 때 false와 같을 것이며, 이를 사용하여 fetchNextPage 함수가 호출되는 시기를 제어할 것입니다.
이렇게 하려면 useInfiniteQuery hook의 getNextPageParam 옵션을 다음과 같이 수정합니다.
getNextPageParam: (lastPage, allPages) => {
const nextPage = allPages.length + 1
return lastPage.items.length !== 0 ? nextPage : undefined
}
위에서 우리는 마지막 fetch에서 데이터가 반환되었는지 여부에 따라 다음 페이지 매개변수 또는 undefined를 반환합니다.
이제 hasNextPage가 false일 때만 fetchNextPage를 호출하도록 handleObserver 함수를 수정해 보겠습니다.
// App.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting && hasNextPage) {
fetchNextPage()
}
}, [fetchNextPage, hasNextPage])
참고하기
Pagination and infinite scroll with React Query v3
React | Infinite Scroll 구현하기 (react-query, react-intersection-observer)
https://jforj.tistory.com/247
이 분의 코드를 참고해서 작성했는데 좀 더 공부하고 수정해야겠다. 이건 Observer를 사용하지 않고 더 보기 버튼을 눌렀을 때 불러오게 작성한 코드!
import { BiDotsHorizontalRounded } from 'react-icons/bi';
import { useInfiniteQuery } from 'react-query';
import { getCommentsRequest } from '../../modules/board/api';
import { getDateText } from '../../utils';
export default function CommentList({ id }: { id: string }) {
const fetchComments = (page: number) => getCommentsRequest(+id, page);
const { data, fetchNextPage, isFetching, isFetchingNextPage, hasNextPage } =
useInfiniteQuery(
['CommentList', id],
({ pageParam = 1 }) => fetchComments(pageParam),
{
// keepPreviousData: true,
getNextPageParam: (lastPage, allPages) => {
// console.log('getNextPageParam:', lastPage, allPages);
const nextPage = allPages.length + 1;
return lastPage.currentPage < lastPage.totalPage
? nextPage
: undefined;
},
}
);
return (
<div>
<ul>
{data?.pages.map((page) =>
page.commentList.map((comment) => (
<li key={comment.commentId} className="">
...
</li>
))
)}
</ul>
{hasNextPage ? (
<button
className="font-medium m-1 text-sm md:text-base"
onClick={fetchNextPage}
>
댓글 더 보기
</button>
) : (
<p className="font-medium text-gray-4 m-1 text-sm md:text-base">
마지막 댓글입니다
</p>
)}
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</div>
);
}
hasNextPage로 마지막 페이지인지도 체크했는데 현재 페이지랑 마지막 페이지 위치도 표시하고 싶고(1/14) 그리고,,
백엔드님이 댓글 데이터 왕창 넣어주신 거로 테스트



React Example: Load More Infinite Scroll
react-intersection-observer
import { useCallback, useEffect, useRef } from 'react';
import { useInfiniteQuery } from 'react-query';
import { useNavigate, useParams } from 'react-router-dom';
import { getLikesRequest } from '../../modules/board/api';
import { useInView } from 'react-intersection-observer';
export default function LikeList() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { ref, inView } = useInView({
threshold: 0,
});
const fetchLikes = (page: number) => getLikesRequest(+id, page);
const { data, fetchNextPage, isFetching, isFetchingNextPage, hasNextPage } =
useInfiniteQuery(
['Likes', { id }],
({ pageParam = 0 }) => fetchLikes(pageParam),
{
getNextPageParam: (lastPage, allPages) => {
return lastPage.currentPage < lastPage.totalPage
? allPages.length + 1
: undefined;
},
}
);
useEffect(() => {
document.body.style.cssText = `
position: fixed;
overflow-y: scroll;
width: 100%;
height: 100%;`;
return () => {
const scrollY = document.body.style.top;
document.body.style.cssText = '';
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
};
}, []);
useEffect(() => {
if (inView) fetchNextPage();
}, [inView]);
return (
<div
onClick={(e) => {
if (e.target === e.currentTarget) {
navigate(`/board/${id}`);
}
}}
>
<div>
<div>좋아요</div>
<ul>
{data?.pages.map((page) =>
page.likeList.map((like) => (
<li key={like.likeId}>...</li>
))
)}
<div ref={ref} className="m-1">
{isFetching && isFetchingNextPage && hasNextPage
? '불러오는 중...'
: null}
</div>
</ul>
</div>
</div>
);
}

안녕하세요. isFetching과 isFetchingNextPage props가 있던데, 어떤 경우에 isFetching을 사용하고 어떤 경우에 isFetchingNextPage를 사용하나요?