[React Query] Section6 - Query Features 2: Transforming and Re-Fetching Data

이해용·2022년 9월 2일
0
post-thumbnail

※ 본 강의에서의 react query는 3버전으로 4버전과는 버전 차이가 있어 코드가 다를 수 있습니다.

Transforming Data and Re-fetch

Filtering with the select option

  • Allow user to filter out any appointments that aren’t available
  • Why is the select option the best way to do this?
    • React Query memo-izes to reduce unnecessary computation (React Query는 불필요한 연산을 줄이는 최적화를 하는데 이를 메모이제이션(memoization)이라고 합니다.)
    • tech details:
      • triple equals comparison of select function (React Query는 셀렉트 함수를 삼중 등호로 비교하며)
      • only runs if data changes and the function has changed (셀렉트 함수는 데이터와 함수가 모두 변경되었을 경우에만 실행됩니다. 마지막으로 검색한 데이터와 동일한 데이터이고 셀렉트 함수에도 변동이 없으면 셀렉트 함수를 재실행하지 않는 것이 React Query의 최적화입니다.)
    • need a stable function (useCallback for anonymous function) (따라서 셀렉트 함수에는 안정적인 함수가 필요합니다. 매번 바뀌는 익명 함수, 즉 삼중 등호로 비교하는 함수는 실패합니다. 익명함수를 안정적인 함수로 만들고 싶을 때는 React의 useCallback함수를 사용하면 됩니다.)
    • reference: https://tkdodo.eu/blog/react-query-data-transformations

useQueryselect 옵션을 사용하면 쿼리 함수가 반환하는 데이터를 변환할 수 있습니다.

// useAppointments.ts

...
interface UseAppointments {
  appointments: AppointmentDateMap;
  monthYear: MonthYear;
  updateMonthYear: (monthIncrement: number) => void;
  showAll: boolean; // calendar.tsx 파일에서 showAll 이 boolean으로 나타낼 수 있도록 설정합니다.
  setShowAll: Dispatch<SetStateAction<boolean>>;
}
...

const [showAll, setShowAll] = useState(false); // showAll의 useState

  const { user } = useUser();

  const selectFn = useCallback((data) => getAvailableAppointments(data, user), [
    user, // 로그인하는 사용자가 바뀌거나 사용자가 로그아웃할 때마다 이 함수를 변경해야 합니다.
  ]);
	// 익명함수를 안정적인 함수로 만들기 위해 useCallback 함수를 사용합니다.
...

const { data: appointments = fallback } = useQuery(
    [queryKeys.appointments, monthYear.year, monthYear.month],
    () => getAppointments(monthYear.year, monthYear.month),
    {
      select: showAll ? undefined : selectFn, // 기존 useQuery함수에서 select option을 
    }, // 추가 하여 showAll이 참이면 undefined가 나타나고 거짓이면 selectFn 함수가 나타나게 합니다.
  );

...

select option

staff 페이지의 라디오 버튼 활성화

// useStaff.ts 변경 전
...

export function useStaff(): UseStaff {
  const [filter, setFilter] = useState('all'); // all이 기본으로 설정되고 이 상태의 현재 값고 setter 는 이 훅을 실행하는 이라면 누구에게나 공개 되어 있습니다.
// 그렇게 해야 이 값에 접근하고 업데이트 할 수 있습니다.
  const fallback = [];
  const { data: staff = fallback } = useQuery(queryKeys.staff, getStaff);

  return { staff, filter, setFilter };
}

// AllStaff.tsx

...
const { staff, filter, setFilter } = useStaff();

...


// utils.ts

import type { Staff } from '../../../../shared/types';

export function filterByTreatment(
  staff: Staff[], // 거르지 않은 staff 데이터
  treatmentName: string, // & 서비스명
): Staff[] {
  return staff.filter((person) =>
    person.treatmentNames
      .map((t) => t.toLowerCase())
      .includes(treatmentName.toLowerCase()),
  );
}
// 내가 시도 한 코드

