[GraphQL] Pagination, Infinite Scroll

0 0 0

GraphQL에서 페이지네이션과 무한 스크롤을 구현하는 방법을 정리합니다.


Pagination

**페이지네이션**은 보여 줄 데이터가 많을 때 한 번에 불러오지 않고, 페이지 번호를 눌러 이동하는 방식입니다.

GraphQL에서는 refetch 함수로 구현할 수 있습니다.

export default function PaginationPage() {
  const [startPage, setStartPage] = useState(1); // 초기값 설정 (index번호는 0부터 시작이기때문에)

  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
   >(FETCH_BOARDS);

   const onClickPage = (event: MouseEvent<HTMLSpanElement>) => {
    // refetch(page : 보여줄 페이지번호)
	void refetch({ page: Number(event.currentTarget.id) });
   };

   return (
    <div>
     {data?.fetchBoards?.map((el) => ( 
     // data?. => 데이터가 있으면 / fetchBoards?. => fetchBoards가 있으면 map메소드 실행
      <div key={el._id}>
       <span >{el.title}</span>
       <span >{el.contents}</span>
      </div>
     ))}
     <PageNationWrap>
      {new Array(10).fill(0).map(
      // 초기에 반복할 갯수가 없기에 new Array로 반복할 횟수를 설정
      // new Array(10)만하면 빈값이기에 fill(0)으로 모든 배열에 값 넣어주기(빈값이면 반복문이 돌지 않아요)
       (_, index) => (
       // _는 사용하지 않는 매개변수일경우 작성
         <PageButton
          key={index + startPage}
          id={String(index + startPage)}
          onClick={onClickPage}
         >
          {index + startPage}
         </PageButton>
        )
      )}
	  </PageNationWrap>
    </div>
   );
 }

여기까지가 페이지네이션의 기본입니다. 여기에 이전·다음 버튼을 만들고, 데이터가 없는 페이지 번호는 보이지 않도록 설정을 추가했습니다.

const [isPage, setIsPage] = useState(1); // 현재 보고 있는 페이지

 const { data: dataBoardsCount } = useQuery<
  Pick<IQuery, "fetchBoardsCount">,
  IQueryFetchBoardsCountArgs
 >(FETCH_BOARDS_COUNT);
// boards의 총 갯수
 
 const lastPage =
  dataBoardsCount != null
   ? Math.ceil(dataBoardsCount?.fetchBoardsCount / 10)
   : 1;
// 마지막 페이지 구하기
// 만약에 796개의 리스트가 있다면 반올림하여 80개의 페이지가 마지막 페이지

 const onClickPrevPage = () => { 			// 이전페이지
  if (startPage === 1) return; 				// 첫번째 페이지면 작동금지 (early return 패턴)
  setStartPage(startPage - 10); 			// 10씩 줄이기 (페이지네이션이 10개를 노출 하기에)
  void refetch({ page: startPage - 10 });	
  setIsPage(startPage - 10); 				
 };

 const onClickNextPage = () => {			// 이전페이지
  if (startPage + 10 <= lastPage) {			// 라스트 페이지를 넘기기전까지만 작동 => (71 + 10 <= 80) false
   setStartPage(startPage + 10);
   void refetch({ page: startPage + 10 });
   setIsPage(startPage + 10);
  }
 };

위 코드를 추가해 이전·다음 버튼과 현재 페이지 표시(active)까지 구현했습니다.

최종 코드

