React Query(Tanstack Query) 실 사용기

6mn12j·2022년 9월 6일
6

앞단!

목록 보기
1/2

React Query(Tanstack Query)

  • React Query 공식문서 링크

  • 기존의 client state를 다루는 상태관리 라이브러리들과 다르게 sever state를 Fetching, Caching, 비동기적으로 업데이트 하는데 도움을 주는 라이브러리다.

프로젝트를 진행하면서 상태관리에 대해 고민을 하게 되었다.

  1. 기능추가가 될 수록 store가 커진다.
  2. API 관련해서 작성되는 코드수와 반복되는 작업이 너무 많다.
  3. 컴포넌트가 렌더링 하는 작업만 하면 좋을거 같은데 API나 다른 로직들도 포함된다.
  4. 현재 프론트에서 보여주는 모든 데이터는 API를 통해 가져온 데이터 인데 이 데이터를 다시 state로 만들어서 사용 시 이데이터의 값을 보장할 수 있는지?

위의 고민들을 하고 있을때 기존에 rtk-Query를 사용했을때 비동기 API 처리가 되게 간단했던게 기억이 났고 마침 프리온 보딩을 진행하면서 React-Query에 대해 더 자세히 알 수 있어서 이점을 정리하고 도입하게 되었다.

간단한 사용 후기

1. 코드의 라인 수가 줄어 들어서 코드 가독성이 좋아짐

  • redux를 사용할때는 Dispath, Store나 비동기 처리를 위한 saga등 개발자가 작성해야할 코드 양이 많았는데 reactQuery를 이용하니 작성해야할 부분이 줄어드는게 체감 됐다.
  • 궁금해서 코드 수를 세는 명령어를 실행해 봤는데 실제로 코드 줄 수가 11397 -> 11173 로 줄었다.
    • 실제 왼쪽이 reactQuery 적용 전 컴포넌트 내부 코드들 이고 오른쪽이 reactQuery를 적용한 사진이다.
      코드 수 비교

2. 기존의 비동기 API로직을 한곳에서 확인이 가능해서 관심사 분리에 용이하다.
3. onSuccess onError isFetching 등 ErrorFlag를 지원해줘서 편리하게 사용이 가능하다.

4. 지원해주는 다양한 옵션을 이용하여 효율적으로 데이터 변환이 가능

  • 지원해주는 옵션 중 select 진짜 짱 편하다.

리액트쿼리에서 캐싱 관련 기능이 주된 기능인데 현재 진행하고 있는 프로젝트에서는 비동기 API의 데이터와 동기화가 되어 있어야 해서 캐싱된 값을 사용하지는 못했다.

도입해보기

  • useQuery에 사용하는 각 query들을 hook으로 따로 폴더를 분리하여 관리 하였다.
  • queries에 해당 query를 만들고 관련 로직들을 처리하고 컴포넌트에서는 렌더링할 데이터만 hook으로 가져와서 컴포넌트에 뿌려주면된다. 기존에는 useEffect 내부에서 API 요청을 보내고 해당 값을 데이터에 넣고, isLoading등 처리를 해야 했는데 API요청 관련된 부분을 신경 쓰지 않아도 되고 코드의 수가 확실히 적어져서 매우 편리 했다.

useQuery

서버로부터 데이터를 조회할 때 사용한다.

  • useQuery 의 첫번째 인자값인 queryKey를 이용하여 캐싱된 값을 이용하거나 API로 데이터를 가져온다.

회사에서 SWR을 사용중인데 SWR의 경우에는 key 값으로 API URL 을 사용한다. 서버에 의존된 서버상태이다 보니까 API URL을 key값으로 사용하게 되니 따로 어떤 값을 key로 사용해야할지, 이값이 이미 사용중인 값인지 아닌지에 대한 고민을 하지 않아도 되어서 관리하기가 매우 편하다는 생각을 하여 후에 어드민 개발 시 React Query의 key 값도 API URL을 이용 하였다.

대표적인 인수로는
- QueryKey, QueryFn, options...
대표적인 반환값으로는
- isError, isSuccess, isLoading, isFetching...

useQuery의 select를 이용하여 데이터 변환하기

select 옵션을 사용하면 데이터를 가공 시킬 수 있다.

  • 기존에는 Axios의 interceptor 에서 data객체를 벗겨서 return 하였는데 이때는 API 통신이 성공/실패, status 코드값에 따라서 분기로 나누어 처리를 하다보니 interceptor 내부에서 따로 처리를 해줘야 하고 복잡해지는 부분이 있었다.
  • 이러한 점을 select에서 처리하게 되면 성공할때만 실행되는 옵션이라 성공/실패를 분리하는 로직이 들어가지 않아도 되는점이 편했다.
//hooks/queries/useMainAccTimesQuery.ts
export const useMainAccTimesQuery = (options?: UseQueryOptions) => {
  return useQuery(["accTimes"], getAccumulationTimes, { select: ({ data }) => data });
};
  • 프론트엔드 측과 벡엔드측의 데이터의 형태가 일치 하지 않는 문제가 있었다. select를 이용해서 데이터를 가공 해서 처리 하여 프론트 측에서는 useQuery를 이용하여 컴포넌트에서 데이터를 보여주는데만 집중 할 수 있엇다.