export function useStaff(): UseStaff {
  const [filter, setFilter] = useState('all');

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

  const fallback = [];
  const { data: staff = fallback } = useQuery(
    [queryKeys.staff, filter],
    getStaff,
    {
      select: filterFn,
    },
  );

  return { staff, filter, setFilter };
}


// 실제 코드

export function useStaff(): UseStaff {
  const [filter, setFilter] = useState('all');

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

  const fallback = [];
  const { data: staff = fallback } = useQuery(queryKeys.staff, getStaff, {
    select: filter !== 'all' ? selectFn : undefined,
  });

  return { staff, filter, setFilter };
}
  • useStaff 훅에서는 selectFn에 unfilteredStaff를 가져와서 filterByTreatment로 전달하라고 정의를 내릴 겁니다.

  • useCallback으로 불러온 익명함수는 안정적이고 재처리가 필요한 데이터가 있는지를 검토하는 React Query의 3중 동등 검사를 통과할 것입니다.

  • useCallback을 위한 의존성 배열은 filter 상태 값이 될 것입니다.

  • useQuery를 위한 select 옵션은 all 인지 여부와는 무관하게 filter에 의존합니다. filter가 all이라고 설정되면 filter 함수가 필요 없고 여과되지 않은 데이터를 얻게 됩니다.

  • filter가 all이 아닌 경우에는 데이터를 거를 겁니다.

staff 페이지의 라디오 버튼 활성화

느낀점

실제 코드에서 filterFn = selectFn 은 같게 만들었으나 select 옵션의 설정 값이 달랐다. 내가 직접 실했을 때 all 버튼을 눌렀을 경우 모든 스태프가 나오지 않아서 고민을 했었는데 실제 코드의

select: filter !== 'all' ? selectFn : undefined,

로 설정 해주니 all 라디오 버튼을 클릭했을 때 모든 스태프가 나오게 되었다. 내가 만든 프로젝트가 아니다보니 다른 컴포넌트의 코드 이해가 부족해서 틀리지 않았나 싶다.

Re-fetching

What? When?

  • Re-fetch ensures stale data gets updated from server (서버가 만료 데이터를 업데이트 합니다. 일정 시간이 지나면 서버가 만료된 데이터를 삭제하는데 개발자의 의지와는 상관이 없습니다.)
    • Seen when we leave the page and refocus (이런 리페칭은 페이지를 벗어났다가 다시 돌아왔을 때 볼 수 있습니다.)
  • Stale queries are re-fetched automatically in the background when: (stale 쿼리는 어떤 조건 하에서 자동적으로 다시 가져오기가 됩니다.)
    • New instaces of the query mount (새로운 쿼리 인스턴스가 많아지거나 쿼리 키가 처음 호출된다거나)
    • Every time a react component (that has a useQuery call) mounts (쿼리를 호출하는 반응 컴포넌트를 증가시킨다거나)
    • The window is refocused (창을 재포커스 한다거나)
    • The network is reconnected (만료된 데이터의 업데이트 여부를 확인할 수 있는 네트워크가 다시 연결된 경우에 리페칭이 일어납니다.)
    • configured refetchInterval has expired (리페칭 간격이 지난 경우도 해당되는데 )
      • Automatic polling (이 경우는 간격에 리페칭을 해서 서버를 풀링하고 사용자 조치가 없더라도 데이터가 업데이트 되는 경우입니다.)

How?

  • Control with global or query-specific options: (옵션으로 제어를 할 수 있는데 일반적인 경우인 전역일 수도 있고 호출쿼리 사용에 특정된 것일 수도 있습니다.)
    • refetchOnMount, refetchOnWindowFocus, refetchOnReconnect, refetchInterval (앞의 3개는 boolean, 마지막은 밀리초 단위의 시간)
  • Or, imperatively: refetch function in useQuery return object (리페칭을 명령할 수도 있어서 useQuery를 쓰면 객체를 반환합니다. 데이터나 오류 같은 것인데 refetch 함수를 반환하기도 합니다. 불러오려는 데이터가 있을 때 호출하는 방법입니다.)
  • reference: https://tanstack.com/query/v4/docs/guides/important-defaults?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/important-defaults

