개발일지

redux-saga에서 react-query로 전환하기 2

FE 2022. 3. 2. 17:33

지난 포스트에 이어 react-query를 사용하며 얻은 유용한? 점 들에 대해 정리해보려고 합니다.

react-query는 정말 다양한 기능들을 제공해주고 제어할 수 있게 해줍니다. 저는 제가 직접 사용했던 범위내에서 헷갈리거나 유용했던 것들을 정리해보겠습니다.

 

 

잠깐 npm trends를 보면 react-query의 인기가 실감나지 않나요? 많은 개발자분들이 react-query를 사용중인 것 같습니다.

 

#1 useQuery isLoading VS isFetching

useQuery의 isLoading과 isFetching

이 둘이 순간 헷갈렸던 적이 있는데요.

isLoading과 isFetching을 가장 간단하게 설명하면 isLoading은 처음 로드할 때, 아직 데이터가 없을 때 isFetching은 데이터를 다시 가져와야 할 때라고 말할 수 있을 것 같습니다.

 

  1. 캐시가 없을 때 (첫 번째 쿼리를 가져올 때) isLoading이 true에서 false로 전환되면서 작동
  2. 캐시데이터가 있거나 없거나 데이터가 요청 중일 때 (다른 구성 요소에서 쿼리를 사용하는 경우) isFetching이 true에서 false로 전환

 

useQuery는 이전에 캐시된 데이터를 반환하고 서버에서 다시 데이터를 가져와야 합니다. 이러한 경우 isLoading은 항상 false지만 isFetching은 true에서 false로 전환됩니다.

<>
  {isFetching && (
    <div>
      <Spinner width={48} height={48} />
    </div>
  )}
</>

 

#2 useQuery queryKey

useQuery의 쿼리키의 중요성은 react-query를 사용하면 다들 아실거라고 생각합니다.

useQuery의 기능으로 refetch를 시킬 수 있는 방법은 정말 다양합니다. (refetch를 사용하거나 refetchQueries("쿼리키")를 사용, invaildQueries 등) 쿼리키를 바꿔서 데이터를 다시 불러올 수도 있고 쿼리키는 문자열, 배열, 객체 다양한 형태로 들어갈 수 있습니다. (문자열은 배열로 감싸집니다)

 

useQuery의 queryKey는 DeepCopy(깊은 복사)까지 해줍니다.

https://react-query.tanstack.com/comparison

 

Comparison | React Query vs SWR vs Apollo vs RTK Query

2 Render Optimization - React Query has excellent rendering performance. It will only re-render your components when a query is updated. For example because it has new data, or to indicate it is fetching. React Query also batches updates together to make s

react-query.tanstack.com

 

#3 useQuery의 여러가지 속성 사용하기

export const useGlobalGetMember = () => {
  const { data, error, isLoading, refetch } = useQuery(["member1"], getMember, {
    onError: error => console.log(error),
    refetchOnWindowFocus: false,
    refetchOnMount: false,
    enabled: !!isLogged,
  });

  const memberData = data?.data;

  return { memberData, memberError: error, isLoading, refetch };
};
  • refetchOnWindowFocus - 마우스가 윈도우 창 바깥에 갔다가 다시 클릭되면 데이터 요청의 대한 여부
  • refetchOnMount - 마운트(렌더링) 될 때 데이터를 가져올지의 대한 여부
  • enabled - 쿼리 비활성화 여부
  • retry - 쿼리 재시도 (기본값 3)

 

로그인한 member의 데이터는 자주 바뀌는 변경사항이 아니기 때문에 refetchOnMount를 false로 설정하여 페이지마다 refetch 방지를 위해 설정을 해 두었습니다.

 

enabled는 로그아웃시에는 member 데이터를 호출 할 필요가 없는데, 헤더 컴포넌트가 memberData를 사용해야 하는 경우가 있었습니다. 그래서 로그인을 판별하는 isLogged로 쿼리를 비활성화시켜 막을 수 있었습니다.

 

retry옵션은 쿼리가 실패하면 useQuery(쿼리 함수에서 오류가 발생) 쿼리의 요청이 최대 재시도 횟수(기본값 3)에 도달하지 않았거나, 재시도가 허용되는지 확인하는 함수가 제공되면 자동으로 재시도 합니다. 이 쿼리 재시도 때문에 제가 의도한 기능이 너무 뒤늦게 실행되는 현상이 발생했었습니다. retry 횟수 조절도 가능하고 비활성화도 가능합니다.

 

