페이지네이션
이 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 })
}
parallel
옵션을 활성화하면 getKey
함수의 previousPageData
인수가 null
이 됩니다.
특정 페이지 재검증
이 API를 사용하려면 최신 버전(≥ 2.2.5)으로 업데이트하세요.
useSWRInfinite
의 기본 동작은 로드된 모든 페이지를 재검증하는 것입니다. 하지만 변경된 특정 페이지만 재검증하고 싶을 수도 있습니다. 이 경우, revalidate
옵션에 함수를 전달하여 특정 페이지만 재검증할 수 있습니다.
function App() {
const { data, mutate, size } = useSWRInfinite(
(index) => [`/api/?page=${index + 1}`, index + 1],
fetcher
);
mutate(data, {
// 마지막 페이지에 대해서만 재검증을 수행
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 보여주기
- 끝에 도달했을 때 "더 보기" 버튼 비활성화
- 변경 가능한 데이터 소스
- 전체 리스트 새로 고침