본문 바로가기

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

쿼리 특성 2 : select 옵션을 통한 데이터 filtering과 refetching & polling

반응형

 useQuery의 select 옵션으로 데이터 필터링하기

 

useQuery 결과에 대하여 select 옵션을 사용하여 결과를 필터링 할 수 있다. 리액트 쿼리는 불필요한 연산을 줄이는 최적화를 하고 이를 메모이제이션이라고 한다. 리액트 쿼리는 select 함수를 삼중 등호로 비교하여 데이터와 함수가 모두 변경되었을 경우에만 실행된다. 마지막으로 검색한 데이터와 동일한 데이터이고 select 함수에도 변동이 없으면 실행되지 않는다.  따라서 리액트 쿼리가 제공하는 캐시 기능을 잘 활용하기 위해 select 함수에는 안정적인 함수가 필요하고, 이때 익명 함수를 안정적인 함수로 만들기 위해 useCallback 을 사용한다.

useQuery({
	select: (data) => filteredData;
});

예시 코드를 보자. 캘린더 컴포넌트에서 예약 가능한 데이터만 보여주는 로직이다.

// useAppointments.ts

const { user } = useUser();
const selectFn = useCallback(
    (data) => getAvailableAppointments(data, user),
    [user],
  );

  // useQuery call for appointments for the current monthYear
  const queryClient = useQueryClient();

  useEffect(() => {
    const nextmonthYear = getNewMonthYear(monthYear, 1);
    queryClient.prefetchQuery({
      queryKey: [
        queryKeys.appointments,
        nextmonthYear.year,
        nextmonthYear.month,
      ],
      queryFn: () => getAppointments(nextmonthYear.year, nextmonthYear.month),
      commonFetchOption,
    });
  }, [queryClient, monthYear]);


  const { data: appointments = [] } = useQuery({
    queryKey: [queryKeys.appointments, monthYear.year, monthYear.month],
    queryFn: () => getAppointments(monthYear.year, monthYear.month),
    // showAll 이면 모든 예약을 보여주기 위해 undefined로 설정한다 => 옵션으로 include 되지도 않게 하기 위해
    select: showAll ? undefined : selectFn,
  });

다른 예시로 스태프의 목록을 filtering 하는 코드를 보자.

export function useStaff(): UseStaff {
  // for filtering staff by treatment
  const [filter, setFilter] = useState('all');

  const selectFn = useCallback(
    (data) => filterByTreatment(data, filter),
    [filter],
  );

  // TODO: get data from server via useQuery
  const { data: staff = [] } = useQuery({
    queryKey: [queryKeys.staff],
    queryFn: getStaff,
    select: filter === 'all' ? undefined : selectFn,
  });

  return { staff, filter, setFilter };
}

 

refetching & polling

 

백그라운드 리패칭은 만료 데이터가 서버로부터 업데이트 된 데이터로 바뀐다는 것을 보장한다. 리패칭은 페이지를 벗어났다가 다시 돌아올 때 발생한다.

stale 상태의 쿼리는 특정 조건 하에서 백그라운드에서 자동으로 리패치된다.

  • 새로운 쿼리 인스턴스가 생김
  • 리액트 컴포넌트가 mount
  • 윈도우 리포커스
  • 네트워크가 다시 연결
  • refetchInterval이 expired 될 때 = 사용자 조치 없어도 자동 업데이트

리액트 쿼리는 리패치에 매우 강하다. 리액트 쿼리가 제공하는 메서드는 다음과 같다. 

  • refetchOnMount
  • refetchOnWindowForcus
  • refetchOnReconnect
  • refetchInterval

변동이 적은 데이터에는 리패칭을 제한하고 싶을 수도 있다. 리패칭을 제한하면 네트워크 호출을 줄일 수 있다. 방법은 다음과 같다.

  • stale time을 증가한다.
  • refetchOnMount, refetchOnWindowForcus, refetchOnReconnect 을 끈다.

 

전역 refetch

