<캐럿마켓 클론코딩> 강의에서 니꼬쌤이 pagination 코드 챌린지를 권유했다. SWR은 take와 skip을 이용해 pagination 구현 어렵지 않았다.

GET 요청을 받으면 query에 담긴 page와 limit을 받아온다. string으로 오기 때문에 +를 붙여 number로 형변환한다.
pagination 로직은 (현재 page - 1) * limit을 skip에 할당하면 된다. take는 가져올 데이터의 양, skip은 건너뛸 데이터의 양을 의미한다. 예를 들어, take: 10, skip: 10이라면 0~9번 데이터를 패스하고 다음 10개, 즉 10~19번 데이터를 가져오는 셈이다.
// user.ts
if (req.method === "GET") {
const {
query: { page, limit },
} = req;
const products = await client.products.findMany({
include: {
user: {
select: {
id: true,
},
},
},
take: +limit,
skip: (+page - 1) * +limit,
orderBy: { created: "desc" },
});
res.json({
ok: true,
products,
});
}
useSWR로 page와 limit을 파라미터로 담아 요청을 보내고, 받아온 데이터로 화면을 구성한다. 버튼의 onClick으로 이전 페이지라면 page - 1, 다음 페이지라면 page + 1한다.
const Products = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const { data } = useSWR(
`/api/products?page=${page}&limit=${limit}`
);
const onPrevBtn = () => {
setPage((prev) => prev - 1);
};
const onNextBtn = () => {
setPage((prev) => prev + 1);
};
return (
<div>
{data?.products?.map((product) => (
<div key={product.id}>
<span>{product.name}</span>
<span>{product.price}</span>
</div>
))}
</div>
<button onClick={onPrevBtn}>이전</button>
<button onClick={onNextBtn}>다음</button>
);
};
export default Products;

SWR에서는 무한 스크롤을 위한 useSWRInfinite함수도 제공한다. Backend부분은 같다.
const getKey = (pageIndex, previousPageData) => {
if (pageIndex === 0) return `/api/products?&page=1&limit=10`;
if (pageIndex + 1 > +previousPageData.pages) return null;
return `/api/products?&page=${pageIndex + 1}&limit=10`;
};
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function ProductList() {
const { data, setSize, size } = useSWRInfinite(getKey, fetcher);
const products = data ? data.map((item) => item.products).flat() : [];
const onNextBtn = () => {
setSize((prev) => prev + 1);
};
return data ? (
<>
<div>
{products?.map((product) => (
<div key={product.id}>
<span>{product.name}</span>
<span>{product.price}</span>
</div>
))}
</div>
<button onClick={onNextBtn}>더 보기</button>
</>
) : null;
}
getKey함수를 설정해야 하는데, 두 가지 인자(pageIndex, previousPageData)만 받아서 limit은 url에 직접 적어뒀다. 유저가 수정할 이유는 없을 테니, Backend로 빼도 괜찮겠다.
getKey로 인해 페이지가 변동될 때마다 데이터를 추가로 로드하는데, useSWR은 페이지를 새로고침해 렌더링하는 반면, useSWRInfinite는 새로고침 없이 추가로 데이터를 렌더링한다. 따라서 더 보기 버튼을 누르면 현재의 마지막 데이터 밑에 새로운 데이터가 추가된다. 버튼 대신 scroll이벤트를 사용할 수도 있다.
const [page, setPage] = useState(1);
function handleScroll() {
if (
document.documentElement.scrollTop + window.innerHeight ===
document.documentElement.scrollHeight
) {
setPage((p) => p + 1);
}
}
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);