페이지네이션
이 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
:useSWR
의 fetcher 함수와 동일options
:useSWR
이 지원하는 모든 옵션을 받음. 네 개의 추가 옵션을 포함:initialSize = 1
: 초기에 로드해야 하는 페이지의 수revalidateAll = false
: 항상 모든 페이지의 갱신 시도revalidateFirstPage = true
: always try to revalidate the first pagepersistSize = false
: 첫 페이지의 키가 변경될 때, 페이지 크기를 1(initialSize
가 설정된 경우initialSize
)로 초기화하지 않음parallel = false
: 여러 페이지를 병렬적으로 동시에 불러옴
initialSize
옵션은 생명 주기 내의 변경을 허용하지 않습니다.
반환 값
data
: 각 페이지의 응답 값의 배열error
:useSWR
의error
와 동일isLoading
:useSWR
의isLoading
과 동일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`
}
병렬 데이터 요청
이 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.
Revalidate Specific Pages
Please update to the latest version (≥ 2.2.5) to use this API.
The default behavior of the mutation of useSWRInfinite
is to revalidate all pages that have been loaded. But you might want to revalidate only the specific pages that have been changed. You can revalidate only specific pages by passing a function to the revalidate
option.
function App() {
const { data, mutate, size } = useSWRInfinite(
(index) => [`/api/?page=${index + 1}`, index + 1],
fetcher
);
mutate(data, {
// only revalidate the last page
revalidate: (pageData, [url, page]) => page === size
});
}
useSWRInfinite
를 사용한 Global Mutate
useSWRInfinite
는 모든 페이지 데이터를 각 페이지 데이터와 함께 특수 캐시 키로 저장하므로 global mutate로 데이터를 다시 감증하려면 swr/infinite
에서 unstable_serialize
를 사용해야 합니다.
The revalidate
function is called for each page.
import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
function App() {
const { mutate } = useSWRConfig()
mutate(unstable_serialize(getKey))
}
이름에서 알 수 있듯이 unstable_serialize
는 안정적인 API가 아니기 때문에 앞으로 변경될 수도 있습니다.
고급 기능
useSWRInfinite
로 다음 기능들을 구현하는 방법을 보여주는 예시입니다.
- 로딩 상태
- 비어 있으면 특별한 UI 보여주기
- 끝에 도달했을 때 "더 보기" 버튼 비활성화
- 변경 가능한 데이터 소스
- 전체 리스트 새로 고침