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를 사용하나요?