개발자
류준열
무한스크롤 useQuery로 만들고 후회함
결론부터 말하면 useQuery로 무한스크롤 만들고 후회했다.
무한스크롤만 있는 페이지라면 상관없다.
무한스크롤, 필터링이 함께있는 페이지에서 무한스크롤 로딩과 필터링 로딩을 분리하기가 쉽지 않았기 때문이다.
이래서 @tanstack/react-query에서 useInfiniteQuery를 따로 만들어 놓은건가 싶다.
useQuery로 무한스크롤 만들기
라이브러리는 @tanstack/react-query
와 react-intersection-observer
를 사용했다.
먼저 react-intersection-observer
와 lodash/throttle
을 이용하여 커스텀 훅 useThrottleScroll.ts를 만들었다.
// useThrottleScroll.ts
/* eslint-disable react-hooks/exhaustive-deps */
import throttle from "lodash/throttle";
import { useCallback, useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
export const useThrottleScroll = (ms: number = 1000) => {
const [ref, inView] = useInView();
const [page, setPage] = useState(1);
const [isLastData, setIsLastData] = useState(false);
const throttledSetPage = useCallback(
throttle(
() => {
const isWaitingToken =
localStorage.getItem("waitingToken") === "true" ? true : false;
if (isWaitingToken) {
return;
}
setPage((prev) => prev + 1);
},
ms,
{ trailing: false },
),
[],
);
useEffect(() => {
if (inView) {
if (isLastData) return;
else {
throttledSetPage();
window.scrollBy({ top: -100, behavior: "smooth" });
}
}
}, [inView]);
return { ref, state: { page, setPage, isLastData, setIsLastData } };
};
그리고 무한스크롤을 구현한 페이지는 다음과 같다.
export const Home = () => {
const { data, isLoading, isError } = useNewsQuery({
headline,
countries: selectedCountries,
beginDate,
page,
});
const { ref,state: { page }} = useThrottleScroll(1000);
return (
<>
<Filters
...
/>
<Cards news={news} />
{!isLoading && !isError && news.length > 0 && (
<span ref={ref}>{page}</span>
)}
</>
}
이렇게 만들면
- 1000ms간 스크롤 이벤트에 쓰로틀이 생기고,
- 가장 아래에 있는 ref를 넣어둔 span태그가 view에 나타날때마다 setPage(page=>page+1)이 실행되면서
- useQuery가 비동기로 실행된다. 동시에 스크롤이 -100만큼 이동한다.
- 로딩중일때는 ref가 들어가있는 span태그가 보이지 않는다.
- 로딩이 끝나면 아래에 데이터가 추가된다.
필터와 무한스크롤을 같은 useQuery에 연결했을때
문제는 필터링에 쓰는 fetch도 같은 useQuery를 사용한 것이다. suspense를 연동하여 fallback 로딩처리를 하면 다음과 같이 작동한다.
필터로딩과 무한스크롤 로딩을 분리하려 하다가, useInfiniteQuery를 사용해서 두 쿼리를 분리해보기로 했다. 다음 포스트는 분리한 과정이다.