[TIL] 2023/11/03

yongkini ·2023년 11월 3일
0

Today I Learned

목록 보기
165/173
post-thumbnail

Today I Learned

React-Query 인터페이스 관련 정리

react-query devtools 사용

: 처음에 동료 개발자가 이걸 쓰는걸 보고 뭐지,, 겐조 마크인가? 했던 그 마크가 바로 react-query에서 제공하는(사실 그냥 리액트 쿼리 마크다) devtools 버튼이다.

    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
    </QueryClientProvider>

이런식으로 써주면 된다. devtools의 기능은 사용하면서 익혀보고자 한다.

isFetching 과 isLoading

  • isFetching 이 isLoading을 포함하는 개념이다. 즉, isLoading은 isFetching의 부분집합이다.
  • isLoading을 기준으로 보면 다음과 같은 2가지 개념을 만족시켜야 true 가 된다
    1) fetching 중이어야 한다.
    2) 현재 캐싱된 데이터가 없어야 한다.
    결론적으로, isLoading은 prefetching으로 데이터를 미리 받아놓은 상황이라면, false로 유지된다(이 때, 캐싱된 데이터는 설정을 따로 해놓지 않는 이상 5분간 유지된다고 한다).

useQuery

  • useQuery의 key가 바뀔때마다 query를 재실행한다.
  const {
    isFetching,
    data: videos,
    refetch,
  } = useQuery({
    queryKey: ["videos", videoId],
    queryFn: () => getVideos(videoId),
    select: (data) => data.data,
  });

위와 같이 queryKey: ["videos", videoId], 여기에 써준 videoId가 바뀔 때마다 쿼리를 재실행 (queryFn)한다는 말이 된다.

** 써본 결과 useQuery는 api가 실패했을 때 디폴트로 3번까지 재요청을 한다(결과적으로 기본 세팅이 계속 에러가 발생해쓸 경우 총 네번의 api를 실행하는 것이라고 할 수 있다).

  • staleTime 으로 신선도 유지(?) 시간을 설정할 수 있다(ms단위)
  const { data, isLoading, isError, error } = useQuery(
    ["posts", currentPage],
    () => fetchPosts(currentPage),
    { staleTime: 10000, keepPreviousData: true },
  );

이렇게 staleTime을 10초로 해두면 페이지네이션 상태라고 했을 때 해당 페이지 넘버(currentPage state)로 이동을 해도 10초가 지나지 않았다면(처음 api 호출 이후) 캐싱된 데이터를 사용한다(api를 호출하지 않는다).

  • keepPreviouseData
    : useQuery option에는 keepPreviousData라는 옵션이 있다. 이 옵션을 위의 코드처럼 true로 해주게 되면, background에서 다음 데이터를 fetch하는 동안(prefetch) 화면에 이전의 데이터를 가져와 보여줄 수 있는 기능이다. 이건 유저 경험을 좋게하려고 쓰는 것 같다. 러프하게 말하면, 다음 데이터를 fetch하는 동안, 사용자에게 아무것도 안보여주기는 뭐하니까 이전 데이터라도 보여주고자 할 때 쓰는 옵션이라고 할 수 있다.

prefetching

  • prefetching의 사용 예시 정도로 알고가면 좋을 코드
export function usePrefetchPost(): void {
  const queryClient = useQueryClient();
  queryClient.prefetchQuery(queryKeys.post, getPosts);
}

위와 같이 prefetchQuery를 커스텀 훅으로 만들어서 특정 컴포넌트에 진입하기 전에 미리 데이터를 불러와 캐싱해놓을 수 있다. 이렇게하면 예전에 최적화 공부할 때 했던 것처럼 특정 페이지 혹은 모달창 등을 열기전에 미리 컨텐츠를 받아놓고, 지연시간 없이 바로 콘텐츠를 렌더링해서 보여줄 수 있다는 장점이 있다. 이에 더해서, 커스텀 훅으로 만들지 않고, 예를 들어, pagination을 해놓은 부분이 있다고 할 때

  const queryClient = useQueryClient();

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

이런식으로 현재 페이지네이션 내의 페이지가 변할 때마다 prefetchQuery를 통해 다음 페이지의 데이터를 페칭해와서 캐싱할 수 있다. 이 때, prefetch를 해온 데이터도 staleTime이 지나면 결국 해당 페이지가 됐을 때 다시 fetching 한다.

이 떄, { staleTime: 1000000000000000000, keepPreviousData: true }, 이런식으로 staleTime을 사실상 무한대로 주면 어떻게될까? 당연하게도 데이터는 처음 api를 호출 한번을 한 뒤로는 더이상 fetching을 하지 않는다(신선도를 유지할 필요가 없는 데이터는 이런식으로 쓰면 될 것 같다 => 한번 받으면 갱신할 필요가 없는 데이터들).

위에서 말한 것처럼 한번 받으면 자주 갱신할 필요가 없는 데이터가 있을 경우에는 react-query에서 자동으로 refetch 하는 옵션을 끌 수 있다.

  const { data = [] } = useQuery(queryKeys.comments, getComments,{
    refetchOnMount: false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
  })
refetchOnMount (boolean | "always")

: refetchOnMount 는 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션이다. default는 true이고, 만약 always 로 설정하면 마운트 시 마다 매번 refetch 를 실행한다.

refetchOnWindowFocus (boolean | "always")

: refetchOnWindowFocus 는 데이터가 stale 상태일 경우 윈도우 포커싱 될 때 마다 refetch를 실행하는 옵션이다. 예를 들어, 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 중인 탭을 눌렀을 때도 이 경우에 해당한다. 심지어 F12로 개발자 도구 창을 켜서 네트워크 탭이든, 콘솔 탭이든 개발자 도구 창에서 놀다가 페이지 내부를 다시 클릭했을 때도 이 경우에 해당한다.
default true이고, always 로 설정하면 항상 윈도우 포커싱 될 때 마다 refetch를 실행한다.

