useInfiniteQuery를 사용하여 페이지네이션 구현 (Cursor Based Pagination)

Singsoong·2024년 4월 19일
2

react

목록 보기
6/7
post-thumbnail

📌 페이지네이션

📕 페이지네이션의 개념

페이지네이션이란, 많은 데이터를 부분적으로 불러오는 기술을 의미한다.

우리가 일반적으로 데이터를 불러올 때, 만약에 모든 데이터를 불러오게 되면 매우 비효율적일 것이다. 데이터가 1억 개가 있다고 한다면, 사용자의 입장에서는 이 데이터를 모두 보지도 않는데 1억 개의 데이터가 모두 불러와질 때까지 기다려야 한다. 또한, 모든 데이터를 불러오게 되면 데이터를 보내는 데 드는 시간과 비용이 더 올라가게 되며, 이는 둘째치고 클라이언트 단에서 모든 데이터를 저장하기에 메모리가 부족할 것이다.

때문에 UX적 측면이나, 비용 효율화의 측면이나, 기술적인 한계의 측면에서 모든 데이터를 불러오기보다 일부의 데이터만을 불러오는 게 훨씬 효율적이다.

이것이 바로 Pagination의 개념이다.

📕 페이지네이션의 종류

  1. Page Based Pagination
    전통적인 페이지네이션이다. 현대의 UI에서 페이지 기반 페이지네이션은 많이 사용하지 않는다. 페이지를 기반으로 데이터를 잘라서 요청하는 방식이다.

request로 페이지 번호를 보내면, 총 토탈 페이지와 현재 페이지 데이터를 리턴해주는 방식이다.

Page Based Pagination의 문제점으로는 데이터 신규 삽입 시 중복된 데이터가 발생한다.

위와 같이, 중복된 데이터 (id=3)가 발생하고 있다. 이처럼 페이지 기반 페이지네이션의 경우 신규 데이터가 삽입될 시 중복된 데이터가 발생할 수 있다.


이번에는 위 상태에서 id=3인 아이탬이 삭제된다고 가정하면,


id = 4인 아이탬이 누락된다. 그렇기 때문에 그에 대한 대안으로 나오게 된 것이 커서 기반 페이지네이션이다.

  1. Cursor Based pagination
  • 가장 최근에 가져온 데이터를 기준으로 다음 데이터를 가져오는 페이지네이션
  • 요청을 보낼 때, 가장 마지막 데이터의 id와 함께 몇 개의 데이터를 가져올 것인지(limit) 개수를 보낸다
  • 스크롤 형태의 리스트에서 자주 사용하며, 주로 어플리케이션에서 많이 사용하게 되는 형태이다.
  • 무한 스크롤 방식이 해당 페이지네이션 기법이다.
  • 페이지 기반 페이지네이션에 비해서 비교적 구현이 복잡하지만, 최근 데이터의 id값을 기준으로 쿼리가 작성되기 때문에 데이터가 중복되거나 누락될 확률이 낮아진다.


첫 요청에서는 몇개를 가져올 것인지 개수를 명시 한다. 위 경우는 limit=3으로 쿼리를 한 상태이다.

첫 요청 이후 다음 요청부터는 몇개의 아이탬을 가져올 것인지 limit 개수와 함께 내가 가지고 있는 데이터 중에 마지막 아이탬의 id 값을 서버에 요청한다. 위와 같은 경우에는 id = 3, limit = 3 으로 서버에 요청한 상태이다.

커서 기반 페이지네이션에서 데이터가 새로 삽입된다면?

위와 같이 신규 데이터가 들어온다고 하더라도 데이터의 중복 문제 없이 페이지네이션은 진행되지만, 신규 데이터가 누락 되는것 자체는 피할 수 없다. 하지만 논리적 설계 측면에서도 사용자가 관심있는 데이터는 id > 3인 데이터지 id = 2.5인 데이터는 현재의 관심사가 아닐 것 이다. 이는 새로고침 등을 통해서 신규 데이터를 확보하는게 자연스럽다.

커서 기반 페이지네이션에서 데이터가 삭제된다면?

이때도 마찬가지로 id를 기반으로 하는 커서 기반 페이지네이션에서는 데이터 누락 문제 없이 페이지네이션이 동작한다.

📌 구현

tanstack queryuseInfiniteQuery를 사용하여 무한스크롤을 구현하려고 한다. 전통적인 페이지네이션 기법인 Page Based Pagination이곳에서 구현했고, 이번에는 Cursor Based Pagination 을 사용하여 페이지네이션을 구현하려 한다.

