React-Query) 다양한 기능 + 활용법 살펴보기

김명성·2022년 9월 9일
7
post-custom-banner

isFetching vs isLoading

두 메서드의 공통점: fetch 중인 request가 존재할 때에는 값이 true이다.

다만 isLoadingisFetching의 부분집합으로서 그 범위가 더 좁다.

isLoading이 true인 경우는
1. fetching중이면서
2. 현재 캐싱된 데이터가 없을 때
두 조건을 만족해야 true가 된다.

즉, prefetching으로 데이터를 미리 받아놓은 상황이라면 isLoading은 false값을 유지한다.
(캐싱데이터는 특별히 설정하지 않는 이상 5분간 유지된다.)

사용 예시

useQuery
react-query는 react의 hooks에 꽤 의존적이다.
currentPage가 바뀔 때마다(정확히는 useQuery의 key가 바뀔때마다) query를 재실행한다.

  const { data, isError, error, isLoading, isFetching } = useQuery(
    ['posts',currentPage],
    () => fetchPosts(currentPage),
    {
      staleTime: 1000,
    }
  );

prefetching을 통해 다음페이지를 caching 하므로 isLoading은 cached data가 gc에 의해 사라지기 전까지 계속 false 값을 유지할 수 있다.

Prefetching

  const queryClient = useQueryClient();

  useEffect(() => {
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['posts', nextPage], () =>
        fetchPosts(nextPage)
      );
    }
  }, [currentPage, queryClient]);

prefetching의 또 다른 사용 예시

export function usePrefetchPost(): void {
  const queryClient = useQueryClient();
  queryClient.prefetchQuery(queryKeys.post, getPosts);
}

prefetchQuery를 커스텀 훅으로 만들어서 해당 컴포넌트에 진입하기 이전에 먼저 필요한 데이터를 받아와 caching 할 수 있다.
즉, getPostshttp://localhost:3000/posts에 진입할 때 실행되는 요청이지만, http://localhost:3000에 진입할 때 /posts에 필요한 요청을 사전에(Pre) 진행하게 할 수 있다.

useIsFetching() / useIsMutating()

두 메서드는 현재 진행중인 request의 개수정수로 나타낸다.

const isFetching = useIsFetching();
const isMutating = useIsMutating();
const display = isFetching || isMutating ? 'inherit' : 'none';

할당한 display 변수의 값을 통해 로딩 스피너를 조정할 수 있다.

    <Spinner display={display} />

fallback

fallback을 설정하여 초기 data의 상태가 undefined로 인해 error가 발생되는 것'을 방지할 수 있다.

export function useTreatments(): Treatment[] {
  
  // 일일히 isLoading을 해줄 필요 없이 중앙화 할 수 있음.
  const fallback = [];
  const { data = fallback } = useQuery(queryKeys.something, getData, {
    onError: (error) => {
      const errorMessage = error instanceof Error
      ? error.message
      : 'error connecting to the server';
      toast(errorMessage)
    }
  });
  return data
}

defaultOptions

queries: onError

위 fallback에 작성한 onErroruseTreatments 내에서 작성했기 때문에 해당 훅에만 적용된다.
만약 여러개의 queries가 존재한다면 똑같은 onError를 작성해야 되는 부분이 많아지기에, 모든 쿼리에 적용할 수 있는 onError를 기본값으로 정의할 수 있다.

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: queryErrorHandler
    }
  }
});

queryClient의 onError를 queryErrorHandler를 기본 값으로 설정했다.
queryErrorHandler는 다음과 같다.

function queryErrorHandler(error: unknown): void {
  const id = 'react-query-error';
  const title = error instanceof Error
  ? error.message
  : 'error connecting to server';
  toast(`${id}: ${title}`);
}

mutations: onError

데이터를 변형할 때 사용하는 useMutation의 defaultOptions도 지정할 수 있다.

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

Select

useQuery를 통해 받아오는 data를, 원하는 형식에 맞게 transform 해주는 기능.
useQuery의 3번째 인자인 options 객체에 입력한다.

React-query의 최적화 (memoization)
Select function은 데이터뿐만 아니라 함수 모두가 변경되었을 경우에만 실행하게 만드는 함수이다.
select function이 변경되지 않는다면 useCallback을 사용하여 재실행하지 않게 만들어 최적화해야 하고, 값이 자주 변경되지 않는 stable function이 적합한다.

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 };
}

filterByTreatment 함수는 다음과 같다.

