Skip to content
문서
페이지네이션

페이지네이션

이 API를 사용하려면 최신 버전(≥ 0.3.0)으로 업데이트해 주세요. 이전의 useSWRPages API는 이제 사용되지 않습니다.

SWR은 페이지네이션인피니트 로딩과 같은 일반적인 UI 패턴을 지원하는 전용 API useSWRInfinite를 제공합니다.

useSWR을 사용하는 시점

페이지네이션

다음과 같은 무언가를 구축한다면 우선 useSWRInfinite은 필요하지 않고 useSWR만 사용하면 됩니다.

...전형적인 페이지네이션 UI입니다. useSWR을 사용해 쉽게 구현하는 방법을 확인해 봅시다.

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // React state인 페이지 인덱스를 포함하는 API URL
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... 로딩 및 에러 상태를 처리
 
  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

이 "페이지 컴포넌트"를 위한 추상화를 생성할 수도 있습니다.

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... 로딩 및 에러 상태를 처리
 
  return data.map(item => <div key={item.id}>{item.name}</div>)
}
 
function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

SWR의 캐시로 인해 다음 페이지를 프리로드할 수 있는 이점을 갖습니다. 숨겨진 div 내에 다음 페이지를 렌더링하므로 SWR이 다음 페이지의 데이터 가져오기를 트리거할 수 있습니다. 사용자가 다음 페이지로 이동하면 데이터가 이미 있습니다.

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

단 한 줄의 코드로 훨씬 더 나은 UX를 얻었습니다. useSWR hook은 아주 강력하며, 대부분의 시나리오를 다룰 수 있습니다.

인피니트 로딩

리스트에 데이터를 이어 붙이는 "더 보기" 버튼(또는 스크롤할 때 자동으로 완료)으로 인피니트 로딩 UI를 구축하길 원하는 경우가 있습니다.

이를 구현하기 위해선 페이지에 동적인 수의 요청을 만들어야 합니다. React Hook은 몇 가지 규칙 (opens in a new tab)을 갖고 있어, 뭔가 다음과 같이 할 수 없습니다.

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 여기가 잘못되었습니다! 일반적으로 반복문 내에 hook을 사용할 수 없습니다.
    const { data } = useSWR(`/api/data?page=${i}`)
    list.push(data)
  }
 
  return <div>
    {list.map((data, i) =>
      <div key={i}>{
        data.map(item => <div key={item.id}>{item.name}</div>)
      }</div>)}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

대신에 이를 위해 생성했던 <Page /> 추상화를 사용합니다.

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

고급 사례

하지만 일부 고급 사례에서는 위 해결책이 동작하지 않습니다.

예를 들어, 동일한 "더 보기" UI를 구현하지만, 전체 항목의 수를 표시해야 할 수도 있습니다. 최상위 레벨 UI(<App />)가 각 페이지 내의 데이터를 필요로하므로, <Page /> 해결책을 더는 사용할 수 없습니다.

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    <p>??? items</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

또한 페이지네이션 API가 커서 기반일 경우에도 이 해결책은 동작하지 않습니다. 이전 페이지로부터의 데이터가 필요하기 때문에 각 페이지가 독립적이지 않습니다.

이것이 새로운 useSWRInfinite Hook이 도움이 되는 방법입니다.

useSWRInfinite

useSWRInfinite는 하나의 Hook으로 많은 요청을 트리거할 수 있습니다. 이렇게 생겼습니다.

import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

useSWR과 유사하게, 이 새로운 Hook은 요청 키, fetcher 함수, 옵션을 반환하는 함수를 받습니다. useSWR이 반환하는 모든 값을 반환하며, 추가로 두 개의 값을 포함합니다: React state와 같이 페이지 크기 및 페이지 크기 setter.

인피니트 로딩에서, 하나의 페이지는 하나의 요청이고, 우리의 목적은 여러 페이지를 가져와 렌더링하는 것입니다.

⚠️

SWR 0.x 버전을 사용중이시면, swr로부터 useSWRInfinite을 임포트 해야 합니다.
import { useSWRInfinite } from 'swr'

API