//utils/decode.ts
export const decodeUserData = (init: UserInfoResponse): Omit<UserInfoType, "isLogin"> => ({
  loginID: init.login,
  profileImage: init.profileImage,
  isAdmin: init.isAdmin,
  inoutState: init.inoutState,
});

//hooks/queries/useMainQuery.ts
const { data, ...queryInfo } = useQuery(["mainInfo"], getUserInfo, {
    select: (data: AxiosResponse<UserInfoResponse>) => decodeUserData(data.data),
    onSuccess: (data) => {
      setUserInfo(data);
    },
  });

인자가 있는 useQuery

  • queryKey 값에 인자를 함께 넘겨준다. reactQuery에서 queryKey를 기반으로 캐싱을 하기 때문에 사용하는 인자값이 변경되면 새로운 데이터가 필요하기 때문에 해당 값도 이용하여 캐싱 하는거 같다.
//hooks/queries/useMonthTimeLogsQuery.ts
type useMonthTimeLogsProps = {
  year: number;
  month: number;
  options?: UseQueryOptions;
};

export const useMonthTimeLogsQuery = ({ year, month, options }: useMonthTimeLogsProps) => {
  ...
  const { data, ...queryInfo } = useQuery(
    ["timeLogs", year, month],
    () => getLogsmonth(year, month),
    ...
  );

 ...
};
export const useMainQuery = (options?: UseQueryOptions) => {
  const [userInfo, setUserInfo] = useState<UserInfoType>({
    loginID: "user",
    isAdmin: false,
    profileImage: "",
    inoutState: "OUT",
  });

  const { data, ...queryInfo } = useQuery(["mainInfo"], getUserInfo, {
    select: (data: AxiosResponse<UserInfoResponse>) => decodeUserData(data.data),
    onSuccess: (data) => {
      setUserInfo(data);
    },
  });

  return { userInfo, setUserInfo, queryInfo };

useInfiniteQuery

무한 스크롤도 useInfiniteQuery 의 hasNextPage,fetchNextPage와 같은 return 값을 이용하여 간단하게 구현가능 하다.
기존에 무한 스크롤 구현시 page도 스테이트로 만들고 hasMore도 state로 만들어 관리했던걸 생각하면 엄청나게 간단하게 구현 가능!!

//hooks/queries/useInfinitiePointCouponList.ts

 const { data, refetch, hasNextPage, fetchNextPage } = useInfiniteQuery(
    [pointCouponApiUrl.getCouponCodes(campaignId)],
    async ({ pageParam = 0 }) => {
      return axios.get<PointCouponApi.GetPointCouponList>(pointCouponApiUrl.getCouponCodes(campaignId), {
        params: {
          offset: pageParam,
          pageSize: DEFAULT_PAGE_SIZE,
        },
      });
    },
    {
      enabled: !isSearch,
      getNextPageParam: (lastPage) => {
        return lastPage.data.result.length === 0 ? undefined : lastPage.config.params.offset + DEFAULT_PAGE_SIZE;
      },
    }
  );


return {
data:data?.pages
      .map((pageData) => {
        return [...pageData.data.result];
      })
      .flat(),
refetch,
}

위의 커스텀 훅을 불러 data를 map 돌면서 사용 가능하다.

  • useInfiniteQuery의 특정 페이지만 refetch하기.

데이터가 많아지면서 API를 페이지네이션을 통해 받고, 프론트엔드는 인피니티 스크롤로 표현되게끔 구현했다.
전체 리스트가 아닌 특정 페이지에 대한 API 요청만 보내고 싶다면 useInfiniteQuery의 return값 중 reftch를 이용하면 아래와 같이 사용할 수 있다.

const { mutate: updateBulkOwnerUserId } = useMutation(
   ... mutationFunction,
    {
      onSuccess: (response, variables) =>
        refetch({
          refetchPage: (page: AxiosResponse<PointCouponApi.GetPointCouponList>) => {
            const { result } = page.data;
            const shouldRefetch = result.some((coupon) =>
              variables.some(({ couponCode }) => couponCode === coupon.couponCode)
            );
            return shouldRefetch;
          },
        }),
      onError: (error) => {
        if (axios.isAxiosError(error)) {
          console.error(error);
          window.alert(`update bulk coupon code  error \n ${error.response?.data.resultMessage} 
              \n${error.response?.data.resultCode}`);
        }
      },
    }
  );

useMutation의 onSuccess 콜백을 통해 useInfiniteQuery의 refetch 메소드를 호출하여 특정 페이지의 데이터만 다시 불러오는 방법

마무리

컴포넌트에 API통신을 이용해 데이터를 불러오는 오고 불러온 데이터를 또 state로 관리하는 부분이 섞이다 보니 컴포넌트가 무거워지고 코드가 많아지는 부분을 해결할 수 있어서 좋았다.
공식문서가 진짜 잘되어 있어서 웬만한 부분은 공식문서를 위주로 찾아봤다. 다른 많은 기능들이 있던데 더 찾아봐야 할거 같다.

참고 자료

https://parang.tech/react/2022-react-01/#keep-server-and-client-state-separate
https://parang.tech/react/2022-react-01/
https://tech.kakao.com/2022/06/13/react-query/
https://tech.kakaopay.com/post/react-query-1/
https://tech.osci.kr/2022/07/13/react-query/

profile
TIL 쩨끼럽 남기는 중 💻

0개의 댓글