무한 스크롤이란 사용자가 페이지 하단에 도달했을 때, 컨텐츠가 계속 로드되는 사용자 경험(UX) 입니다. 무한 스크롤을 구현하기 위해 필요한 기술은 다음과 같습니다.
컨텐츠를 가져오는 data fetch 는 캐싱과 최적화가 가능한 react-query
를, 페이지 하단 요소 관찰을 위해서 react-intersection-observer
를 활용하여 구현해보도록 하겠습니다.
리액트에서 데이터를 fetch, cache, update 하는 라이브러리인 React Query에서 useInfiniteQuery
Hook을 제공합니다.
React Query 공식 문서에 따르면 다음과 같은 매개변수와 반환 값을 가집니다.
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery(
queryKey,
({ pageParam = 1 }) => fetchPage(pageParam),
{
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
}
);
react-query가 데이터를 캐싱할 때 사용하는 key
값 입니다.
개발자가 임의로 지정할 수 있습니다.
불러오는 데이터 마다 다른 key
값을 사용해야합니다.
({ pageParam = 1 }) => fetchPage(pageParam)
두 번째 인수는 데이터를 요청하는 API 함수가 포함된 함수입니다.
fetchPage
는 응답으로 받은 값을 반환해야합니다.
무한스크롤을 구현해야 하는 상황에서는 다음 요청을 위한 데이터도 포함해서 반환할 수 있습니다.
예를들어 현재는 1 페이지 데이터를 요청했다면, 다음에는 2 페이지 데이터를 요청하도록 반환값에 결과값과 함께 담아서 내보낼 수 있습니다.
pageParam
는 이때 사용하는 값으로, 초기값으로 처음에 시작할 페이지를 설정할 수 있습니다.
정리하자면 다음과 같이 nextPage
와 isLast
정보를 결과 값과 함께 내보내면, 다음 요청시 활용할 수 있습니다.
const fetchPage = ({ pageParam = 0 }) => {
// API
const { data } = await getData({ startIndex: pageParam });
// 다음 요청시 사용할 nextPage와 isLast
return {
result: data,
nextPage: pageParam + 1,
isLast: data.isLast,
}
}
{
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
}
마지막 인수는 options로 추가적인 기능을 제공합니다.
getNextPAgeParam
는 추가적으로 데이터를 fetch 하는 경우에, 두 번째 인수였던 콜백 함수가 반환한 값을 가져와서 사용할 수 있습니다. 따라서 위의 예제에서는 nextPage
를 객체에 담아서 반환했으므로, lastPage.nextPage
로 사용할 수 있습니다.
getNextPageParam
는 반드시 하나의 값만 반환해야하며, 그 값은 두 번째 인수인 콜백 함수를 호출할 때 인수로 전달됩니다.
만약 undefined
를 반환하는 경우엔 fetch 콜백을 호출하지 않기 때문에, 마지막 페이진 경우엔 이를 활용하면 되겠습니다.
fetchNextPage
콜백해당 함수를 호출하면 다음 페이지의 데이터를 요청할 수 있습니다. 이 경우 두 번째 인수로 넘겨졌던 콜백이 다음 페이지 정보를 매개변수로 받은 다음 호출됩니다.
isFetching
useQuery
의 isLoading
과는 다르게 isFetching
을 통해서 로딩중임을 확인할 수 있습니다.
data
data
는 각각의 페이지별 데이터가 리스트 형태입니다. 그리고 페이지별 데이터는 두 번째 인수였던 콜백의 반환값이 들어가게 됩니다.
위의 콜백을 사용하였다면 data
는 다음과 같습니다.
const data = {
pages: [
{
result,
nextPage,
isLast,
},
{
result,
nextPage,
isLast,
},
// ...
]
}
Intersection Observer API 는 기본 JS 스펙에 포함되어있는 API입니다.
리엑트에서 Intersection Observer API를 사용하기 위해서는 React Intersection Observer
라이브러리를 활용할 수 있습니다.
라이브러리 문서에 따르면 다음과 같이 useInView
Hook을 사용할 수 있습니다.
import React from 'react';
import { useInView } from 'react-intersection-observer';
const Component = () => {
const [ref, inView] = useInView();
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};
ref
관찰할 요소에게 ref 를 전달하면 됩니다.
inView
boolean 값으로, 관찰을 진행하는 요소가 viewport 상에서 나타나면 true
가 반환되며 그 외에는 false
가 반환됩니다.
위의 두 개의 기술을 사용하여 무한 스크롤을 구현하였습니다.
반환값중 관찰할 컴포넌트(ObservationComponent
)를 컨텐츠의 가장 하단에 추가하면 정상적으로 동작합니다.
ObservationComponent
가 관찰되면, inView
값이 변경될 것이므로 useEffect
를 사용하여 fetch 함수를 호출하였습니다.
import { ReactElement, useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { useInfiniteQuery } from 'react-query';
import { AnyObject } from 'immer/dist/internal';
import { DEFAULT_SEARCH_QUANTITY, SearchType } from '@type/web/event';
import { getEventList } from '@api/event/event';
interface UseInfiniteQueryWithScrollParamsTypes {
currentSearchType: SearchType;
queryString: AnyObject;
}
interface UseInfiniteQueryWithScrollReturnTypes {
data: any[] | undefined;
error: string | undefined | unknown;
isFetching: boolean;
ObservationComponent: () => ReactElement;
}
/**
* 사용 기술
* Recat query: useInfiniteQuery (https://react-query.tanstack.com/guides/infinite-queries)
* react-intersection-observer: useInView
*/
export default function useInfiniteQueryWithScroll({
currentSearchType,
queryString,
}: UseInfiniteQueryWithScrollParamsTypes): UseInfiniteQueryWithScrollReturnTypes {
const getEventListWithPageInfo = async ({ pageParam = 0 }) => {
const { data } = await getEventList({
...queryString,
eventType: currentSearchType,
searchType: currentSearchType,
startIndex: pageParam,
});
const nextPage =
data.length >= DEFAULT_SEARCH_QUANTITY ? pageParam + 1 : undefined;
return {
result: data,
nextPage,
isLast: !nextPage,
};
};
const { data, error, isFetching, fetchNextPage } = useInfiniteQuery(
[`eventListData-${currentSearchType}`],
getEventListWithPageInfo,
{
getNextPageParam: (lastPage) => lastPage.nextPage,
},
);
const ObservationComponent = (): ReactElement => {
const [ref, inView] = useInView();
useEffect(() => {
if (!data) return;
const pageLastIdx = data.pages.length - 1;
const isLast = data?.pages[pageLastIdx].isLast;
if (!isLast && inView) fetchNextPage();
}, [inView]);
return <div ref={ref} />;
};
return {
data: data?.pages,
error,
isFetching,
ObservationComponent,
};
}
안녕하세요. isFetching은 언제 쓰고, isFetchingNextPage는 언제 쓰는 건가요?