-
Notifications
You must be signed in to change notification settings - Fork 8
Cursor Based Pagination을 구현해보자
changgunyee edited this page Dec 21, 2019
·
1 revision
- 오프셋 - DB의 offset쿼리를 이용하여 페이지 단위로 구분하여 요청/응답하게 구현
- 커서 - 클라이언트가 가져간 마지막 row의 순서상 다음 row들을 n개 요청/응답하게 구현
클라이언트가 1 페이지를 보고있는 사이, DB에 item이 추가가 된다면 클라이언트가 2 페이지로 넘겼을 때 1페이지 내용을 또 접할 수 있습니다.
DB는 offset을 가진 쿼리를 만났을 때 해당 쿼리의 모든 값들을 전부 만들어 지정된 갯수만큼을 순회하여 자르는 방식을 사용합니다. row수가 아주 많으면 offset값이 올라갈수록 쿼리의 퍼포먼스는 더욱 떨어지게 되어있습니다.
커서 기반으로 페이지네이션을 위해서는 반드시 정렬 기준이 되는 필드 중 하나는 고유값이어야 합니다.(구분을 위해) or절을 이용한 구현과 커스텀 cursor를 이용하여 가격이 14100이상인 상품 목록을 조회하는 쿼리를 만들어 비교해 보겠습니다.
SELECT id, title, price
FROM `products`
WHERE
(price > 14100
OR
(price = 14100 AND id > 446))
ORDER BY price ASC, id ASC
LIMIT 5
위와 같은 방식으로도 구현이 가능합니다.
하지만 클라이언트가 ORDER BY에 걸려있는 모든 필드를 알아야하고, 매 페이지 요청 시마다 이값들을 전부 보내야하기 때문에 불편합니다.
SELECT id, title, price,
CONCAT(LPAD(price, 10, '0'), LPAD(id, 10, '0')) as `cursor`
FROM `products`
HAVING `cursor` < '00000058000000000242'
ORDER BY price DESC, id DESC
LIMIT 5;
위 와같이 커스텀으로 커서 attribute를 만든다면 클라이언트에서 필요한 필드를 몰라도 일관성있게 쉽게 처리할 수 있습니다.
{
products(first: 10, after: "beforeLastCursor") {//지난 페이지의 마지막 커서를 기준으로 다음 10개의 데이터를 가져옴
edges {
cursor
node {//실제 하나의 데이터
id
title
price
}
}
pageInfo {
lastCursor//현재 페이지에서 마지막 커서
hasNextPage
}
}
}
쿼리를 보낼 때 위와 같은 형식의 쿼리를 보내게 됩니다.
- first : 페이지의 원하는 데이터 개수입니다.
- after: 해당 cursor이후의 데이터를 가지고 옵니다.
- edges: 원하는 데이터의 array
- node: 단일 데이터
- pageInfo: 현재 페이지의 정보
Apollo-client에서는 useQuery와 같은 훅에서 fetchmore함수를 지원합니다
fetchmore함수에서 이전 데이터와 받아온 데이터를 합쳐 local state에 저장하고 query를 업데이트하여 간단하게 매번 새로운 페이지를 받아올 수 있습니다.
const COMMENTS_QUERY = gql`
query Comments($cursor: String) {
Comments(first: 10, after: $cursor) {
edges {
node {
author
text
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`;
function CommentsWithData() {
const { data: { Comments: comments }, loading, fetchMore } = useQuery(
COMMENTS_QUERY
);
return (
<Comments
entries={comments || []}
onLoadMore={() =>
fetchMore({
variables: {
cursor: comments.pageInfo.endCursor
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const newEdges = fetchMoreResult.comments.edges;
const pageInfo = fetchMoreResult.comments.pageInfo;
return newEdges.length
? {
// Put the new comments at the end of the list and update `pageInfo`
// so we have the new `endCursor` and `hasNextPage` values
comments: {
__typename: previousResult.comments.__typename,
edges: [...previousResult.comments.edges, ...newEdges],
pageInfo
}
}
: previousResult;
}
})
}
/>
);
}