#4 useQuery 좀 더 유연하게 사용하기

export const useGlobalGetMember = () => {
  const { data, error, isLoading, refetch } = useQuery(["member1"], getMember);
  const memberData = data?.data;

  return { memberData, memberError: error, isLoading, refetch };
};
export const useGlobalGetMember = () => {
  return useQuery(["member1"], getMember);
};

큰 건 아니지만 위 부분과 아래 부분의 차이점이 보이시나요? 위에 코드는 어떤걸 리턴할 지 정해주고 아래 코드는 바로 리턴을 합니다.

위에 코드는 data, error, refetch 등 추가적으로 사용하는 메서드들에 대해 계속 return에 추가해줘야 합니다. 처음에는 필요한 걸 위 코드 처럼 리턴해줬지만 계속 필요할 때마다 useQuery에서 뺀 이후에 리턴까지 적어주는 것이 너무 비효율적이라 생각했습니다.

 

아래코드와 같이 쓰면 쓰는 쪽에서 유연하게 필요한 값만 정해서 쓸 수 있습니다. 더불어 중복되는 data들은 유동적으로 메소드명을 변경해서 사용 가능합니다.

  const { data: memberData, error: memberDataError } = useGlobalGetMember();

 

#5 useInfiniteQuery 사용하기

export const useInfiniteProjectList = (tab: ProjectTab, keyword: string = "") => {
  const { data, fetchNextPage, isLoading, isFetching, refetch, hasNextPage } = useInfiniteQuery(
    ["projectList"],
    ({ pageParam = 0 }) => projectApiService.getProjectList(pageParam, tab, keyword),
    {
      refetchOnWindowFocus: false,
      getNextPageParam: (lastPage) => {
        return lastPage.last ? undefined : lastPage.pageable.pageNumber + 1;
      },
    },
  );

  // 배열 하나에 데이터를 차곡차곡 담아줌
  const pages = data?.pages.map((page) => page.data);
  const contents = pages?.reduce((prev, current) => prev.concat(current), []);

  return {
    projects: contents || [],
    fetchNextProjectList: fetchNextPage,
    isLoading,
    isFetching,
    refetch,
    hasNextPage,
    totalElements: data?.pages[0].totalElements || 0,
  };
};

 

프로젝트 검색을 할 수 있는 페이지에서 사용한 useInfiniteQuery입니다.

 

무한스크롤로 구성되어 있기 때문에 스크롤을 일정시점 내렸을 때, 데이터를 계속해서 불러옵니다.

useInfiniteQuery를 사용해서 데이터를 불러오게 되면 dept가 useQuery로 불러왔을 때보단 복잡해집니다.  인피니티 쿼리로 받아온 데이터를 미리 풀어주어, 사용하는 곳에서 좀 더 편리하게 사용하도록 하였습니다.

 

  const onIntersect: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting && hasNextPage) {
        fetchNextProjectList();
        observer.unobserve(entry.target);
      }
    });
  };

  useEffect(() => {
    let observer: IntersectionObserver;
    if (element.current) {
      observer = new IntersectionObserver(onIntersect, { threshold: 0.8 });
      observer.observe(element.current);
    }
    return () => observer && observer.disconnect();
  }, [projects]);

 

useInfiniteQuery를 사용하는 곳의 로직입니다. IntersectionObserver로 영역에 도달했을 때 fetchNextProjectList() 함수를 호출하여 데이터를 잘 불러올 수 있었습니다.

 

지금은 redux-saga를 떼어내고 react-query로 바꾸는 것에 먼저 초점을 맞추고 있는데 react-query를 동적으로 사용하는 방법의 대해서 계속 궁금하고 해보고 싶단 생각이 듭니다.

 

계속 리팩터링을 진행하면서 더욱더 유용한 정보나 내용이 있으면 추후에 업데이트를 해보도록 하겠습니다.

 

감사합니다!!

'개발일지' 카테고리의 다른 글

타입스크립트로 마이그레이션 여정기 2  (0) 2022.05.05
타입스크립트로 마이그레이션 여정기 1  (0) 2022.04.30
redux-saga에서 react-query로 전환하기  (0) 2022.02.28
2021년 회고  (0) 2022.01.01
Admin 회고  (0) 2021.06.05