React + GraphQL + Apollo로 무한 스크롤 (Offset-based)

React + Apollo로 무한 스크롤을 구현했을 때의 비망록을 남겨 두자.
무한 스크롤 그 자체보다, GraphQL의 페이지 네이션의 구현으로 생각하는 것이 많은 생각이 든다.
↓실장 이미지


1. 전제 지식·조건



1-1. 구현 환경 및 라이브러리



클라이언트는 React(Javascript), 서버측은 Hasura에서 postgreSQL과 연결하도록 하고 클라이언트측 GraphQL의 처리는 Apollo를 사용."@apollo/client": "^3.3.19
"react": "^17.0.2
Hasura v1.3.3
무한 스크롤 구성 요소는 react-infinite-scroll-component를 사용합니다."react-infinite-scroll-component": "^6.1.0"

1-2.무한 스크롤이란?



무한 스크롤은 Google 검색 결과와 같은 페이지 전환이 아니라 스크롤로 차례로 콘텐츠를 표시시키는 UI입니다. Twitter라든지 인스타 같은 느낌.
모든 콘텐츠를 표시하는 대신 일정량의 콘텐츠를 표시하여 페이지 하단에 도달했을 때 (전에) 다음 콘텐츠를로드해야합니다.

1-3.2 종류의 페이지 네이션에 대해서



일정량의 콘텐츠의 표시(페이지 네이션) 구현은 offset-based와 cursor-based의 2종류로 크게 나눌 수 있다.
offset-based
Limit과 Offset을 지정하여 데이터 취득할 때마다 Offset의 수를 늘려 데이터 취득 범위를 늘려가는 이미지. 구현은 쉽지만 데이터량이 늘어나면 데이터를 보는 양이 늘어나 성능이 나빠지는 것 같다. SQL의 LIMIT와 OFFSET 사용하는 느낌.
cursor-based
데이터 취득 범위의 최초와 마지막을 지정해, 데이터 취득하는 이미지. SQL로 말한다면 Where 절로 데이터를 좁히는 느낌. GraphQL로 하는 경우, 취득하는 데이터에 일의이고 순서가 있는 컬럼(unique, sequential인 컬럼)을 갖게 할 필요가 있는 것 같다.

이 근처의 자세한 해설은 ↓의 기사가 굉장히 참고가 된다.

cursor-based 안에 한층 더 relay-style-pagination이라고 하는 수법이 있어, Apollo라고 InMemoryCache로 relayStylePagination()를 지정하면 좋은 느낌으로 할 수 있는 것 같지만, ID의 흔들림등으로 막혔으므로 여기에서는 스루.

1-4. Apollo의 fetchMore 소개



fetchMore는 useQuery 훅에 포함되는 함수로, useQuery의 쿼리를 인수만큼 바꾸어 실행하고 싶을 때에 사용한다.

스크롤로 다음 콘텐츠를 로드할 때마다 fetchMore를 실행하는 느낌.

1-5.react-infinite-scroll-component 정보



스크롤하려는 요소를 태그 바로 아래의 자식 요소로 배치합니다.

↓사용할 때의 이미지

infiniteScroll.js
<InfiniteScroll
//表示する子要素の数(スクロールする度に増やす必要がある)
dataLength={items.length} 
//ページ下部に到達した時に実行する関数を指定
next={fetchData}
//データ取得がを続けるかどうかを指定。
//trueだとページ下部に到達した時にnextで指定した関数が実行さる。
//falseだと実行されず、endMessageが表示される。
hasMore={true}
//nextの関数結果が返ってくるまで表示するう要素を指定
loader={<h4>Loading...</h4>}
//hasMoreがfalseの時に表示する要素を指定
endMessage={<p>You have seen it all</p>}
>
//スクロールさせたい要素を列挙
{sampleData.map((v) =>
  <sampleDataChild key={v.key}/>)}
</InfiniteScroll>

2. 구현



2-1.GraphQl 쿼리



책 정보를 검색하는 검색어
limit와 offset은 인수로 적절히 지정할 수 있다.

getBooks.js
const GET_BOOKS = gql`
  query getBooks(
    $limit_number: Int
    $offset_number: Int
  ) {
    books(
      limit: $limit_number
      offset: $offset_number
      order_by: { updated_at: desc }
    ) {
      title
      isbn
      author
      image_path
      price
      publish_date
    }
  }
`;

취득 데이터의 이미지. 뭐라고 말할 수 없는 책의 선택.


2-2.state 관리 및 useQuery 설정



로드할 때마다 12건씩 취득하는 이미지

BookList.js
const BookLists = React() => {
  //取得したデータを格納
  const [bookData, setBookData] = useState([]);
  //オフセット数値を格納
  const [offset, setOffset] = useState(0);
  //スクロール後のデータフェッチを続けるかの判定を格納
  const [hasMore, setHasMore] = useState(true);
  //fetchMore取得
  const { fetchMore } = useQuery(GET_BOOKS,
    onCompleted: (data) => {
      if (!data.books.length) {
        const { books } = data.books;
        setBookData(books);
      }
    },
  };
  //infiniteScrollのnextで呼ぶ関数を定義
  const getBooksData = async () => {
  //呼ぶ度にoffsetに12加算
    setOffset((prevOffset) => prevOffset + 12);
    const { data } = await fetchMore({
      variables: { limit_number: 12, offset_number: offset },
    });
    // 取得したdata件数が0の時はhasMoreをfalseにして、スクロールを終了
    data.books.length === 0 && setHasMore(!hasMore);
    setBookData((prev) => [...prev, ...data.books]);
  };
  //初回画面表示に使用
  useEffect(() => {
    getBooksData();
  }, []);

  return (
    <div>
      {bookData && bookData.length > 0 && (
        <BookContents
          data={bookData}
          nextFunc={() => getBooksData()}
          hasMore={hasMore}
        />
      )}
    </div>

2-3.InfiniteScroll의 컴포넌트 설정



2-2에서 설정한 내용을 InfiniteScroll로 적절히 호출한다.

InfiniteBooks.js
const InfiniteBooks = (props) => {
  return (
        <InfiniteScroll
  //InfiniteScroll直下の要素配置を変えたい時に指定
          style={{ display: 'flex', flexWrap: 'wrap' }}
          dataLength={booksData.length}
          next={() => props.nextFunc()}
          hasMore={props.hasMore}
  //material-uiのローダーを入れてる
          loader={<CircularProgress />}
          endMessage={
            <h1 style={{ textAlign: 'center' }}>
              <b>You have seen it all</b>
            </h1>
          }
        >
          {booksData &&
            booksData.map((v) => 
              <BooksChild key={v.title} props={v} />)}
        </InfiniteScroll>
  );
};

3. 참고



↓ 참고로 한 code sandbox
릭 앤 모티라는 애니메이션 캐릭터를 늘 ​​스크롤 할 수 있습니다.

좋은 웹페이지 즐겨찾기