[PPLOG] Tanstack Query + Intersection Observer를 사용해 무한 스크롤 구현하기!

김현중·2024년 11월 23일

PPLOG

목록 보기
8/14
post-thumbnail

🥩Tanstack Query의 UseInfiniteQuery

Tanstack Query의 useInfiniteQuery는 페이지네이션된 데이터를 효율적으로 관리할 수 있게 해주는 훅입니다. useQuery와 달리 useInfiniteQuery는 여러 페이지의 데이터를 누적하여 관리할 수 있게 해줍니다.

useCustomQuery

먼저 커스텀으로 제작한 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,
  });
}
  1. 기본 옵션 설정(defaultQueryOptions)
  • refetchOnMount: false: 컴포넌트가 마운트될 때마다 데이터를 다시 가져오지 않습니다.
  • refetchOnWindowFocus: false: 윈도우가 포커스를 받을 때마다 데이터를 다시 가져오지 않습니다.
    이러한 설정은 불필요한 API 호출을 줄여 성능을 개선합니다.
  1. 타입스크립트의 타입 안정성 보장

  2. 제네릭 타입 <T>를 사용하여 다양한 데이터 타입 지원


useCustomInfiniteQuery

그리고 무한스크롤용으로 만든 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 훅은 다음과 같은 특징을 가집니다.

  1. 페이지네이션된 데이터의 자동 누적
    • 여러 페이지의 데이터를 자동으로 배열 형태로 관리
    • 페이지 상태 관리 자동화
  2. 에러 처리와 타입 안정성
    • Error 타입을 제네릭으로 지정하여 타입 안전성 보장
    • API 요청 실패 시 자동 에러 처리

useCustomQuery vs useInfiniteQuery

두 훅은 다음과 같은 차이점이 있습니다.

  • 데이터 구조: useQuery는 단일 데이터, useInfiniteQuery는 pages 배열 구조
  • 페이지 관리: useInfiniteQuery는 pageParam을 자동으로 관리
  • 데이터 누적: useInfiniteQuery는 이전 데이터를 유지하며 새 데이터를 누적

실제 페이지에서 구현하기

getNextPageParam 함수

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;
      ...
    }
  }
}, [의존성배열]);

해당 로직은 다음과 같은 특징을 가집니다.

  1. 효율적인 감지
    • Intersection Observer가 요소의 가시성을 효율적으로 감지합니다.
    • 스크롤 이벤트와 달리 성능 오버헤드가 적습니다.
    • 비동기적으로 작동하여 메인 스레드 블로킹을 방지합니다.
  2. 중복 요청 방지
    • hasNextPageisFetchingNextPage 플래그를 통해 중복 요청을 방지합니다.
    • 데이터 페칭의 상태를 정확하게 관리합니다.
    • 불필요한 API 호출을 최소화하여 서버 부하를 줄입니다.


이렇게 Intersection ObserveruseCustomInfiniteQuery를 사용해 간편하게 무한스크롤을 구현할 수 있습니다.

다시 한 번 프로젝트에서 사용한 로직을 간단하게 설명하면 다음과 같습니다.

  1. 초기 데이터 로드 ➡️ 첫 10개 데이터 + lastId 반환
  2. 사용자의 스크롤이 ref에 도달하면 Intersection Observer 감지 ➡️ ref 요소 가시성 확인
  3. lastId를 담아서 함께 새로운 요청 ➡️ 요청에 대한 값을 반환 받음
  4. 반환 받은 lastId가 -1이라면 마지막 데이터로 간주하여 더이상 요청을 보내지 않음
profile
진짜 성실한 사람

0개의 댓글