검색
검색
공개 노트 검색
회원가입로그인

next ssr pagination, infinite scroll 구현하기

next 에서 ssr 에서 무한 스크롤일 때 페이지네이션을 어떻게 구현할 지 궁금했었는데 다음 블로그에서 힌트를 얻어 구현했다.

https://ellismin.com/2020/05/next-infinite-scroll/

방법은 페이지가 바닥에 닿거나 마지막 요소가 나왔을 때 router.push 를 통해 페이지를 바꿔주는 것이다. 이 경우 백엔드에서는 페이지네이션을 할 때 limit 을 주지 않고 skip 값만 변경해 주어 page * page number 개의 아이템이 나오게 조정을 해주면 된다.

이 부분은 조금 더 발전을 시킬 수 있는 요소가 있다. 페이지 넘버에 따라 많은 아이템의 갯수가 나올 수 있으니 데이터를 cache 해 줄 수 있는 라이브러리(react query, apollo client)나 리덕스를 활용해 기존 데이터에 새로운 페이지 데이터를 붙이고 getServerSideProps 에 넣어주는 것이다. 이 부분은 아직 어떻게 할지 결정을 하지 않아서 백엔드의 skip 값만 조정해 놓았다. graphql을 붙이는 건 지금하면 작업량이 많을거 같아서 (백엔드, 프론트 둘 다 조정해야 하는 것 같다.) react query 쪽으로 보고 있다.

먼저 페이지가 로드되는 페이지의 getSiverSiderProps 를 다음과 같이 조정해 준다. getServerSideProps 는 페이지 레벨의 라우팅에서만 사용할 수 있다. (컴포넌트 레벨에서는 사용이 안된다.)

export const getServerSideProps = async ({ query }) => {
  const page = query.page || 1;
  let pageData = null;
  try {
    const res = await API.get(
      `${process.env.NEXT_PUBLIC_SERVER}/api/pages?page=${page}`
    );
    if (res.status !== 200) {
      throw new Error("Failed to fetch");
    }
    pageData = await res.data.data;
  } catch (err) {
    pageData = { error: { message: err.message } };
  }

  return { props: { pageData, propPage: page } };
};

페이지 데이터와 페이지 넘버를 위 쪽 컴포넌트로 넘겨주었다. 위 쪽의 컴포넌트에서는 다음과 같이 받는다.

const PublicPage = ({ pageData, propPage }) => {

받아온 데이터를 컴포넌트 내에 세팅을 해준다.

  useEffect(() => {
    if (pageData) {
      if (pageData.error) {
        // Handle error for your logic
      } else {
        // Set users from userData
        setData(pageData.pages);
        if (parseInt(propPage, 10) < parseInt(pageData.lastPage, 10)) {
          setHasMore(true);
        } else {
          setHasMore(false);
        }
      }
    }
  }, [pageData]);

현재 페이지와 마지막 페이지를 비교하여 현재 페이지가 마지막 페이지와 같거나 크면 더 이상 로딩을 하지 않도록 처리를 해주었다. 마지막 페이지 넘버는 백엔드에서 반환한다.

  // handle pagination
  const handlePagination = (page) => {
    const path = router.pathname;
    const query = router.query;
    query.page = parseInt(page, 10) + 1;
    router.push(
      {
        pathname: path,
        query: query,
      },
      undefined,
      { scroll: false }
    );
  };

마지막 요소가 나왔을 때 페이지네이션을 처리하는 함수이다. 이 부분이 핵심이다. useRouter 에서 현재 path 를 받아 page query 부분을 바꿔 라우트를 이동시킨다.

router.push 를 사용할 때 두 번째 인자는 as 로 url 이 어떻게 보일지에 대한 부분인데 사용하지 않으므로 undefined 를 넣어 주었다. 세번째 옵션 인자에는 scroll : false 를 넣어주어 스크롤이 변하지 않게 했다. 이렇게 하면 페이지가 바뀌어도 스크롤이 유지되어 기존 보던 데이터의 위치를 유지할 수 있다.

  const lastElementRef = useCallback(
    (node) => {
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          handlePagination(propPage);
        }
      });
      if (node) observer.current.observe(node);
    },
    [hasMore, handlePagination]
  );

데이터에서 마지막 요소가 나왔을 때 페이지네이션을 발동시키는 부분이다. 스크롤의 위치를 기반으로 하지 않고 IntersectionObserver 를 사용했다.

렌더 부분에서 마지막 요소가 나올 때 ref 를 연동시키는 부분이다.

data.map((page, index) => (
<div
  key={page._id}
  className="p-10"
  ref={data.length === index + 1 ? lastElementRef : null}
>

마지막으로 서버에서 데이터를 반환하는 부분이다. mongo db를 사용했다.

const page = req.query.page ? req.query.page : 1;
const perPage = 20;
const totalPageCount = await Page.find().countDocuments();

const pages = await Page.find()
  .limit(perPage * page)
  .sort({ _id: -1 });

일반적인 페이지네이션에서 skip 부분이 사라졌다. countDocuments의 성능이 어느 정도일지 살짝 걱정된다.

여기서 프론트에서 기존 데이터 캐싱이 된다면 베스트일것 같다. 그 때는 skip 을 다시 적용한다.

조회수 : 1535
heart
공유하기
카카오로 공유하기
페이스북 공유하기
트위터로 공유하기
url 복사하기
T
페이지 기반 대답
AI Chat