
Tanstack Query의 useInfiniteQuery는 페이지네이션된 데이터를 효율적으로 관리할 수 있게 해주는 훅입니다. useQuery와 달리 useInfiniteQuery는 여러 페이지의 데이터를 누적하여 관리할 수 있게 해줍니다.
먼저 커스텀으로 제작한 useCustomQuery훅입니다.
import { useQuery, UseQueryOptions, QueryFunction, QueryKey } from '@tanstack/react-query';
const defaultQueryOptions = {
refetchOnMount: false, // 다른 탭에 갔다오거나
refetchOnWindowFocus: false, // 마우스를 다시 클릭 했을 때 fetch하는거 false로
};
export default function useCustomQuery<T>(
key: QueryKey,
queryFn: QueryFunction<T>,
options?: Omit<UseQueryOptions<T>, 'queryKey' | 'queryFn'>,
) {
return useQuery<T>({
queryKey: key,
queryFn,
...defaultQueryOptions, // 기본 옵션 추가
...options,
});
}
refetchOnMount: false: 컴포넌트가 마운트될 때마다 데이터를 다시 가져오지 않습니다.refetchOnWindowFocus: false: 윈도우가 포커스를 받을 때마다 데이터를 다시 가져오지 않습니다.타입스크립트의 타입 안정성 보장
제네릭 타입 <T>를 사용하여 다양한 데이터 타입 지원
그리고 무한스크롤용으로 만든 useCustomInfiniteQuery 훅입니다.
import {
useInfiniteQuery, // 무한 스크롤 쿼리를 위한 기본 훅
UseInfiniteQueryOptions, // 무한 스크롤 쿼리 옵션 타입
QueryFunctionContext, // 쿼리 함수의 컨텍스트 타입
InfiniteData, // 무한 스크롤 데이터를 위한 타입
} from '@tanstack/react-query';
const defaultQueryOptions = {
refetchOnMount: false,
refetchOnWindowFocus: false,
initialPageParam: null,
} as const;
export default function useCustomInfiniteQuery<TData>(
queryKey: string[],
queryFn: (context: QueryFunctionContext<string[], unknown>) => Promise<TData>,
options?: Omit<
UseInfiniteQueryOptions<TData, Error, InfiniteData<TData>, TData, string[], unknown>,
'queryKey' | 'queryFn'
>,
) {
return useInfiniteQuery<TData, Error, InfiniteData<TData>, string[]>({
queryKey,
queryFn, // 데이터를 가져오는 함수
getNextPageParam: () => undefined, // 다음 페이지 파라미터 계산 함수 (기본값: undefined)
...defaultQueryOptions, // 기본 옵션 적용
...options,
});
}
useCustomInfiniteQuery 훅은 다음과 같은 특징을 가집니다.
- 페이지네이션된 데이터의 자동 누적
- 여러 페이지의 데이터를 자동으로 배열 형태로 관리
- 페이지 상태 관리 자동화
- 에러 처리와 타입 안정성
- Error 타입을 제네릭으로 지정하여 타입 안전성 보장
- API 요청 실패 시 자동 에러 처리
두 훅은 다음과 같은 차이점이 있습니다.
- 데이터 구조:
useQuery는 단일 데이터,useInfiniteQuery는 pages 배열 구조- 페이지 관리:
useInfiniteQuery는 pageParam을 자동으로 관리- 데이터 누적:
useInfiniteQuery는 이전 데이터를 유지하며 새 데이터를 누적
TanstackQuery에서 제공하는 getNextParam함수를 통해 무한 스크롤을 구현할 수 있습니다.
getNextPageParam 함수는 다음 페이지를 가져오기 위한 파라미터를 결정하는 핵심 함수입니다.
const commonQueryOptions = {
getNextPageParam: (lastPage: PostResponse) => {
if (lastPage.data.lastId === -1) return undefined; // -1일 경우 종료
return lastPage.data.lastId; // lastId 반환
},
initialPageParam: '0', // 첫 페이지 요청 시 사용할 파라미터
};
이 함수는 다음 페이지의 파라미터를 결정합니다. 만약 lastId값이 -1일 경우에는 더 이상 데이터가 없음을 의미합니다. 그 외에는 다시 lastId를 반환시켜주며 계속해서 lastId를 업데이트 시켜줍니다.
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useCustomInfiniteQuery(
['recommendPosts'], // 쿼리 키
({ pageParam = '0' }) => fetchPosts(pageParam as string), // 데이터 fetch 함수
{
...commonQueryOptions, // 공통 옵션 적용
enabled: activeTab === '추천', // 조건부 쿼리 활성화
}
);
useCustomInfiniteQuery의 반환값은 다음과 같습니다.
data: 현재까지 누적된 모든 페이지 데이터isLoading: 초기 데이터 로딩 상태fetchNextPage: 다음 페이지 데이터를 요청하는 함수hasNextPage: 추가 데이터 존재 여부isFetchingNextPage: 다음 페이지 로딩 상태받아온 데이터를 처리하는 유틸리티 함수를 구현합니다.
// 페이지 데이터를 처리하는 유틸 함수
const processPagesData = (pages?: PostResponse[]) => {
if (!pages) return []; // 페이지가 없으면 빈 배열
// 모든 페이지 게시글을 하나의 배열로 합침
return pages.reduce((acc: PostProps[], page: PostResponse) => {
if (page.data.posts) {
return [...acc, ...page.data.posts];
}
return acc;
}, []);
};
이 함수는 페이지별로 나뉘어 있는 데이터를 하나의 배열로 통합하고, reduce를 사용하여 효율적인 배열 처리를 구현합니다.
다음은 Intersection Observer를 사용해 무한 스크롤을 구현하는 로직입니다.
먼저 Instersection Observer를 설치한 뒤 useInView훅을 초기화 합니다.
npm install react-intersection-observer
const { ref, inView } = useInView();
useInView 훅은 react-intersection-observer 라이브러리에서 제공하는 React 훅으로, 특정 요소가 뷰포트에 들어왔는지 감지하는데 사용됩니다.
주요 반환값
ref: 관찰할 요소에 연결할 참조 객체inView: 요소가 뷰포트 내에 있는지 여부 (boolean)그리고 무한 스크롤 구현 로직입니다. inView를 통해 사용자의 화면이 ref 영역에 도달하였을 경우 실행됩니다.
useEffect(() => {
if (inView) {
switch (activeTab) {
case '추천':
if (hasRecommendNextPage && !isFetchingRecommendNextPage) {
fetchRecommendNextpage();
}
break;
case '팔로잉':
if (hasFollowingNextPage && !isFetchingFollowingNextPage) {
fetchFollowingNextPage();
}
break;
...
}
}
}, [의존성배열]);
해당 로직은 다음과 같은 특징을 가집니다.
- 효율적인 감지
- Intersection Observer가 요소의 가시성을 효율적으로 감지합니다.
- 스크롤 이벤트와 달리 성능 오버헤드가 적습니다.
- 비동기적으로 작동하여 메인 스레드 블로킹을 방지합니다.
- 중복 요청 방지
hasNextPage와isFetchingNextPage플래그를 통해 중복 요청을 방지합니다.- 데이터 페칭의 상태를 정확하게 관리합니다.
- 불필요한 API 호출을 최소화하여 서버 부하를 줄입니다.
이렇게 Intersection Observer와 useCustomInfiniteQuery를 사용해 간편하게 무한스크롤을 구현할 수 있습니다.
다시 한 번 프로젝트에서 사용한 로직을 간단하게 설명하면 다음과 같습니다.
- 초기 데이터 로드 ➡️ 첫 10개 데이터 + lastId 반환
- 사용자의 스크롤이 ref에 도달하면 Intersection Observer 감지 ➡️ ref 요소 가시성 확인
- lastId를 담아서 함께 새로운 요청 ➡️ 요청에 대한 값을 반환 받음
- 반환 받은 lastId가 -1이라면 마지막 데이터로 간주하여 더이상 요청을 보내지 않음