본문 바로가기

온라인 강의(유데미, 인프런 등)/React-Query(유데미)

쿼리 특성 1 : Pre-fetching과 페이지네이션

반응형

Pre-fetching(초기 데이터 채우기)

 

사용자에게 보여주고 싶은 데이터가 있는데 캐시에 아직 데이터가 없는 경우, 리액트 쿼리를 사용하여 새로운 데이터를 가져오기 전에 잠시 데이터를 미리 보여줄 수 있도록, 해당 데이터를 미리 가져와 캐시에 추가하는 방법에 대해 알아보자.

  • where to use? = 미리 채우기 옵션이 사용될 리액트 쿼리 메서드
  • data from? = 서버 또는 클라이언트
  • added to cache ? = 캐시 저장 여부
  where to use? data from? added to cache ?
prefetchQuery method to queryClient server yes
setQueryData method to queryClient client yes
placeholderData option to useQuery client no
initialData option to useQuery client yes
  • prefetchQuery에서는 서버로 이동하여 데이터를 캐시에 추가한다.
  • setQueryData는 클라이언트에서 데이터를 가져오기 때문에 서버에서 변이에 대한 응답으로 나온 데이터일 수 있다. queryClient에서 setQueryData 메소드를 사용하여 캐시에 데이터를 추가하면 useQuery가 데이터를 요청하면 캐시가 해당 데이터를 제공할 수 있다.
  • placeholerData는 useQuery를 실행할 때 데이터를 제공하기 때문에 클라이언트에서 가져오게 되고 캐시에 저장되지 않는다. placeholderData는 고정값 또는 함수를 사용할 수 있다. placeholderData 값을 동적으로 결정하는 함수를 사용하려는 경우 placeholderData를 사용하는 것이 가장 좋다. (이전 글의 fallback =[] 처럼). 달리 표시할 데이터가 없는 경우 사용하는 표시 데이터라고 생각하면 편하다.
  • initialData는 placeholderData와 달리 캐시에 추가한다. 이것이 쿼리에 대한 공식적인 데이터라고 기록해둔다.

 

이전에 사용자가 현재 페이지를 보는 동안 다음 페이지 버튼을 누르기 전에 다음 페이지를 미리 가져오는 페이지네이션에 대해 다뤄보았다.  여기서는 블로그 앱과는 다른 스타일의 예약 앱을 만든다고 가정해보자.

예를 들어, 사용자가 홈페이지를 보다가 특정 페이지로 이동한다는 확률이 높다는 결과가 있었다고 가정해자. 이런 경우 사용자가 그 특정 페이지로 이동할 때 필요한 데이터를 기다리지 않도록 미리 가져오는 것이 좋다.

주식 시세와 같이 동적인 데이터를 가져오는 것이 아니라면 사용자 경험을 위해 캐시 된 데이터를 잠깐 보여줘도 큰 문제는 없다. 캐시 시간 내에 useQuery로 데이터를 호출하지 않으면 가비지 컬렉션으로 수집되기 때문에  만약 사용자가 기본 캐시 시간, 즉 5분 이내에 그 페이지를 로드 하지 않는다면 cacheTime을 늘릴 수도 있다.

 

prefetchQuery에 대해 자세히 알아보자.

prefetchQuery는 queryClient의 메서드다. 따라서 useQuery와 달리 Client 캐시에 추가된다. useQuery는 페칭과 리페칭 등의 작업이 필요한 쿼리를 생성하지만 prefetchQuery은 일회성이다. prefetchQuery는 queryClient의 메서드이므로 queryClient를 반환해야 하며, 이를 위해 useQueryClient 훅을 사용한다. 

prefetchQuery의 flow를 살펴보자.

1. prefetch는 사용자가 홈페이지를 로드할 때 발생한다.

홈페이지를 로드할 때 queryClient.prefetchQuery를 호출하고, 이를 통해 원하는 데이터가 캐시에 추가된다. 

2. 그런 다음 사용자가 특정 컴포넌트 페이지를 로드한다.

쿼리를 처음 prefetch 한 이후로 부터 cahceTime이 초과되지 않은 경우 그 데이터가 캐시로부터 로드된다. 이때, 컴포넌트가 마운트하여 리패치가 트리거 되었기 때문에 useQuery는 데이터가 만료 상태라는 것을 알고 새로운 데이터를 가져온다. useQuery를 통해 새로운 데이터 가져와지는 동안에 이미 prefetch 하여 캐시에 있는 데이터가 잠시 잠깐 사용자에게 보여지는 것이다. 이것이 prefetch의 힘이다. 만약 캐시 시간이 지났다면 데이터는 가비지 콜렉션이 되었기 때문에 useQuery가 새로운 데이터를 가져오는 수 밖에 없다.

 

예시 코드를 살펴보자.

//useTreatments.ts
import { useQuery, useQueryClient } from 'react-query';

import type { Treatment } from '../../../../../shared/types';
import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';

// for when we need a query function for useQuery
async function getTreatments(): Promise<Treatment[]> {
  const { data } = await axiosInstance.get('/treatments');
  return data;
}

export function useTreatments(): Treatment[] {
  const fallback = []; 
  
  const { data = fallback } = useQuery(queryKeys.treatments, getTreatments);
  return data;
}