📕 스크롤 바닥 감지하기

스크롤 바닥을 감지해서 다음 페이지 데이터를 가져올 지 판단해야 한다.
이번에도 마찬가지로 react-intersection-observer 라이브러리를 사용할 것이다.
관찰하는 객체(observer) 하나를 ref로 설정한 후 해당하는 객체가 화면에 보이면 특정 코드를 실행시킬 수 있다.

https://github.com/thebuilder/react-intersection-observer#readme

  • 설치
npm install react-intersection-observer
  • Import
import { useInView } from 'react-intersection-observer';
const LiveAdminPage = () => {
  const [ref, inView] = useInView();

  useEffect(() => {
    if (inView){
      console.log("다음 페이지 호출")
    }
  }, [inView]);

  return (
    <div className="live_admin_page_container">
      <div className="live_admin_item_container empty">
        <div ref={ref} className="live_loading_indicator" />
      </div>
    </div>
  );
};

export default LiveAdminPage;

감지할 영역 엘리먼트를 ref로 주고 inView가 true값이 됐을 때 다음 페이지를 호출한다.

📕 API: 요구사항

페이지네이션을 위해 준비된 API는 아래와 같은 리퀘스트를 요구한다.

[GET] /list

  • start_id(optional): 제일 마지막 id 값, 예를 들어 [1,2,3]이 있으면 start_id로 2를 보내게 되면 3부터 명시한 limit 개수까지 리턴해준다.
  • limit: 한 페이지에 몇개의 데이터를 리턴해줄것인지 명시

📕 구현 (Fetching) - useInfiniteQuery

// 한 페이지에 몇개를 가져올 것인지 LIMIT 명시
const LIMIT = 6;

export const useGetLiveList = ({ community_id, user_id }) => {
  // 핵심: 첫 fetching시 start_id는 없으므로 pageParam을 undefined로 명시한다.
  const getLiveList = async ({ pageParam = undefined }) => {
    try {
      const response = await client.get(`${BASE_URL}/list`, {
        params: {
          community_id,
          user_id,
          start_id: pageParam,
          limit: LIMIT,
        },
      });
      return response.data;
    } catch (error) {
      console.error(error);
    }
  };

  const { data, fetchNextPage, isLoading, hasNextPage } = useInfiniteQuery(
    ['LIVE_LIST', community_id, user_id],
    getLiveList,
    {
      getNextPageParam: (lastPage) => {
        // 마지막으로 가져온 페이지 아이탬 개수가 LIMIT에 도달하지 않으면 다음 페이지를 가져오지 않는다.
        if (lastPage.data.result.length < LIMIT) {
          return undefined;
        }

        // 마지막으로 가져온 페이지 아이탬 개수가 LIMIT에 도달했다면 다음 페이지 아이탬이 있으므로 (LIMIT와 같다면 없을 수도 있음) 
        const len = lastPage.data.result.length;
        const lastLiveId = lastPage.data.result[len - 1].live_id;
        return lastLiveId;
      },
    },
  );
  return { data, fetchNextPage, isLoading, hasNextPage };
};

📕 구현 (Component)

const LiveAdminPage = () => {
  const { userId: user_id, communityId: community_id } = useModalBasicAPIRequirement();
  const { data, isLoading, fetchNextPage, hasNextPage } = useGetLiveList({ user_id, community_id });
  const queryClient = useQueryClient();
  const [ref, inView] = useInView();

  const liveListReload = () => {
    queryClient.invalidateQueries(['LIVE_LIST', community_id, user_id]);
  };

  useEffect(() => {
    if (inView && data.pages[data.pages.length - 1].data.result.length === 6 && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, data]);

  return (
    <div className="live_admin_page_container">
      <AbsoluteLoadingIndicator isLoading={isLoading} />
      <button onClick={liveListReload}>새로고침</button>
      <div className="live_admin_page_grid">
        {data &&
          data?.pages.map((page) => {
            const pageData = page.data.result;
            return pageData.map((item) => {
              return <LiveAdminItem key={item.live_id} liveItemData={item} />;
            });
          })}

        {hasNextPage && (
          <div className="live_admin_item_container empty">
            <div ref={ref} className="live_loading_indicator" />
          </div>
        )}

        {data && data.pages[0].data.result.length === 0 && (
          <Text text="진행중인 라이브가 없어요." size="18px" weight="700" />
        )}
      </div>
    </div>
  );
};

📌 참고자료

페이지네이션의 개념
Tanstack query - useInfiniteQuery Reference

profile
Frontend Developer

0개의 댓글