PJH's Shopping Mall - 무한스크롤

박정호·2022년 12월 28일
0

Shopping Project

목록 보기
8/11
post-thumbnail

🚀 Start

이전에 제작한 커뮤니티 프로젝트에서도 게시글에 대해 무한스코롤을 주어, 게시글들이 스크롤되는 페이지마다 불러오게 하였다.

그때는 SWR의 useSWR InfiniteIntersection Observer을 사용하였다.

이번에는 React Query의 useInfiniteQuery를 사용하고, Intersection Observer은 그대로 활용해보자.



⚙️ Server Pagination

복습

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️⃣ sliceproducts 배열의 0부터 15 인덱스까지의 데이터를 추출.
      즉, 1부터 15까지의 상품을 보여준다.

  • 만약 cursor가 15라면?(두번째 페이지라면...)

    • 1️⃣ product id가 15인 데이터를 반환한다. 따라서, 14 + 1 = 15으로 fromIndex에는 15이 저장.

    • 2️⃣ sliceproducts 배열의 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일 때



⚙️ useInifiniteQuery

앞서 본 것 같이 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 사용법



⚙️ Intersection observer

이제 해주어야할 것은 실제로 페이지의 끝에 도달했는지를 확인하는 것이다. 위에서는 페이지의 끝에 오면 마지막 상품의 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>
  );
  }

참고하자!
👉 PJH's Community Site - Infinite Scroll



profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글