사용자가 스크롤할 때마다 추가 데이터를 자동으로 불러와 보여주는 기법이다. 지금부터 이 방식을 TanStack Query(React Query)의 useInfiniteQuery를 사용해 Supabase에서 채용 공고 데이터를 페이지네이션 방식으로 가져오고, 이를 무한 스크롤 UI로 구현하는 방법에 대해 적겠다.
Supabase : 데이터베이스에서 데이터를 가져오기 위한 클라이언트
TanStack Query : 데이터 페칭 및 캐싱을 위한 라이브러리
React: UI 컴포넌트를 작성하기 위한 프레임워크
무한 스크롤을 구현하려면 서버(API)가 페이지네이션을 지원해야 한다. 즉, 한 번에 일정 수의 데이터를 반환하도록 설정해야 된다.
export const fetchJobsData = async (table1) => {
try {
const { data } = await supabase
.from(table1)
.select('*, resumes(*), bookmarks(*)');
return data || [];
} catch (error) {
console.error('fetching error', error);
}
};
모든 데이터를 한 번에 받아오면 초기 로딩 시간이 길어짐
export const fetchJobsInfinite = async ({ startPageParam = 0 }) => { //시작인덱스 (기본값 0)
const limit = PAGE_SIZE; //페이지당 가져올 항목 수
const endPageParam = startPageParam + limit - 1; //마지막 인덱스 ex)limit= 10 이라면 0+10-1
const { data, error } = await supabase
.from('jobs')
.select('*, resumes(*), bookmarks(*)')
.range(startPageParam, endPageParam); //범위 정해주기(0, 9)
if (error) throw error;
// nextPage : 현재 페이지의 데이터길이가 limit이랑 같아진다면 다음 페이지의 시작 인덱스를 반환
const nextPage = data.length === limit ? startPageParam + limit : undefined;
return { data, nextPage }; //data는 현재 가져온 데이터, nextPage는 다음 요청시 사용할 인덱스
};
export const useInfiniteJobsQuery = () => {
return useInfiniteQuery({
queryKey: [QUERY_KEY.JOBSINFINITE], // 캐시와 무효화를 관리하는 키
queryFn: ({ pageParam = 0 }) => // 시작 인덱스 pageParam = 0 (Defalt = 0)
//tanstack query는 pageParam이라는 이름으로 전달
fetchJobsInfinite({ startPageParam: pageParam }),
//마지막 페이지에 nextPage 값이 있으면 그 값을 반환
getNextPageParam: (lastPage) => lastPage.nextPage,
// 초기 데이터 구조 보장을 위해 initialData 설정 가능
//Ex) initialData: { pages: [{ data: [], nextPage: 0 }], pageParams: [0] },
});
};
const JobListPage = () => {
const {
data,
isPending,
isError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteJobsQuery();
if (isPending) return <LoadingPage state="load" />;
if (isError) return <LoadingPage state="error" />;
//data에는 {pages, pageParams} 형태
//{data: [...], nextPage:[number]} 형태의 배열로 전달 받기 때문에 평탄화로 하나의 배열로 만들기
const jobData = data.pages.map((page) => page.data).flat();
return (
<div className="mt-5 flex justify-center">
{hasNextPage ? ( //hasNextPage가 있을 때 버튼을 렌더링
<Button mode={BUTTON_MODE.S} onClick={() => fetchNextPage()}> //fetchNextPage로 다음 데이터 호출
{isFetchingNextPage ? '로딩중...' : '더 보기'}
</Button>
) : (
<div>더 이상 채용 공고가 없습니다.</div>
)}
</div>
);
};
[
{ data: [job1, job2, ..., job10], nextPage: 10 },
{ data: [job11, job12, ..., job20], nextPage: 20 },
...
]
이런식으로 중첩 배열 형태로 받기 때문에 평탄화를 해줘야 한다.