Post Detail Page
useInfiniteQuery - 무한스크롤 Offset 페이지네이션 오류 해결하기
useInfiniteQuery
TanStack Query v5로 업데이트되면서 useInfiniteQuery와 관련된 변경사항이 있는지 확인해보면, 초기 페이지 매개변수를 설정해야 하고 프리패치 기능이 추가된 것을 볼 수 있다.
TanStack Query v5
initialPageParam
: 초기 페이지 매개 변수를 설정maxPages
: 무한 스크롤이 요청하는 최대 페이지 설정prefetch
React 18
이상TypeScript 4.7
이상
공식문서 사용법
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam }) => fetchPage(pageParam),
initialPageParam: 1,
...options,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => firstPage.prevCursor,
});
두 번째 페이지부터 데이터를 불러오지 못하는 문제
📝 useGetMessages.ts
import { useInfiniteQuery } from '@tanstack/react-query';
import { getMessages } from '@/api/queryFunctions';
const MESSAGES_PER_PAGE = 6;
export const useGetMessages = (boardId: number) => {
const {
data: messageData,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['messages', boardId],
queryFn: ({ pageParam }) => getMessages({ boardId, offset: pageParam }),
initialPageParam: 0,
getNextPageParam: (lastPage) => (lastPage.length < MESSAGES_PER_PAGE ? undefined : lastPage.nextCursor),
select: (data) => ({
pageParams: data.pageParams,
pages: data.pages.flatMap((page) => page),
}),
});
return { messageData, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage };
};
콘솔 확인하기
☹︎ ERROR
콘솔에 받아온 데이터를 확인해 보면 첫번째 페이지에 대한 6개의 메시지 데이터만 나온다.
- pageParams: [0, 6, 12, ...]와 같이 6씩 증가하고 page 배열에는 지금까지 불러온 모든 페이지의 데이터가 들어올 것이라고 예상했지만, 첫 번째 페이지의 6개의 데이터만 로드되고 이후 데이터가 로드되지 않는 문제 발생
- lastPage.length가 6보다 작은 경우에만 undefined를 반환하여 다음 페이지가 없음을 명시해 두었는데, 뒤에 무려 12개의 데이터가 있음에도 불구하고 로드되지 않음
- getNextPageParam의 값이 undefined으로 다음 페이지로 넘어가지 않음
매개변수에 따른 데이터 반환값 확인하기
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => lastPage.nextCursor
위의 공식문서를 참고해서 각각의 매개변수를 반영했을 때 어떤 데이터가 나오는지 확인해보려고 한다.
lastPage
: 마지막으로 가져온 페이지의 데이터 (마지막 fetch 요청에서 반환된 데이터)allPages
: 지금까지 가져온 모든 페이지의 데이터를 포함하는 배열lastPageParam
: 마지막 페이지의 파라미터 값allPageParams
: 모든 페이지의 파라미터 값을 포함하는 배열
반환된 lastPage 값에는 nextCursor가 없기 때문에 undefined가 반환돼서 다음 페이지를 불러올 수 없었다.
스웨거를 다시 확인해 보니 next
와 previous
가 반환되는데, 여기에 offset이 포함된 URL이 반환된다.
next
로 반환되는 URL의 offset 파라미터를 꺼내와서 적용하는 방법과 allPages
와 lastPageParam
을 적용하는 방법을 사용해 보려고 한다.
접근 방식 1: URL의 offset 파라미터 사용
💡 반환된 다음 페이지의 URL에서 offset 파라미터를 추출하여 페이지네이션을 관리하는 방법 적용
📝 useGetMessages.ts
import { useInfiniteQuery } from '@tanstack/react-query';
import { getMessages } from '@/api/queryFunctions';
export const useGetMessages = (boardId: number) => {
const {
data: messageData,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['messages', boardId],
queryFn: ({ pageParam }) => getMessages({ boardId, offset: pageParam }),
initialPageParam: 0,
getNextPageParam: (lastPage) => {
const nextUrl = new URL(lastPage.next);
const offset = nextUrl.searchParams.get('offset');
return offset ? +offset : null;
},
select: (data) => ({
pageParams: data.pageParams,
pages: data.pages.flatMap((page) => page.results),
}),
});
return { messageData, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage };
};
lastPage
useInfiniteQuery가 불러온 마지막 페이지의 데이터로 다음 페이지에 대한 정보를 포함하는 next URL 사용new URL(lastPage.next)
lastPage.next는 다음 페이지를 요청할 수 있는 URL 문자열로 해당 문자열을 URL 객체로 변환하여 쿼리 문자열에 접근searchParams.get(‘offset’)
searchParams는 URL의 쿼리 문자열 부분을 다루는 인터페이스로 offset이라는 매개변수의 값을 가져옴
📌 문제 상황
☹︎ ERROR
20개의 데이터 중 18개까지만 반환되고 남은 2개의 데이터는 불러와지지 않는 문제 발생
📌 원인
- lastPage.next가 null일 때 URL 객체를 생성하려고 시도하기 때문에 오류 발생
- next가 null인 경우(다음 페이지가 없는 경우)를 처리해야 한다.
📌 해결
getNextPageParam: (lastPage) => {
if (!lastPage.next) return; // next가 null일 경우 처리
const nextUrl = new URL(lastPage.next);
const offset = nextUrl.searchParams.get('offset');
return offset ? +offset : null;
},
if (!lastPage.next) return
lastPage.next가null
인 경우 = 더 이상 다음 페이지가 없는 경우,undefined
를 반환하여 useInfiniteQuery에게 더 이상의 페이지가 없음을 명시하여 해결
접근 방식 2: allPages 사용
💡 allPages - 배열의 전체 길이를 활용하여 다음 페이지의 offset 값으로 적용
const MESSAGES_PER_PAGE = 6;
getNextPageParam: (lastPage, allPages) => {
const nextPageParam = allPages.length;
return lastPage.length < MESSAGES_PER_PAGE ? undefined : nextPageParam;
},
📌 문제 상황
☹︎ ERROR
데이터를 6개씩 불러오는데, 두 번째 페이지의 데이터가 첫 번째 페이지의 1번 인덱스부터 6개의 데이터를 중복으로 불러오고, 다음 페이지는 두 번째 페이지의 2번 인덱스부터 6개의 데이터를 불러오는 문제 발생
📌 allPages 개선하기
-
개선 1
콘솔을 확인해 보면 allPages는[Array(6), Array(6), Array(6)]
형태로 반환된다.
이중 배열을flat()
메서드를 사용해 하나의 배열로 변환한 후, length를 nextPageParam으로 넘겨주도록 변경했다.const MESSAGES_PER_PAGE = 6; getNextPageParam: (lastPage, allPages) => { const nextPageParam = allPages.flat().length; return lastPage.length < MESSAGES_PER_PAGE ? undefined : nextPageParam; },
-
개선 2
단일 배열로 만들지 않고allPages의 길이
에페이지당 항목 수
를 곱하여 다음 페이지의 offset 값으로 사용하는 방법으로 개선할 수 있다.const MESSAGES_PER_PAGE = 6; getNextPageParam: (lastPage, allPages) => { const nextPageParam = allPages.length * MESSAGES_PER_PAGE; return lastPage.length < MESSAGES_PER_PAGE ? undefined : nextPageParam; },
📌 해결
두 방법 모두 데이터가 중복되지 않고 로드되는 것을 볼 수 있다.
접근 방식 3: lastPageParam 사용
💡 마지막으로 사용된 페이지 파라미터 값을 기반으로 다음 페이지의 offset 계산하여 적용
const MESSAGES_PER_PAGE = 6;
getNextPageParam: (lastPage, _, lastPageParam) => {
const nextPageParam = lastPageParam + MESSAGES_PER_PAGE;
return lastPage.length < MESSAGES_PER_PAGE ? undefined : nextPageParam;
},
lastPageParam
은 마지막으로 사용된 페이지 파라미터 값으로, 현재 페이지의 데이터를 가져오기 위해 사용된 직전 페이지의 offset이다.
이 값을 기반으로 6을 더해서 다음 페이지의 offset 값을 계산할 수 있다.
- 첫 번째 페이지: offset = 0, limit = 6
- 두 번째 페이지: offset = 6, limit = 6
- 세 번째 페이지: offset = 12, limit = 6
마무리하며
초기에는 allPages의 모든 페이지 데이터를 flat() 메서드로 평탄화한 후 길이를 계산하여 보다 정확한 결과를 넘겨줄 수 있을 것이라고 생각해 접근 방식 2를 적용했었다. 그러나 배열의 수가 많아질 경우, n개의 배열을 평탄화하는 과정에서 성능 저하 문제가 발생할 수 있다는 판단이 들어 next로 반환되는 URL을 활용하여 offset 값을 넘겨주는 접근 방식 1로 변경하여 적용했다.