
대량의 게시글을 한 번에 전부 불러오면 브라우저 메모리와 네트워크 대역폭에 심각한 부하를 준다.
그래서 데이터를 쪼개서 점진적으로 제공하는 대표적인 방식은 크게 Pagination 방식과 Infinite Scroll 방식이 있다.
하단에 1, 2, 3과 같은 페이지 번호 묶음을 띄우고, 사용자가 원하는 번호를 클릭했을 때 해당 구간의 데이터만 새로 조회(Refetch)해 화면을 갱신하는 방식이다.
totalCount)를 한 페이지당 보여줄 게시글 수(예: 10개)로 나눈 후 올림(Math.ceil) 처리한다.startPage 상태를 두고 이전/다음 버튼을 누를 때마다 10씩 가감한다.startPage + 10이 lastPage를 초과할 때 다음 버튼 비활성화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}>
<
</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}>
>
</button>
</div>
</div>
);
}
소셜 미디어(유튜브, 인스타그램 등)처럼 스크롤이 페이지 하단 부근에 도달했을 때 백그라운드에서 다음 데이터를 자동으로 불러와 기존 목록 아래에 이어 붙이는 방식이다.

fetchMore와 updateQuery를 사용해 기존 캐시 데이터 뒤에 새 응답 데이터를 결합한다.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 노드가 과도하게 쌓여 메모리 병목 발생 |