이전에 제작한 커뮤니티 프로젝트에서도 게시글에 대해 무한스코롤을 주어, 게시글들이 스크롤되는 페이지마다 불러오게 하였다.
그때는 SWR의 useSWR Infinite
와 Intersection Observer
을 사용하였다.
✅ 이번에는 React Query의 useInfiniteQuery
를 사용하고, Intersection Observer
은 그대로 활용해보자.
✅ 복습
Apollo server는 해당 데이터에 대한 요청에 응답할 수 있도록 스키마의 모든 필드에 대한 데이터를 채우는 방법을 위해 Resolver을 사용한다고 했다.
Resolver는 스키마의 단일 필드에 대한 데이터를 채우는 역할을 담당하고, 4개의 위치 인수를 선택적으로 받아들일 수 있다.
✅ GraphQL 인수 - cursor 생성
// src/schema/product.ts
const productSchema = gql`
...
type Query {
products(cursor: ID): [Product!]
product(id: ID!): Product!
}
`;
✅ cursor 빈값으로 설정
만약 cursor가 빈값이라면?(첫번째 페이지라면...)
1️⃣ product.id
와 동일한 index
는 찾지 못하므로 findIndex
는 -1
을 반환한다. 따라서, -1 + 1 = 0
으로 fromIndex
에는 0
이 저장.
2️⃣ slice
로 products
배열의 0부터 15
인덱스까지의 데이터를 추출.
즉, 1부터 15
까지의 상품을 보여준다.
만약 cursor가 15라면?(두번째 페이지라면...)
1️⃣ product id가 15인 데이터를 반환한다. 따라서, 14 + 1 = 15
으로 fromIndex
에는 15
이 저장.
2️⃣ slice
로 products
배열의 14부터 29
인덱스까지의 데이터를 추출.
즉, 15부터 30
까지의 상품을 보여준다.
// src/resolvers/product.ts
const productResolver: Resolver = {
Query: {
products: async (parent, { cursor = "" }, { db }) => {
const fromIndex =
db.products.findIndex((product) => product.id === cursor) + 1;
return db.products.slice(fromIndex, fromIndex + 15) || [];
},
...
},
};
💡 참고하자!
👉 Cursor-based pagination
✅ Apollographql로 데이터 확인하기!
cursor 값이 null일 때
cursor 값이 15일 때
앞서 본 것 같이 cursor값에 따라 상품이 제한적으로 출력되는 것을 확인할 수 있다.
하지만, 앞의 방법은 cursor의 값을 수동적으로 설정한 것이었다.
✅ 이제는 Client에서 스크롤하여 화면이 끝에 도달하였을 때 마지막 상품의 ID를 cursor값으로 주면 자동적을 cursor값이 변경될 것이다.
이와 같이 기존 데이터에 더해 데이터를 더 불러와서 렌더링할 때나 무한스크롤과 같은 패턴으로 리스트형 데이터 요청을 하기 위해 React Query에서는 useQuery의 유용한 버전인 useInfiniteQuery
를 지원한다.
1️⃣ getNextPageParam
: 함수 실행 시 return값으로 페이지마지막의 상품 id값이 전달
getNextPageParam: (lastPage, allPages) => unknown | undefined
undefined
: 사용할 수 있는 다음 페이지가 없음을 나타내기 위해 돌아 갑니다.2️⃣ pageParam = ""
: return 받은 상품 id값이 cursor에 전달.
// src/pages/products.indext.ts
const { data, isSuccess, isFetchingNextPage, fetchNextPage, hasNextPage } =
useInfiniteQuery<Products>(
QueryKeys.PRODUCTS,
({ pageParam = "" }) => // 2️⃣ 번
graphqlFetcher(GET_PRODUCTS, { cursor: pageParam }),
{
getNextPageParam: (lastPage) => {
return lastPage.products[lastPage.products.length - 1]?.id; // 1️⃣ 번
},
}
);
✅ 이중 객체 주의하자!
위와 같은 방법으로 하면, data는 다음과 같은 형태로 들어온다.
data : {
pages: [
{products: {...}} // 1 ~ 15 상품
{products: {...}} // 16 ~ 30 상품
{products: {...}} // 30 ~ 45 상품
...
],
pageParams: [undefined, 14, 29 ...]
}
따라서, 상품리스트를 출력할 때, 이중 map
을 통해 제한된 개수로 들어있는 페이지들
안의 상품들의 상품
을 출력해야한다.
😭 진짜 어렵다 ㅜㅜㅜ
💡 참고하자!
👉 useInfiniteQuery
👉 React Query - useInfiniteQuery 사용법
이제 해주어야할 것은 실제로 페이지의 끝에 도달했는지를 확인하는 것이다. 위에서는 페이지의 끝에 오면 마지막 상품의 ID값을 넘겨주는 것이었다.
✅ 그렇다면, 스크롤시 페이지의 끝에 도달하는지를 관찰하는 관찰자가 필요할 것이다.
✅ 주의할 점!
만약 위아래로 또는 아래로 계속해서 빠르게 스크롤하면 요청하는 중에도 요청이 이루어지며 target에 대한 값이 true, false로 혼돈이 올 것이다.
그래서, 이때 isFetchNextPage
를 이용하자.
isFetchingNextPage
은 boolean 값으로 true, false에 따라 앞서 생성한 fetchNextPage을 실행여부를 판단할 수 있다.따라서, 다음과 같이 true값이면, 데이터를 불러오지 말고 return하게끔 useEffect를 생성한다.
useEffect(() => {
if (isFetchingNextPage) return;
fetchNextPage();
}, [intersecting]);
✅ DOM요소에 접근했을 때의 ref을 useIntersection Hook에 전달하자.
const ProductListPage = () => {
const fetchMoreRef = useRef<HTMLDivElement>(null);
const intersecting = useIntersection(fetchMoreRef);
...
return (
<div>
<h2>상품목록</h2>
<ProductList list={data?.pages || []} />
<div ref={fetchMoreRef} /> // 페이지의 끝에 도달했을때
</div>
);
}