본문 바로가기

Codeit Frontend PB

Web API: Pagination

➡️ 정리에 참고한 영상입니다.

http://www.example.com/products?limit=50

 

 

 

페이지네이션이란 데이터를 나눠서 가져와 렌더링해주는 것이다.

예를 들면 온라인 쇼핑몰에서 1~10페이지까지 상품 목록을 조회하는 것이나, 넷플릭스에서 콘텐츠 더보기란을 눌러

10개 혹은 20개씩 추가로 콘텐츠를 조회하는 동작 등을 말한다.

 

페이지네이션 동작 기법에는 크게 두가지가 있다. 이 두가지에 대한 동작 원리를 알아보자.

 

1️⃣ Offset-based Pagination

 

Client측에서 request를 보낼 때 제공해야 하는 정보는 limitoffset이 있다.

아래 경우, 101~150 item들을 가져온다.

GET/products?limit=50&offset=100

 

📌 Limit: 한 페이지당 batch 가능한 최대 item 개수

📌 Offset: item list에서의 starting position ( = 지금까지 받아온 데이터의 개수)


1) https://www.codeit.kr/topics/handling-data-with-react/lessons/5035
2) https://www.codeit.kr/topics/handling-data-with-react/lessons/5043



2️⃣ Cursor-based Pagination

커서 기반 페이지네이션을 이해하기 전,  중요하게 짚고 넘어가야 할 배경지식이 하나 있다.

 

⚠️ 배열 인덱스와 고유 ID간의 관계

배열 인덱스는 배열에 부여된 고유한 값이 아니다.
따라서 id값과 같이 배열의 순서 상관없이 항상 고유한 값을 갖는 변수를 페이지네이션에 이용해야 한다.

배열 인덱스의 경우 배열 요소가 재정렬되거나 요소가 추가 혹은 삭제되면 특정 item이 갖는 인덱스 값은 변경될 수 있다.

 

커서 기반 페이지네이션에서는 페이지네이션에 다음 커서값을 함께 넘겨준다.

데이터가 바뀌더라도 커서가 가리키는 데이터는 바뀌지 않게 하기 위해서 각 요소에 부여된 고유 ID 값을 사용해야한다.

 

client측에서 request를 보낼 때는 limit값을 함께 전달해야 한다.

# 1) client requests with a limit
https://www.example.com/products?limit=50

# 2) server responds with results and a next-cursor
next-cursor=12345678

# 3) client includes this cursor in subsequent requests
https://www.example.com/products?limit=50&nextCursor=12345678

 

 

각 방식의 장단점?
커서 기반 페이지네이션 방법이 결과의 일관성을 보장하기에는 더 좋고, large datasets를 렌더링해야 하는 경우에 더욱 좋다. 오프셋 기반 페이지네이션의 경우 데이터가 소실되거나 중복되는 문제가 발생할 수 있기 때문이다.

대신, 오프셋 기반 페이지네이션이 구현이 비교적 간단하다는 장점이 있다.

 

 

 

 

 

+) 코드잇 강의 실습에서 작성한 커서 기반 페이지네이션 사용 코드

import { useEffect, useState } from 'react';
import { getFoods } from '../api';
import FoodList from './FoodList';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [items, setItems] = useState([]);
  const [nextCursor, setNextCursor] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  const handleNewestClick = () => setOrder('createdAt');
  const handleCalorieClick = () => setOrder('calorie');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (orderQuery) => {
    setIsLoading(true);
    try {
      const response = await fetch(`https://learn.codeit.kr/api/foods?order=${orderQuery}&limit=10`);
      const data = await response.json();
      setItems(data.foods);
      setNextCursor(data.paging.nextCursor);
    } catch (error) {
      console.error('Failed to load foods:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const handleLoadMore = async () => {
    if (!nextCursor || isLoading) return;
    setIsLoading(true);
    try {
      const response = await fetch(`https://learn.codeit.kr/api/foods?cursor=${nextCursor}&limit=10`);
      const data = await response.json();
      setItems((prevItems) => [...prevItems, ...data.foods]);
      setNextCursor(data.paging.nextCursor);
    } catch (error) {
      console.error('Failed to load more foods:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  useEffect(() => {
    handleLoad(order);
  }, [order]);

  return (
    <div>
      <button onClick={handleNewestClick}>최신순</button>
      <button onClick={handleCalorieClick}>칼로리순</button>
      <FoodList items={sortedItems} onDelete={handleDelete} />
      <button onClick={handleLoadMore} disabled={!nextCursor || isLoading}>
        더보기
      </button>
    </div>
  );
}

export default App;