Suppressing Re-Fetch (리페치 제한)

  • How
    • Increase stale time
    • turn off refetchOnMount / refetchOnWindowFocus / refetchOnReconnect
  • Only for very rarely changed, not mission-critical data (리페칭을 제한할 때는 신중해야 합니다. 변동이 잦지 않은 데이터에 적용해야 하며 미세한 변동에도 큰 변화를 불러오는 데이터에는 적용하지 말아야 합니다.)
    • treatments or staff (definitely not appointments!)

리페치(Re-fetch) 옵션 업데이트

// 기존 코드

...

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

export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();
  queryClient.prefetchQuery(queryKeys.treatments, getTreatments);
}

기존 코드 적용

  • 기존 코드는 useQuery에 대한 옵션이 없어 다른 메뉴로 넘어갔을 때마다 업데이트가 진행됩니다.
// useQuery option 적용 코드

...
export function useTreatments(): Treatment[] {
  const fallback = [];
  const { data = fallback } = useQuery(queryKeys.treatments, getTreatments, {
    staleTime: 600000, // 10minutes
    cacheTime: 900000, // 15minutes (doesn't make sense for staleTime to exceed cacheTime)
    refetchOnMount: false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
  });
  return data;
}
...

useQuery option 코드 적용

  • 기존 코드에서 useQuery에 옵션을 추가하여 staleTime은 10분, cacheTime은 15분, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false 로 설정하여 Treatment와 Staff의 화면 전환이 되어 업데이트 시간이 변경되지 않습니다. 설정한 10분이 지나야 자동으로 업데이트 됩니다.
// prefetchQuery option 적용 코드

...
export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();
  queryClient.prefetchQuery(queryKeys.treatments, getTreatments, {
    staleTime: 600000, // 10minutes
    cacheTime: 900000, // 15minutes (doesn't make sense for staleTime to exceed cacheTime)
  });
}
...

prefetchQuery option 코드 적용

  • prefetchQuery에 staleTIme, cacheTime의 옵션을 설정하여 메인 홈페이지를 클릭해도 업데이트된 시간은 설정된 10분내라면 변경되지 않습니다.

Update Global Settings 전역 리페치(Re-fetch) 옵션

  • Global default options vs individual query options
  • Here, want settings for everything but appointments
    • User profile and user appointments invalidated after mutations (사용자가 프로필을 업데이트 하면 프로필 정보가 예약을 하게 되면 예약 정보가 변경될 것입니다. 이 2가지를 다른 리페칭 방식으로 처리 할 겁니다. 변이(mutations)를 만들어 데이터를 무효화시키면 리페칭이 됩니다.)
    • Appointments get special settings (including auto-refetching on interval) (appointments 는 기본 값으로 오버라이드하는 설정이 들어갈 겁니다. 만료 시간이 0이 되고 캐시시간은 제한됩니다. 폴링 간격도 설정해서 주기적으로 데이터를 서버에서 불러올 것입니다.)
// queryClient.ts

...

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: queryErrorHandler,
      staleTime: 600000, // 10 minutes
      cacheTime: 900000, // 15 minutes,
      refetchOnMount: false,
      refetchOnReconnect: false,
      refetchOnWindowFocus: false,
    },
  },
});


// useTreatments.ts

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

export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();
  queryClient.prefetchQuery(queryKeys.treatments, getTreatments);
}

리페치(Re-fetch) 기본값 오버라이드와 폴링

treatment와 staff는 실시간으로 데이터가 업데이트 될 필요가 없어 queryClient에 옵션을 설정해주어도 상관 없으나 appointments는 실시간으로 정보가 업데이트 되는 것이 중요합니다. 그래야 사용자들이 예약을 할 수 있습니다.

게다가 appointments는 사용자 활동이 없을 때에도 서버에 변경이 이루어져야 했습니다.

// useAppointments.ts

...

