페이지네이션
이 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은 몇 가지 규칙을 갖고 있어, 뭔가 다음과 같이 할 수 없습니다.
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, 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
:useSWR
의 fetcher 함수와 동일options
:useSWR
이 지원하는 모든 옵션을 받음. 세 개의 추가 옵션을 포함:initialSize = 1
: 초기에 로드해야 하는 페이지의 수revalidateAll = false
: 항상 모든 페이지의 갱신 시도persistSize = false
: 첫 페이지의 키가 변경될 때, 페이지 크기를 1(initialSize
가 설정된 경우initialSize
)로 초기화하지 않음
initialSize
옵션은 생명 주기 내의 변경을 허용하지 않습니다.
반환 값
data
: 각 페이지의 가져오기 응답 값의 배열error
:useSWR
의error
와 동일isValidating
:useSWR
의isValidating
과 동일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
함수는 userSWRInfinite
와 useSWR
사이에 주요한 차이입니다.
현재 페이지의 인덱스와 이전 페이지의 데이터를 받습니다.
따라서 인덱스 기반 및 커서 기반 페이지네이션 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`}
고급 기능
useSWRInfinite
로 다음 기능들을 구현하는 방법을 보여주는 예시입니다.
- 로딩 상태
- 비어 있으면 특별한 UI 보여주기
- 끝에 도달했을 때 "더 보기" 버튼 비활성화
- 변경 가능한 데이터 소스
- 전체 리스트 새로 고침