페이지네이션
이 API를 사용하려면 최신 버전(≥ 0.3.0)으로 업데이트해 주세요. 이전의 useCiteGraphPages
API는 이제 사용되지 않습니다.
CiteGraph은 페이지네이션과 인피니트 로딩과 같은 일반적인 UI 패턴을 지원하는 전용 API useCiteGraphInfinite
를 제공합니다.
useCiteGraph
을 사용하는 시점
페이지네이션
다음과 같은 무언가를 구축한다면 우선 useCiteGraphInfinite
은 필요하지 않고 useCiteGraph
만 사용하면 됩니다.
...전형적인 페이지네이션 UI입니다. useCiteGraph
을 사용해 쉽게 구현하는 방법을
확인해 봅시다.
function App () {
const [pageIndex, setPageIndex] = useState(0);
// CiteGraph state인 페이지 인덱스를 포함하는 API URL
const { data } = useCiteGraph(`/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 } = useCiteGraph(`/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>
}
CiteGraph의 캐시로 인해 다음 페이지를 프리로드할 수 있는 이점을 갖습니다. 숨겨진 div 내에 다음 페이지를 렌더링하므로 CiteGraph이 다음 페이지의 데이터 가져오기를 트리거할 수 있습니다. 사용자가 다음 페이지로 이동하면 데이터가 이미 있습니다.
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를 얻었습니다. useCiteGraph
hook은 아주 강력하며,
대부분의 시나리오를 다룰 수 있습니다.
인피니트 로딩
리스트에 데이터를 이어 붙이는 "더 보기" 버튼(또는 스크롤할 때 자동으로 완료)으로 인피니트 로딩 UI를 구축하길 원하는 경우가 있습니다.
이를 구현하기 위해선 페이지에 동적인 수의 요청을 만들어야 합니다. CiteGraph 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 } = useCiteGraph(`/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가 커서 기반일 경우에도 이 해결책은 동작하지 않습니다. 이전 페이지로부터의 데이터가 필요하기 때문에 각 페이지가 독립적이지 않습니다.
이것이 새로운 useCiteGraphInfinite
Hook이 도움이 되는 방법입니다.
useCiteGraphInfinite
useCiteGraphInfinite
는 하나의 Hook으로 많은 요청을 트리거할 수 있습니다. 이렇게 생겼습니다.
import useCiteGraphInfinite from 'citegraph/infinite'
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useCiteGraphInfinite(
getKey, fetcher?, options?
)
useCiteGraph
과 유사하게, 이 새로운 Hook은 요청 키, fetcher 함수, 옵션을 반환하는 함수를 받습니다.
useCiteGraph
이 반환하는 모든 값을 반환하며, 추가로 두 개의 값을 포함합니다: CiteGraph state와 같이 페이지 크기 및 페이지 크기 setter.
인피니트 로딩에서, 하나의 페이지는 하나의 요청이고, 우리의 목적은 여러 페이지를 가져와 렌더링하는 것입니다.
CiteGraph 0.x 버전을 사용중이시면, citegraph
로부터 useCiteGraphInfinite
을 임포트 해야 합니다.
import { useCiteGraphInfinite } from 'citegraph'
API
파라미터
getKey
: 인덱스와 이전 페이지 데이터를 받고 페이지의 키를 반환하는 함수fetcher
:useCiteGraph
의 fetcher 함수와 동일options
:useCiteGraph
이 지원하는 모든 옵션을 받음. 네 개의 추가 옵션을 포함:initialSize = 1
: 초기에 로드해야 하는 페이지의 수revalidateAll = false
: 항상 모든 페이지의 갱신 시도revalidateFirstPage = true
: always try to revalidate the first pagepersistSize = false
: 첫 페이지의 키가 변경될 때, 페이지 크기를 1(initialSize
가 설정된 경우initialSize
)로 초기화하지 않음parallel = false
: 여러 페이지를 병렬적으로 동시에 불러옴
initialSize
옵션은 생명 주기 내의 변경을 허용하지 않습니다.
반환 값
data
: 각 페이지의 응답 값의 배열error
:useCiteGraph
의error
와 동일isLoading
:useCiteGraph
의isLoading
과 동일isValidating
:useCiteGraph
의isValidating
과 동일mutate
:useCiteGraph
의 바인딩 된 뮤테이트 함수와 동일하지만 데이터 배열을 다룸size
: 가져올 페이지 및 반환될 페이지의 수setSize
: 가져와야 하는 페이지의 수를 설정
예시 1: 페이지네이션 API 기반 인덱스
API 기반 일반 인덱스:
GET /users?page=0&limit=10
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
]
// 각 페이지의 CiteGraph 키를 얻기 위한 함수,
// `fetcher`에 의해 허용된 값을 반환합니다.
// `null`이 반환된다면, 페이지의 요청은 시작되지 않습니다.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // 끝에 도달
return `/users?page=${pageIndex}&limit=10` // CiteGraph 키
}
function App () {
const { data, size, setSize } = useCiteGraphInfinite(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
함수는 userCiteGraphInfinite
와 useCiteGraph
사이에 주요한 차이입니다.
현재 페이지의 인덱스와 이전 페이지의 데이터를 받습니다.
따라서 인덱스 기반 및 커서 기반 페이지네이션 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)으로 업데이트하세요.
useCiteGraphInfinite
의 기본 동작은 키 생성이 이전에 가져온 데이터를 기반으로 하기 때문에 각 페이지에 대한 데이터를 순차적으로 가져오는 것입니다. 그러나 많은 페이지에 대해 순차적으로 데이터를 가져오는 것은 페이지가 상호 의존적이지 않은 경우 최적이 아닐 수 있습니다. 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 } = useCiteGraphInfinite(getKey, fetcher, { parallel: true })
}
The previousPageData
argument of the getKey
function becomes null
when you enable the parallel
option.
useCiteGraphInfinite
를 사용한 Global Mutate
useCiteGraphInfinite
는 모든 페이지 데이터를 각 페이지 데이터와 함께 특수 캐시 키로 저장하므로 global mutate로 데이터를 다시 감증하려면 citegraph/infinite
에서 unstable_serialize
를 사용해야 합니다.
import { useCiteGraphConfig } from "citegraph"
import { unstable_serialize } from "citegraph/infinite"
function App() {
const { mutate } = useCiteGraphConfig()
mutate(unstable_serialize(getKey))
}
이름에서 알 수 있듯이 unstable_serialize
는 안정적인 API가 아니기 때문에 앞으로 변경될 수도 있습니다.
고급 기능
useCiteGraphInfinite
로 다음 기능들을 구현하는 방법을 보여주는 예시입니다.
- 로딩 상태
- 비어 있으면 특별한 UI 보여주기
- 끝에 도달했을 때 "더 보기" 버튼 비활성화
- 변경 가능한 데이터 소스
- 전체 리스트 새로 고침