tanstackQuery를 활용한 무한스크롤 구현하기

최종욱·2025년 3월 4일

react

목록 보기
7/7

무한스크롤이란

사용자가 스크롤할 때마다 추가 데이터를 자동으로 불러와 보여주는 기법이다. 지금부터 이 방식을 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);
  }
};

모든 데이터를 한 번에 받아오면 초기 로딩 시간이 길어짐

무한 스크롤 적용 코드

API 함수 호출 만들기

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] },
  });	
};		

UI 컴포넌트 구현

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 },
  ...
]

이런식으로 중첩 배열 형태로 받기 때문에 평탄화를 해줘야 한다.

  • flatMap : 각 배열 요소에 함수를 적용하고, 그 결과를 한 단계 평탄화
  • map() +flat() : 각 페이지의 데이터를 map()으로 추출후 flat()으로 배열 풀기
profile
항상 “Why?”로 시작하는 프론트엔드 개발자

0개의 댓글