refetchOnReconnect (boolean | "always" | ((query: Query) => boolean | "always")

: 네트워크 연결이 다시 설정될 때 데이터를 다시 가져와야 하는지 여부를 결정한다. 'false'로 설정되면 네트워크 재연결이 발생할 때 데이터를 다시 가져오지 않음을 나타내고, 'true'로 설정하면 구성요소가 인터넷 재연결을 감지하면 자동으로 데이터 다시 가져오기를 트리거한다. 근데 개인적으로,, 네트워크 연결이 다시 설정될 일이 얼마나 있을까 싶기는 하지만 그래도 뭐,,!

refetchInterval

: 추가로 아예 이렇게 refetch 주기를 개발자가 설정해서 쓸 수 있다.

  const { data, isLoading, isError, error } = useQuery(
    ["posts", currentPage],
    () => fetchPosts(currentPage),
    {
        refetchInterval: 60000 // 60초(매분마다)마다 리페치 하도록!
    }
  );

위의 내용을 적용해서 만든 페이지네이션

useIsFetching 과 useIsMutating

: 두 메서드는 모드 현재 진행중인 request의 개수를 정수로 나타낸다. 그렇다면 이걸 어떻게 사용해볼 수 있을까?. 간단하게 전역적으로 혹은 React.createPortal 등으로 사용하는 root depth의 스피너를 on, off 할 때 필요한 flag로 유용할 것 같다. 예를 들어,

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

  return <Spinner display={display} />  
}

위와 같이 해놓으면 현재 request중인 것이 한개라도 있으면 Spinner를 보여줄 것이고, 그렇지 않으면 보여주지 않을 것이다.

fallback

  const {
    isFetching,
    data: comments = [],
    refetch,
  } = useQuery({
    queryKey: ["comments", id],
    queryFn: () => getComments(id),
    select: (data) => data.data,
  });

명시적으로 fallback을 쓰려면

  const fallback = []
  const {
    isFetching,
    data: comments = fallback,
    refetch,
  } = useQuery({
    queryKey: ["comments", id],
    queryFn: () => getComments(id),
    select: (data) => data.data,
  });

위와 같이 써줄 수 있겠다. fallback은 사실 별건 아니고, useState()로 치면 기본값이다. 본래

    {comments?.map((comment) => {
        return <Comment key={comment.id} comment={comment} refetch={refetch} />;
      })}

이렇게 comments가 undefined 인 케이스를 comments?.map 이런식으로 커버했다면, fallback을 쓰면 그럴 필요가 없는 것이다.

defaultOptions

: 작업을 할 때 예를 들어, useMutation의 경우

  const mutation = useMutation({
    mutationFn: deleteComment,
    onSuccess: () => {
      dispatch(
        addToast({
          text: "Successfully deleted",
          variant: "success",
        }),
      );
      refetch();
    },
    onError: () => {
      dispatch(
        addToast({
          text: "failed to delete",
          variant: "error",
        }),
      );
    },
  });

이런식으로 onSuccess, onError 콜백을 둬서 썼었는데, 이렇게하면 해당 mutation이 아닌 것에서는 재정의를 해줬어야했다. 물론 위의 케이스는 delete를 하는 mutation인데, 이 메시지에 특화해서 작성을 했다고 생각할 수 있지만, queryClient쪽에 공통적으로 디폴트 옵션을 걸어놓으면 매번 이렇게 생성해야하는 수고를 덜 수 있다.

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

위와 같이 mutation, query 등에 디폴트 옵션을 해주면 모든 useMutation, useQuery에 적용이 된다. 이 때, queryErrorHandler

function queryErrorHandler(error: unknown): void {
  const id = 'react-query-error';
  const textMessage = error instanceof Error ? error.message : 'error connecting to server';
  
  dispatch(
    addToast({
      text: textMessage,
      variant: "error",
    }),
  );
}

이런식으로 써주면 매번 delete, update 등의 콜백에 따로 onError or onSuccess 콜백을 등록하지 않아도 된다.

Select

: 예전에 레코일에서도 이런게 있었던 것 같은데(물론 useSelector에도 있다), 받아온 데이터를 컨버팅 해주는 로직을 넣어주는 곳이다. 정확히는 useQuery의 3번째 인자인 options 객체에 컨버팅 로직을 넣어주면 useQuery를 통해 받아오는 data를 원하는 형식에 맞게 바꿔준다.

 const commentMapFn = useCallback(
    (unfilteredStaff) => comments.map((comment) => {
    	if(!comment.visible) {
			comment.display = "none"
        	return comment
        }
        
        return comment;
    })
    ,[comments])
    
  const {data: comments = [] } = useQuery(queryKeys.comments, getComments,{
    select: commentMapFn 
  })

Dependent Queries(enabled)

: 개발을 하다보면 특정 데이터를 받아와서 거기서 온 id 값을 통해 다른 api를 호출해서 값을 다시 받아와야 하는 케이스가 있다. 그런 상황에서 아래와 같이 사용해준다. user 정보가 업데이트 되면 그 user 의 comment를 받아오도록 하는 식의 코드이다.

export function useUserComments(): Comment[] {
  const { user } = useUser();
  const fallback:Comment[] = [];

  const { data: userComments = fallback } = useQuery(
    "user-comments",
    () => getUserComments(user),
    {
      enabled: !!user
    })
  
   return userComments;
}
profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글