전역 refetch 설정은 그 설정이 useQuery와 prefetch 쿼리의 기본 설정이 된다는 뜻이다. 이를 각각의 쿼리 옵션으로 오버라이딩 할 수도 있다(refetch가 활발해야 하는 쿼리도 있을 수 있기 때문). 

예시 코드로 네트워크 요청에 보수적인 전역 refetch 설정을 해보자. 

import { createStandaloneToast } from '@chakra-ui/react';
import { QueryClient, QueryClientConfig } from '@tanstack/react-query';

import { theme } from '../theme';

const toast = createStandaloneToast({ theme });

function queryErrorHandler(error: unknown): void {
  // error is type unknown because in js, anything can be an error (e.g. throw(5))
  const title =
    error instanceof Error ? error.message : 'error connecting to server';

  // prevent duplicate toasts
  toast.closeAll();
  toast({ title, status: 'error', variant: 'subtle', isClosable: true });
}

export const queryClient = new QueryClient ({
    defaultOptions: {
      queries: {
        onError: queryErrorHandler,
        staleTime: 1000 * 60 * 10, // 10 Min
        cacheTime: 1000 * 60 * 15, // 15 Min
        refetchOnMount: false,
        refetchOnWindowFocus: false,
        refetchOnReconnect: false,
      },
    },
  });

  return new QueryClient(queryClientOption);
}

 

이제는 appoinments 를 위한 refetch 설정을 해보자. appoinments 데이터는 실시간 업데이트가 중요하다.

import { useQuery, useQueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useState,
} from 'react';

import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
import { useUser } from '../../user/hooks/useUser';
import { AppointmentDateMap } from '../types';
import { getAvailableAppointments } from '../utils';
import { getMonthYearDetails, getNewMonthYear, MonthYear } from './monthYear';

// for useQuery call
async function getAppointments(
  year: string,
  month: string,
): Promise<AppointmentDateMap> {
  const { data } = await axiosInstance.get(`/appointments/${year}/${month}`);
  return data;
}

function identity<T>(value: T): T {
  return value;
}

// types for hook return object
interface UseAppointments {
  appointments: AppointmentDateMap;
  monthYear: MonthYear;
  updateMonthYear: (monthIncrement: number) => void;
  showAll: boolean;
  setShowAll: Dispatch<SetStateAction<boolean>>;
}

const commonOption = {
  staleTime: 0, // 10 Min
  cacheTime: 1000 * 60 * 5, // 15 Min
};

export function useAppointments(): UseAppointments {
  ...
  const queryClient = useQueryClient();

  useEffect(() => {
    const nextmonthYear = getNewMonthYear(monthYear, 1);
    queryClient.prefetchQuery({
      queryKey: [
        queryKeys.appointments,
        nextmonthYear.year,
        nextmonthYear.month,
      ],
      queryFn: () => getAppointments(nextmonthYear.year, nextmonthYear.month),
      commonOption,
    });
  }, [queryClient, monthYear]);

  const { data: appointments = [] } = useQuery({
    queryKey: [queryKeys.appointments, monthYear.year, monthYear.month],
    queryFn: () => getAppointments(monthYear.year, monthYear.month),
    select: showAll ? undefined : selectFn,
    ...commonOption,
    refetchOnMount: true,
    refetchOnWindowFocus: true,
    refetchOnReconnect: true,
  });

  return {
    appointments,
    monthYear,
    updateMonthYear,
    showAll,
    setShowAll,
  };
}

 

polling

이제 refetchInterval 옵션을 적용해보자. 이렇게 하면 1분 주기로 폴링 되며 새로운 데이터를 볼 수 있다.

  const { data: appointments = [] } = useQuery({
    queryKey: [queryKeys.appointments, monthYear.year, monthYear.month],
    queryFn: () => getAppointments(monthYear.year, monthYear.month),
    select: showAll ? (data) => identity<AppointmentDateMap>(data) : selectFn,
    ...commonFetchOption,
    refetchOnMount: true,
    refetchOnWindowFocus: true,
    refetchOnReconnect: true,
    refetchInterval: 1000 * 60 * 1, // 1 Min
  });

 

반응형