export function filterByTreatment(staff: Staff[],treatmentName: string): Staff[] {
  return staff.filter((person) =>
    person.treatmentNames
      .map((t) => t.toLowerCase())
      .includes(treatmentName.toLowerCase()),
  );
}

받아오는 데이터가 자주 변하는 데이터가 아니라면?

  • staletime을 늘려 데이터를 fresh 상태로 길게 유지하게 만들 수 있다.
  • cacheTime을 늘려 데이터가 GC에 처리되는 기간을 늘릴 수 있다.

또한 refetch가 trigger되는 조건들을 off 할 수 있다.
(refetchOnMount,refetchOnWindowFocus,refetchOnReconnect)

export function useTreatments(): Treatment[] {
  const fallback = [];
  const { data = fallback } = useQuery(queryKeys.treatments, getTreatments,{
    staleTime: 600000, // 10 minutes
    cacheTime: 3600000, // 1 hours
    refetchOnMount: false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
  })
  return data
}

refetchInterval

자주 변하는 데이터라면 주기적으로 refetching하여 데이터의 변화를 지속적으로 감시할 수 있다.

  const { data: appointments = fallback } = useQuery(
    [queryKeys.appointments,monthYear.year, monthYear.month],
    () => getAppointments(monthYear.year, monthYear.month,
      {
        refetchInterval: 60000 // every minute;
      }
  ))

setQueryData

쿼리 키와 값을 인수로 받아 존재하는 cached data에 해당 키에 대한 값을 updating 할 수 있게 한다.
setQueryDatafetchQuery의 차이점은, setQueryData는 동기화이며 이미 동기식으로 데이터를 사용할 수 있다고 가정한다.
데이터를 비동기적으로 가져와야 하는 경우 쿼리 키를 다시 가져오거나 fetchQuery를 사용하여 비동기로 처리하는 것이 좋다.

  function updateUser(newUser: User): void {
    queryclient.setQueryData(queryKeys.user, newUser)
  }

  
  function clearUser() {
   queryClient.setQueryData(queryKeys.user, null);
  }

onSuccess

queryFn을 실행하여 data를 받아오거나 ,setQueryData에서 data를 성공적으로 가져왔을 때 후속으로 실행하는 만드는 메서드이다.

만약 로그인 인증 queryFn,setQueryData라면, localStorage에 존재하는 데이터를 load하는 함수를 onSuccess의 콜백으로 입력하여 업데이트 할 수 있다.

  const {data: user} = useQuery(queryKeys.user, () => getUser(user),{
    onSuccess: (received: User | null) => {
      if(!received){
        clearStoredUser()
      }else{
        setStoredUser(received);
      }
    }
  })

initialData

초기 데이터를 cache에 추가하고 싶을 때 사용한다.
fallback과 다른점은 fallback은 placeholderData와 같이 실제 캐시에는 추가되지 않지만 initialData는 실제 cache에 추가된다.

  const {data: user} = useQuery(queryKeys.user, () => getUser(user),{
    initialData:getStoredUser()
  })

Dependent Queries(enabled)

enabled 프로퍼티에 boolean 값을 통해, 특정 값에 대한 결과를 의존하게 하여 쿼리를 활성화/비활성화 할 수 있게 만든다.


export function useUserAppointments(): Appointment[] {
  const {user} = useUser();
  const fallback:Appointment[] = [];

  const { data: userAppointments = fallback } = useQuery(
    "user-appointments",
    () => getUserAppointments(user),
    {
      enabled: !!user
    }
)
  
  return userAppointments
}

useMutation

useQuery와 유사하지만 몇가지 차이점이 존재한다.

  1. Fetcing,Refetching, updateData가 있는 useQuery와는 다르며 기본적으로 재시도가 없다.(useQuery는 기본적으로 3번 재시도한다.)

  2. 캐싱할 데이터가 존재하지 않으므로 isLoading과 isFetching이 구분되지 않는다.(정확히는 isLoading이 존재하지 않는다.)

  3. 캐싱할 데이터가 존재하지 않기 때문에 쿼리키를 필요로 하지 않는다.

  4. useMutationmutate 함수를 반환하는데 요청을 보내는데에 사용되며
    onMutate 콜백도 존재한다. 이것은 optimize query에 사용되며 변이가 실패할 때(http request가 실패할 때) 복원할 수 있도록 이전 상태를 저장하는데 사용할 수 있다.

useMutation 타입 정의하기