export default function StaticRoutedPage() {
 const [startPage, setStartPage] = useState(1);
 const [isPage, setIsPage] = useState(1);

 const { data, refetch } = useQuery<
  Pick<IQuery, "fetchBoards">,
  IQueryFetchBoardsArgs
 >(FETCH_BOARDS);

 const { data: dataBoardsCount } = useQuery<
  Pick<IQuery, "fetchBoardsCount">,
  IQueryFetchBoardsCountArgs
 >(FETCH_BOARDS_COUNT);

 const lastPage =
  dataBoardsCount != null
   ? Math.ceil(dataBoardsCount?.fetchBoardsCount / 10)
   : 1;

 const onClickPage = (event: MouseEvent<HTMLSpanElement>) => {
  void refetch({ page: Number(event.currentTarget.id) });
  setIsPage(Number(event.currentTarget.id));
 };

 const onClickPrevPage = () => {
  if (startPage === 1) return;
  setStartPage(startPage - 10);
  void refetch({ page: startPage - 10 });
  setIsPage(startPage - 10);
 };

 const onClickNextPage = () => {
  if (startPage + 10 <= lastPage) {
   setStartPage(startPage + 10);
   void refetch({ page: startPage + 10 });
   setIsPage(startPage + 10);
  }
 };

 return (
  <div>
   {data?.fetchBoards?.map((el) => (
    <div key={el._id}>
     <span style={{ margin: "10px" }}>{el.title}</span>
     <span style={{ margin: "10px" }}>{el.contents}</span>
    </div>
   ))}
   <PageNationWrap>
    <PageButton disabled={startPage === 1} onClick={onClickPrevPage}>
     <LeftOutlined />
    </PageButton>
    {new Array(10)
     .fill(0)
     .filter((_, filterIndex) => filterIndex + startPage <= lastPage)
     .map((_, mapIndex) => (
      <PageButton
       key={mapIndex + startPage}
       id={String(mapIndex + startPage)}
       onClick={onClickPage}
       isPageActive={isPage === mapIndex + startPage ? true : false}
      >
       {mapIndex + startPage}
      </PageButton>
     ))}
    <PageButton disabled={startPage + 10 > lastPage} onClick={onClickNextPage}>
     <RightOutlined />
    </PageButton>
   </PageNationWrap>
  </div>
 );
}


Infinite Scroll

유튜브나 페이스북처럼 페이지를 아래로 스크롤하다 끝에 도달하면

새 데이터가 계속 추가되는 방식을 **무한 스크롤**이라고 합니다.

React에서 많이 쓰는 무한 스크롤 라이브러리 중 react-infinite-scroller를 사용했습니다. react infinite scroller

npm 사용자
npm install react-infinite-scroller --save

yarn 사용자
yarn add react-infinite-scroller

TypeScript를 사용한다면 타입도 함께 설치합니다.

npm 사용자
npm install --save-dev @types/react-infinite-scroller

yarn 사용자
yarn add --dev @types/react-infinite-scroller

무한 스크롤은 페이지네이션보다 구현이 간단하고 사용하기도 편하다고 느꼈습니다. 다만 원하는 페이지로 바로 이동할 수 없다는 단점이 있습니다.

무한 스크롤을 적용할 영역을 InfiniteScroll 컴포넌트로 감싸서 사용합니다.

코드로 이어서 설명하면 다음과 같습니다.

import InfiniteScroll from "react-infinite-scroller"; // 추가 해주세요

export default function InfiniteScrollPage() {
 const { data, fetchMore } = useQuery<
  Pick<IQuery, "fetchBoards">,
  IQueryFetchBoardsArgs
 >(FETCH_BOARDS);
 // fetchMore를 추가 해주세요

 const onFectchMore = () => {
  if (data === undefined) return; // data가 없을경우 종료 (early return 패턴)

  void fetchMore({
   variables: {
    page: Math.ceil(data.fetchBoards.length / 10) + 1, 
	// fetchBoards의 데이터는 계속 쌓임 그러면 10개씩 계속 증가
   },
   updateQuery(prev, { fetchMoreResult }) { // prev : 이전데이터 , fetchMoreResult : 쿼리 호출 후 결과
    if (fetchMoreResult.fetchBoards === undefined) {
     return {
      fetchBoards: [...prev.fetchBoards],
     };
    } // 쿼리 호출후 결과가 undefined일 경우 이전값까지만 노출 (early return 패턴)
    return {
     fetchBoards: [...prev.fetchBoards, ...fetchMoreResult.fetchBoards],
    }; // 이전 데이터와 호출후 데이터 합치기
   },
  });
 };

 return (
  <div>
   <InfiniteScroll
    pageStart={0} 
    loadMore={onFectchMore} // 화면이 최하단으로 갔을경우 실행할 함수
    hasMore={true}
   >
    {data?.fetchBoards?.map((el) => (
     <div key={el._id}>
      <span style={{ margin: "10px" }}>{el.title}</span>
      <span style={{ margin: "10px" }}>{el.contents}</span>
     </div>
    ))}
   </InfiniteScroll>
  </div>
 );
}



댓글 0