// Home 컴포넌트에서 Treatments 데이터를 프리페칭(prefetchQuery)하기 위한 훅
export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();

  // 쿼리 키는 어떤 useQuery가 이 데이터를 찾아야하는지 알려준다.
  // 캐시에 있는 데이터가 이 useQuery 호출과 일치한다는 것을 말해준다.
  queryClient.prefetchQuery(queryKeys.treatments, getTreatments);
}
import { Icon, Stack, Text } from '@chakra-ui/react';

import { usePrefetchTreatments } from 'components/treatments/hooks/useTreatments';
import { ReactElement } from 'react';
import { GiFlowerPot } from 'react-icons/gi';

import { BackgroundImage } from '../common/BackgroundImage';

export function Home(): ReactElement {

  // 가져온 훅을 컴포넌트의 최상단에서 실행
  // 이 컴포넌트의 렌더 부분에서 동적인 데이터가 거의 없다.
  // 만일 잦은 리렌더링이 걱정되면 stalTime과 cacheTime에 대한 옵션을 추가하여 
  // 모든 트리거마다 리페칭되지 않도록 해줄 수 있다.
  usePrefetchTreatments();

  return (
    <Stack textAlign="center" justify="center" height="84vh">
      <BackgroundImage />
      <Text textAlign="center" fontFamily="Forum, sans-serif" fontSize="6em">
        <Icon m={4} verticalAlign="top" as={GiFlowerPot} />
        Lazy Days Spa
      </Text>
      <Text>Hours: limited</Text>
      <Text>Address: nearby</Text>
    </Stack>
  );
}

다른 예시를 살펴보자. 캘린더를 보여주며 각 달의 예약 정보를 보여주는 컴포넌트다.

아래의 코드는 useAppointments 훅에서 데이터를 가져오는 쿼리 문이다.

여기서 가장 중요한 부분은 쿼리 키이다. 만약 쿼리 키를 단순 queryKeys.appointments로 설정하면 매 달 같은 예약 데이터가 보여진다. 그래서 쿼리 키를 꼭 배열로 설정해주고, 이를 종속성 배열처럼 생각해야 한다. 즉, 데이터가 변경될 경우 키도 변경되도록 하여 새 쿼리를 만들고 새 데이터를 가져올 수 있도록 해야 한다.

여기서 queryKeys.appointments가 배열의 가장 맨 앞에 있는 이유는 데이터 무효화 때문이다. 데이터를 무효화 하려면 모든 배열에 공통점이 있어야 한다. 특히 공통 prefix(queryKeys.appointments)가 있으면 한 번에 모두 무효화할 수 있다.

// useAppointments.ts

const fallback = {}; // 빈 객체로 해당 월에 예약이 없다는 의미

  const { data: appointments = fallback } = useQuery(
    // queryKeys.appointments로 하면 매달 같은 예약 데이터가 보여지는 문제가 있다.
    [queryKeys.appointments, monthYear.year, monthYear.month],
    () => getAppointments(monthYear.year, monthYear.month),
    {
      // 쿼리키가 변경될 때까지 이전의 모든 데이터를 유지시켜주는 옵션, 다음 쿼리키에 대한 데이터를 로드하는 동안 플레이스홀더로 사용
      // 하지만 이렇게 하면 다음 달로 이동시 데이터가 겹치는 현상 발생(백그라운드가 변경되기 때문)
      // 따라서 여기서는 적합한 옵션이 아니다.
      // keepPreviousData: true,
    },
  );

 

페이지네이션

 

위 쿼리에서 keepPreviousData: true를 설정하면 다음 달로 이동하면 이전 달의 데이터가 겹치는 현상이 발생하기 때문에 사용자 경험을 향상 시키고자하는 목적으로 이 옵션을 사용할 수가 없다. 블로그 앱에서 페이지네이션을 적용할 때는 다음으로 넘어갈 때 이전의 데이터가 보여져도 상관없지만 달력에서는 이전의 데이터가 의미가 없기 때문에 다른 방법을 사용해야 한다.

여기에서는 prefetch를 사용하여 해결한다.

monthYear라는 현재 달력의 년도, 달을 가진 데이터가 바뀌면 getNewMonthYear를 통해 달의 상태 값을 현재 달에서 +1 된 다음 달로 업데이트한다. 이렇게 되면 5월의 데이터를 가져올 때 6월의 데이터까지 같이 가져와 사용자가 6월의 데이터를 보려고 다음 버튼을 눌렀을 때 백그라운드에서 새로운 데이터를 가져올 때까지 미리 prefetch된 데이터를 보여주어 단순히 로딩 인디케이터를 보여주는 것보다 좋은 사용자 경험을 만들 수 있다.

  import { useQuery, useQueryClient } from 'react-query';
  
  const queryClient = useQueryClient();
  
  const [monthYear, setMonthYear] = useState(currentMonthYear);

  useEffect(() => {
    // assume increment of one month
    const nextMonthYear = getNewMonthYear(monthYear, 1);
    queryClient.prefetchQuery(
      [queryKeys.appointments, nextMonthYear.year, nextMonthYear.month],
      () => getAppointments(nextMonthYear.year, nextMonthYear.month),
    );
  }, [queryClient, monthYear]);
반응형