제네릭에 아래 타입을 순서대로 입력한다.
1. mutate function자체에서 반환되는 데이터타입, 없다면 void처리
2. Error의 타입. customError가 없다면 unknown
3. mutate function에서 사용하는 매개변수 타입
4. context의 타입으로서 optimize rollback을 위해 onMutate에 설정하는 타입

export function useReserveAppointment(): UseMutateFunction<void,unknown,Appointment,unknown> {
  const { user } = useUser();
  const toast = useCustomToast();
  const {mutate} = useMutation((appointment:Appointment) => setAppointmentUser(appointment,user?.id))

  return mutate
}

invalidateQueries

캐싱된 데이터를 오래된 데이터로 취급변경하고, 재요청을 촉발시킨다.
일반적으로 mutate를 호출하면 mutation에 작성한 onSuccess를 통해 관련 커리를 무효화하고 해당 데이터를 재요청하는 방식으로 사용한다.

invalidateQueries는 정확한 keyName이 아니라 prefix를 통해
모든 queries를 무효화 하는데, 정확한 키로 설정하고 싶다면
exact:true로 설정하면 된다.

export function useReserveAppointment(): UseMutateFunction<void,unknown,Appointment,unknown> {
  const { user } = useUser();
  const queryClient = useQueryClient();
  const {mutate} = useMutation((appointment:Appointment) => setAppointmentUser(appointment,user?.id),{
    onSuccess: () => {
      queryClient.invalidateQueries([queryKeys.appointments])
    }
  })

  return mutate
}

Optimistic Rollback

서버로 요청이 전달되는 도중에 취소할 수 있다는 점으로
서버에서 오는 모든 데이터가 캐시의 optimize update를 덮어쓰는 일이
없도록 해야한다.
userQuery키를 가진 useQuery가 AbortController를 signal로 관리하고, AbortController는 쿼리 함수인 getUser에 전달되는 신호를 생성하고, getUser는 해당 신호를 Axios에 전달하여 Axios와 signal을 연결시킨다.
이후 cancelQuery에 AbortController를 관리하는 동일한 키에 실행하는 경우 AbortController에 취소 이벤트를 전달한다.

onMutate

onMutate 함수는 mutation function(http request)이 실행되기 이전에 작동하며 mutation function이 받는 똑같은 변수를 전달받는다.

동일한 데이터를 사용하는 컴포넌트가 다수 존재하며, 서버에서 업데이트가 오래 걸릴 경우 아주 강력한 도구가되며, 사용자 측에서는 훨씬 반응성이 좋게 느껴지게 할 수 있다.

onMutate 함수의 return 값은 mutation function이 실패할 시 onError및 onSettled 함수에 return값이 전달되며 Optimistic Update를 롤백하는 데 유용하다.

export function usePatchUser():UseMutateFunction<User,unknown,User,unknown>{
  const { user, updateUser } = useUser();
  const toast = useCustomToast();
  const queryClient = useQueryClient();
  const {mutate:patchUser} = useMutation(
    (newUserData:User) => patchUserOnServer(newUserData,user),{

      // onMutate는 onError에 전달된 컨텍스트를 반환한다.
      onMutate: async(newData: User | null) => {
        
        // user를 대상으로 발신하는 쿼리를 모두 취소하게 하여
        // 이전 서버 데이터가 optimize update를 덮어쓰지 않게 abortController를 실행하게 한다. 
        queryClient.cancelQueries(queryKeys.user)

        // 이전 user value의 snapshot
        const prevUserData:User = queryClient.getQueryData(queryKeys.user);

        // 새로운 user 값으로 캐시를 optimistically update하고,
        updateUser(newData)

        // snapshot value가 있는 컨텍스트 객체 반환
        return {prevUserData}
      },
      onError:(error,newData,context) => {
        // 에러가 발생한다면 캐시를 저장된 값으로 롤백한다.
        if(context.prevUserData){
          updateUser(context.prevUserData);
          toast({
              title: 'Update failed; restoring previous data',
              status: 'warning'
          })
        }
      },
      onSuccess: (userData: User | null) => {
        if(user){
          toast({
            title: "user updated!",
            status: 'success'
          })
        }
      },
      // onSettled: mutate의 성공 여부와 관계 없이 onSettled에 작성된 callback이 실행된다.
      onSettled: () => {
        // user 데이터를 무효화하고 서버에서 최신 데이터를 받아 와 보여줄 수 있게 작성한다.
        queryClient.invalidateQueries(queryKeys.user);
      }
    }
  )
  return patchUser;
}
post-custom-banner

0개의 댓글