페이지네이션(Pagination) vs 무한 스크롤(Infinite Scroll)

Kingmo·2022년 4월 2일
post-thumbnail

대량의 게시글을 한 번에 전부 불러오면 브라우저 메모리와 네트워크 대역폭에 심각한 부하를 준다.
그래서 데이터를 쪼개서 점진적으로 제공하는 대표적인 방식은 크게 Pagination 방식과 Infinite Scroll 방식이 있다.

1. 페이지네이션 (Pagination)

하단에 1, 2, 3과 같은 페이지 번호 묶음을 띄우고, 사용자가 원하는 번호를 클릭했을 때 해당 구간의 데이터만 새로 조회(Refetch)해 화면을 갱신하는 방식이다.

핵심 구현 로직

  • 마지막 페이지 계산: 전체 게시글 수(totalCount)를 한 페이지당 보여줄 게시글 수(예: 10개)로 나눈 후 올림(Math.ceil) 처리한다.
  • 페이지 번호 그룹화: 번호를 무한정 나열하지 않고 10개 단위 블록(1~10, 11~20)으로 묶는다. startPage 상태를 두고 이전/다음 버튼을 누를 때마다 10씩 가감한다.
  • 경계 조건(Boundary) 예외 처리:
    • 1페이지 블록일 때 이전 버튼 비활성화
    • startPage + 10lastPage를 초과할 때 다음 버튼 비활성화
    • 마지막 페이지 번호 이후의 불필요한 번호 렌더링 방지

구현 코드 (Apollo Client GraphQL 기준)

import { useState } from "react";

export default function PaginationBoard({ data, totalCount, refetch }) {
  // 현재 페이지 블록의 시작 번호 (1, 11, 21...)
  const [startPage, setStartPage] = useState(1);

  // 전체 데이터 수를 기반으로 마지막 페이지 번호 계산
  const lastPage = Math.ceil((totalCount ?? 0) / 10);

  // 특정 페이지 번호 클릭 시 데이터 재호출
  const onClickPage = (pageNumber) => {
    refetch({ page: pageNumber });
  };

  // 이전 10개 페이지 블록으로 이동
  const onClickPrevPage = () => {
    if (startPage === 1) return;
    setStartPage((prev) => prev - 10);
  };

  // 다음 10개 페이지 블록으로 이동
  const onClickNextPage = () => {
    if (startPage + 10 > lastPage) return;
    setStartPage((prev) => prev + 10);
  };

  return (
    <div>
      {/* 1. 게시글 목록 출력 */}
      <div>
        {data?.fetchBoards.map((el) => (
          <div key={el._id}>
            <span>{el.title}</span> | <span>{el.writer}</span>
          </div>
        ))}
      </div>

      {/* 2. 페이지네이션 번호 네비게이션 */}
      <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
        <button onClick={onClickPrevPage} disabled={startPage === 1}>
          &lt;
        </button>

        {/* 10칸짜리 배열을 순회하며 번호 생성 */}
        {new Array(10).fill(1).map((_, i) => {
          const currentPage = startPage + i;
          return (
            currentPage <= lastPage && (
              <span
                key={currentPage}
                onClick={() => onClickPage(currentPage)}
                style={{ cursor: "pointer", fontWeight: "bold" }}
              >
                {currentPage}
              </span>
            )
          );
        })}

        <button onClick={onClickNextPage} disabled={startPage + 10 > lastPage}>
          &gt;
        </button>
      </div>
    </div>
  );
}

2. 무한 스크롤 (Infinite Scroll)

소셜 미디어(유튜브, 인스타그램 등)처럼 스크롤이 페이지 하단 부근에 도달했을 때 백그라운드에서 다음 데이터를 자동으로 불러와 기존 목록 아래에 이어 붙이는 방식이다.

핵심 구현 원리

  • 기존 목록 데이터를 날리고 새 데이터를 덮어쓰는 것이 아니라, 기존 배열 뒤에 새로 불러온 배열을 불변성을 지키며 병합(Concatenate)해야 한다.
  • Apollo Client의 경우 fetchMoreupdateQuery를 사용해 기존 캐시 데이터 뒤에 새 응답 데이터를 결합한다.
  • 더 이상 불러올 데이터가 없을 때 반복 요청을 방지하기 위해 hasMore 플래그를 false로 전환하는 예외 처리가 필수적이다.

구현 코드 (react-infinite-scroller 활용)

import { useQuery, gql } from "@apollo/client";
import InfiniteScroll from "react-infinite-scroller";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
    }
  }
`;

export default function InfiniteBoardPage() {
  const { data, fetchMore } = useQuery(FETCH_BOARDS);

  const onLoadMore = () => {
    if (!data) return;

    // 현재 누적된 개수를 기준으로 다음 페이지 번호 계산
    const nextPage = Math.ceil(data.fetchBoards.length / 10) + 1;

    fetchMore({
      variables: { page: nextPage },
      updateQuery: (prev, { fetchMoreResult }) => {
        // 더 이상 가져올 추가 데이터가 없으면 기존 목록 유지
        if (!fetchMoreResult?.fetchBoards?.length) {
          return { fetchBoards: [...prev.fetchBoards] };
        }

        // 이전 목록과 새로 받아온 목록을 스프레드 연산자로 병합
        return {
          fetchBoards: [...prev.fetchBoards, ...fetchMoreResult.fetchBoards],
        };
      },
    });
  };

  return (
    <InfiniteScroll
      pageStart={0}
      loadMore={onLoadMore}
      hasMore={true} // 데이터 소진 여부에 따라 조건부 제어 필요
    >
      {data?.fetchBoards.map((el) => (
        <div key={el._id} style={{ height: "60px", borderBottom: "1px solid #ccc" }}>
          <span>{el.writer}</span> - <span>{el.title}</span>
        </div>
      )) || <div>데이터를 불러오는 중입니다.</div>}
    </InfiniteScroll>
  );
}

두 방식의 실무 선택 기준

구분페이지네이션 (Pagination)무한 스크롤 (Infinite Scroll)
적합한 서비스쇼핑몰 상품 검색, 어드민 대시보드, 공지사항 게시판SNS 피드, 유튜브 댓글, 핀터레스트형 탐색 뷰
장점특정 위치(페이지)로의 도달과 북마크가 쉽고, 푸터 영역 접근이 용이함탐색의 흐름이 끊기지 않아 콘텐츠 소비 몰입도가 극대화됨
단점매번 번호를 클릭해야 하므로 탐색 피로도가 높음스크롤이 무한정 늘어날 경우 DOM 노드가 과도하게 쌓여 메모리 병목 발생
  • 무한 스크롤 적용 시 주의점: 데이터가 수백, 수천 개로 불어나면 브라우저 렌더링 성능이 저하된다. 데이터가 극단적으로 많아질 가능성이 있다면 현재 화면에 보이는 영역의 DOM만 남기고 나머지는 가상화하는 윈도잉 기법을 반드시 함께 고려해야 한다.
profile
Developer

0개의 댓글