const fallback = {};

  const { data: appointments = fallback } = useQuery(
    [queryKeys.appointments, monthYear.year, monthYear.month],
    () => getAppointments(monthYear.year, monthYear.month),
    {
      select: showAll ? undefined : selectFn,
      staleTime: 0,
      cacheTime: 3000000, // 5 minutes,
      refetchOnMount: true,
      refetchOnReconnect: true,
      refetchOnWindowFocus: true,
    },
  );

...

위처럼 적용하면 리페칭은 프리페칭에 적용되지 않지만 staleTime과 cacheTime은 프리페칭에 적용 됩니다. 이 부분을 따로 분리해서 새로운 commonsOptions 함수를 만들어 prefetch에 적용합니다.

... 
const commonOptions = { // => 에러 발생
    staleTime: 0,
    cacheTime: 300000, // 5 minutes,
  };
...

const queryClient = useQueryClient();
  useEffect(() => {
    const nextMonthYear = getNewMonthYear(monthYear, 1);
    queryClient.prefetchQuery(
      [queryKeys.appointments, nextMonthYear.year, nextMonthYear.month],
      () => getAppointments(nextMonthYear.year, nextMonthYear.month),
      commonOptions,
    );
  }, [queryClient, monthYear, commonOptions]);

...

const fallback = {};

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

...

함수안에 적용하면 commonOptions 함수에 에러가 발생하여 함수 바깥에서 선언해주고 useEffect의 의존성 배열에서 제거 합니다.

... 
const commonOptions = {
    staleTime: 0,
    cacheTime: 300000, // 5 minutes,
  };
...

export function useAppointments(): UseAppointments {

...

const queryClient = useQueryClient();
  useEffect(() => {
    const nextMonthYear = getNewMonthYear(monthYear, 1);
    queryClient.prefetchQuery(
      [queryKeys.appointments, nextMonthYear.year, nextMonthYear.month],
      () => getAppointments(nextMonthYear.year, nextMonthYear.month),
      commonOptions,
    );
  }, [queryClient, monthYear]);

...

const fallback = {};

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

...

Polling / Auto Re-Fetching 폴링: 간격에 따른 자동 리페칭(Re-fetching)

  • Appointments is the opposite of treatments and staff
    • We want it to be updated even if the user doen’t take action
    • Appointments may change on server, want to keep up-to-date
  • Overrode defaults for staleTime, cacheTime, refetchOn*
  • Use refetchInterval option to useQuery
  • What about userAppointments?
    • Can this go with the defaults?
    • Yes, because it will never be updated “from underneath us”
    • Client will know if there are any changes to logged-in user appointments
    • It’s only other appointments that might change on the server
// useAppointments.ts

...
const fallback = {};

  const { data: appointments = fallback } = useQuery(
    [queryKeys.appointments, monthYear.year, monthYear.month],
    () => getAppointments(monthYear.year, monthYear.month),
    {
      select: showAll ? undefined : selectFn,
      ...commonOptions,
      refetchOnMount: true,
      refetchOnReconnect: true,
      refetchOnWindowFocus: true,
      refetchInterval: 1000, // every second; not recommended for production
    },
  );
...

refetchInterval

Summary

  • select option for filtering
    • stable function to take advantage of caching (어떻게 stable function이 되는지에 대해 알아봤으며 삼중 등호 테스트에서 살아남은 함수가 되야합니다. React Query의 캐싱을 이용하려면 데이터를 리필터링 해서는 안됩니다. 또한, React useCallback으로 함수의 안정성을 확인했습니다.)
  • Suppressing re-fetch with options (리페칭 옵션을 살펴보고 이들을 제한하기도 했습니다. 또한 이 옵션을 특정 쿼리의 사용과 프리페칭 쿼리 호출 시 전역 옵션에 추가하고 오버라이드 했습니다.)
  • Polling / re-fetching at intervals (서버에서 변경이 발생할 경우 특정 간격으로 데이터를 다시 불러오게끔 폴링을 했습니다.)

reference
https://www.udemy.com/course/learn-react-query/
https://tkdodo.eu/blog/react-query-data-transformations
https://tanstack.com/query/v4/docs/guides/important-defaults?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/important-defaults

profile
프론트엔드 개발자입니다.

0개의 댓글