파라미터

  • getKey: 인덱스와 이전 페이지 데이터를 받고 페이지의 키를 반환하는 함수
  • fetcher: useSWRfetcher 함수와 동일
  • options: useSWR이 지원하는 모든 옵션을 받음. 네 개의 추가 옵션을 포함:
    • initialSize = 1: 초기에 로드해야 하는 페이지의 수
    • revalidateAll = false: 항상 모든 페이지의 갱신 시도
    • revalidateFirstPage = true: always try to revalidate the first page
    • persistSize = false: 첫 페이지의 키가 변경될 때, 페이지 크기를 1(initialSize가 설정된 경우 initialSize)로 초기화하지 않음
    • parallel = false: 여러 페이지를 병렬적으로 동시에 불러옴
💡

initialSize 옵션은 생명 주기 내의 변경을 허용하지 않습니다.

반환 값

  • data: 각 페이지의 응답 값의 배열
  • error: useSWRerror와 동일
  • isLoading: useSWRisLoading과 동일
  • isValidating: useSWRisValidating과 동일
  • mutate: useSWR의 바인딩 된 뮤테이트 함수와 동일하지만 데이터 배열을 다룸
  • size: 가져 페이지 및 반환 페이지의 수
  • setSize: 가져와야 하는 페이지의 수를 설정

예시 1: 페이지네이션 API 기반 인덱스

API 기반 일반 인덱스:

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// 각 페이지의 SWR 키를 얻기 위한 함수,
// `fetcher`에 의해 허용된 값을 반환합니다.
// `null`이 반환된다면, 페이지의 요청은 시작되지 않습니다.
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 끝에 도달
  return `/users?page=${pageIndex}&limit=10`                    // SWR 키
}
 
function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // 이제 모든 users의 수를 계산할 수 있습니다
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }
 
  return <div>
    <p>{totalUsers} users listed</p>
    {data.map((users, index) => {
      // `data`는 각 페이지의 API 응답 배열입니다.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

getKey 함수는 userSWRInfiniteuseSWR 사이에 주요한 차이입니다. 현재 페이지의 인덱스와 이전 페이지의 데이터를 받습니다. 따라서 인덱스 기반 및 커서 기반 페이지네이션 API 모두 잘 지원할 수 있습니다.

또한 data는 이제 단 하나의 API 응답이 아닙니다. 여러 API 응답의 배열입니다.

// `data`는 이렇게 생겼을 것입니다
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

예시 2: 커서 또는 오프셋 기반 페이지네이션 API

이제 API가 커서를 요구하고 데이터와 함께 다음 커서를 반환한다고 해봅시다.

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}

getKey 함수를 이렇게 변경할 수 있습니다.

const getKey = (pageIndex, previousPageData) => {
  // 끝에 도달
  if (previousPageData && !previousPageData.data) return null
 
  // 첫 페이지, `previousPageData`가 없음
  if (pageIndex === 0) return `/users?limit=10`
 
  // API의 엔드포인트에 커서를 추가
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

병렬 데이터 요청

이 API를 사용하려면 최신 버전(≥ 2.1.0)으로 업데이트하세요.

useSWRInfinite의 기본 동작은 키 생성이 이전에 가져온 데이터를 기반으로 하기 때문에 각 페이지에 대한 데이터를 순차적으로 가져오는 것입니다. 그러나 많은 페이지에 대해 순차적으로 데이터를 가져오는 것은 페이지가 상호 의존적이지 않은 경우 최적이 아닐 수 있습니다. parallel 옵션을 true로 지정하면 페이지를 독립적으로 병렬로 가져올 수 있으므로 로드 프로세스가 상당히 빨라질 수 있습니다.

// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData는 항상 `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
⚠️

The previousPageData argument of the getKey function becomes null when you enable the parallel option.

useSWRInfinite를 사용한 Global Mutate

useSWRInfinite는 모든 페이지 데이터를 각 페이지 데이터와 함께 특수 캐시 키로 저장하므로 global mutate로 데이터를 다시 감증하려면 swr/infinite에서 unstable_serialize를 사용해야 합니다.

import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
 
function App() {
    const { mutate } = useSWRConfig()
    mutate(unstable_serialize(getKey))
}
⚠️

이름에서 알 수 있듯이 unstable_serialize는 안정적인 API가 아니기 때문에 앞으로 변경될 수도 있습니다.

고급 기능

useSWRInfinite로 다음 기능들을 구현하는 방법을 보여주는 예시입니다.

  • 로딩 상태
  • 비어 있으면 특별한 UI 보여주기
  • 끝에 도달했을 때 "더 보기" 버튼 비활성화
  • 변경 가능한 데이터 소스
  • 전체 리스트 새로 고침