패스트캠퍼스 강의를 수강하면서 진행했던 프로젝트에서 무한 스크롤을 구현했던 것을 정리해보려고 한다.
https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery#useinfinitequery
/* useInfiniteQuery 기본 구조 */
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
프로젝트에서 사용한 부분에 대해서만 간략히 알아보겠습니다.
returns
data : 서버에 요청해서 받아온 데이터
data.pages : 가져온 페이지들의 배열
data.pageParams : 페이지를 가져오기 위한 페이지 매개변수. 배열 형태
isFetching : 데이터를 가져오고 있는지 여부를 확인합니다(boolean).
fetchNextPage, fetchPreviousPage : 다음페이지, 이전페이지를 불러옵니다.
hasNextPage, hasPreviousPage : 다음 및 이전 페이지가 있는지 여부를 나타내는 boolean 값
isFetchingNextPage, isFetchingPreviousPage : 다음 및 이전 페이지의 데이터를 가져오는 동안 로딩 상태를 나타내는 boolean 값
option
queryKey : 쿼리를 구별하여 캐시를 관리하기위한 이름, key입니다.
queryFn : 쿼리가 데이터를 요청하는 데 사용할 함수, API입니다.
getNextPageParam, getPreviousPageParam : 다음 및 이전 페이지에 대한 매개 변수를 생성하는 함수
// API 호출 코드
const fetchStores = async ({ pageParam = 1 }) => {
const { data } = await axios("/api/stores?page=" + pageParam, {
params: {
limit: 10,
page: pageParam,
...searchParams,
},
});
return data;
};
// 무한 스크롤 구현 코드
const {
data: stores,
isFetching,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
isError,
isLoading,
} = useInfiniteQuery(["stores", searchParams], fetchStores, {
getNextPageParam: (lastPage: any) =>
lastPage.data?.length > 0 ? lastPage.page + 1 : undefined,
});
// 다음 API 호출 코드
const fetchNext = useCallback(async () => {
const res = await fetchNextPage();
if (res.isError) {
console.log(res.error);
}
}, [fetchNextPage]);
useEffect(() => {
let timerId: NodeJS.Timeout | undefined;
if (isPageEnd && hasNextPage) {
timerId = setTimeout(() => {
fetchNext();
}, 500);
}
return () => clearTimeout(timerId);
}, [fetchNext, isPageEnd, hasNextPage]);
무한 스크롤 구현 코드를 살펴보면
// 무한 스크롤 구현 코드
const {
data: stores,
isFetching,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
isError,
isLoading,
} = useInfiniteQuery(["stores", searchParams], fetchStores, {
getNextPageParam: (lastPage: any) =>
lastPage.data?.length > 0 ? lastPage.page + 1 : undefined,
});
stores라는 key값으로 searchParams를 매개 변수로 담아 fetchStores함수를 호출하고 데이터를 받아옵니다.
// 다음 API 호출 코드
const fetchNext = useCallback(async () => {
const res = await fetchNextPage();
if (res.isError) {
console.log(res.error);
}
}, [fetchNextPage]);
useEffect(() => {
let timerId: NodeJS.Timeout | undefined;
if (isPageEnd && hasNextPage) {
timerId = setTimeout(() => {
fetchNext();
}, 500);
}
return () => clearTimeout(timerId);
}, [fetchNext, isPageEnd, hasNextPage]);
일정 부분에 도달했을 때 setTimeout을 사용해 다음 페이지의 데이터를 받아오는 fetchNextPage함수를 useCallback함수를 통해 실행시킵니다.
export default function StoreListPage() {
const ref = useRef<HTMLDivElement | null>(null);
const pageRef = useIntersectionObserver(ref, {});
const isPageEnd = !!pageRef?.isIntersecting;
const searchValue = useRecoilValue(searchState);
const searchParams = {
q: searchValue?.q,
district: searchValue?.district,
};
const fetchStores = async ({ pageParam = 1 }) => {
const { data } = await axios("/api/stores?page=" + pageParam, {
params: {
limit: 10,
page: pageParam,
...searchParams,
},
});
return data;
};
const {
data: stores,
isFetching,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
isError,
isLoading,
} = useInfiniteQuery(["stores", searchParams], fetchStores, {
getNextPageParam: (lastPage: any) =>
lastPage.data?.length > 0 ? lastPage.page + 1 : undefined,
});
const fetchNext = useCallback(async () => {
const res = await fetchNextPage();
if (res.isError) {
console.log(res.error);
}
}, [fetchNextPage]);
useEffect(() => {
let timerId: NodeJS.Timeout | undefined;
if (isPageEnd && hasNextPage) {
timerId = setTimeout(() => {
fetchNext();
}, 500);
}
return () => clearTimeout(timerId);
}, [fetchNext, isPageEnd, hasNextPage]);
if (isError) {
return (
<div className="w-full h-screen mx-auto pt-[10%] text-red-500 text-center font-semibold">
다시 시도해주세요
</div>
);
}
return (
<div className="px-4 md:max-w-4xl mx-auto py-8">
{/* search filter */}
<SearchFilter />
<ul
role="list"
className="divide-y divide-gray-100"
>
{isLoading ? (
<Loading />
) : (
stores?.pages?.map((page, index) => (
<React.Fragment key={index}>
{page.data.map((store: StoreType, i: any) => (
<StoreList
store={store}
i={i}
key={i}
/>
))}
</React.Fragment>
))
)}
</ul>
{(isFetching || hasNextPage || isFetchingNextPage) && <Loader />}
<div
className="w-full touch-none h-10 mb-10"
ref={ref}
/>
</div>
);
}