RTK Query에서 React Query 마이그레이션(+ InfiniteScroll 구현)

허재원·2023년 2월 14일
0
post-custom-banner

RTK Query와 React Query

서버 데이터 상태관리 라이브러리

React Query와 RTK Query 모두 서버에서 넘어온 데이터를 효율적으로 캐싱하는 것이 주요한 기능이다. 그리고 서버에서 가져온 데이터들도 하나의 상태로 관리하여 이를 서버 상태라고 한다. 서버 상태 관리 라이브러리 덕분에 서버와의 통신 과정에서 로딩 상태, 에러 여부 등을 관련 컴포넌트 내부나 전역 상태에서 관리하지 않아도 된다. 그리고 자동 데이터 캐싱을 통해 서버의 부담도 줄여줄 수 있고, 일정 시간이 지나거나 데이터 변동이 생겼을 경우 자동으로 캐시된 데이터를 제거하고 최신의 데이터를 보여줄 수 있다.

RTK Query 사용 이유

  • 이전 프로젝트에서 사용하고 있던 전역 상태 관리 라이브러리로 Redux Toolkit으로 이에 대한 연장선으로 사용하게 되었다.
  • 하나의 모듈(createApi)를 통해 관련 코드들을 모두 작성할 수 있어 파일과 모듈 관리에 대해 큰 고민을 하지 않아도 되기 때문에 유지보수가 편리할 것이라 생각했다.
  • 캐싱만이 아닌 데이터 패칭까지 지원한다.

RTK Query에서 React Query로 변경하려는 이유

  • React Query가 RTK Query보다 더 많은 기능들을 지원한다.
    - React Query에서는 RTK Query와는 달리 무한스크롤 관련 쿼리들을 간단하게 처리할 수 있다.
    - 또한, React Query에서는 suspense: trueuseErrorBoundary: true를 통해 좀 더 선언적이고 재활용가능한 코드를 작성할 수 있다.
  • 둘 모두 사용했을 때 React Query가 RTK Query에 비해 사용하기 편리하고 기능도 더 많이 지원해주는 것 같으면서 또 러닝커브가 낮다고 생각했다.
  • RTK Query의 엄격한 사용 규칙으로 장점이 될 수 있지만 반대로 러닝커브가 높은 것 같다.
  • RTK Query 사용하면서 React Query와 비교했을 때 참고할 자료가 부족하다고 느꼈다.
  • 사용면에서 자유도가 높다.
    • 데이터 패칭하는 비동기 함수를 지원하지 않기 때문에 파일 구조나 모듈 작성에 RTK Query에 비해 고민을 해야 한다. 하지만 패턴만 만들면 또 사용하기 편리하다.

위의 이러한 이유로 인해 나는 RTK Query에서 React Query로 변경하기로 했다.

React Query 적용하기

RTK Query

export const lookbookApiSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    getLookbooks: builder.query<LookbookData[], Pagination>({
      query: ({ currentPage, perPage }) => ({
        url: '/lookbooks',
        params: {
          currentPage,
          perPage,
        },
      }),
      providesTags: (result, error, arg) => {
        return result
          ? [
              { type: queryKeys.lookbook, id: 'LIST' },
              ...result.map(({ id }) => ({ type: queryKeys.lookbook, id })),
            ]
          : [{ type: queryKeys.lookbook, id: 'LIST' }];
      },
    }),
  }),
});

export const { useGetLookbooksQuery } = lookbookApiSlice;

위 코드는 /lookbooks로 데이터 패칭을 하여 데이터를 받아와 Tag를 통해 캐싱을 관리하도록 작성한 코드이고 아래 useGetLookbooksQuery를 통해 Query를 사용할 수 있다.

데이터 패칭

// src/api/lookbook.ts
export const getLookbooks = async (currentPage: number): Promise<TLookbooksDataRes> => {
  const { data } = await axiosInstance.get(
    `/lookbooks?currentPage=${currentPage}&perPage=${perPage}`,
  );
  return data;
};

나 같은 경우는 api 폴더에서 데이터 패칭 관련 비동기 함수들을 작성하고, queries 폴더에서 React Query와 관련된 함수들을 관리할 수 있도록 작성했다. 위는 lookbooks 데이터를 받아오는 비동기 함수이다.

React Query + useInfiniteQuery

// src/queries/lookbook.ts
export const useLookbooksInfiniteQuery = () => {
  return useInfiniteQuery({
    queryKey: [queryKeys.lookbooks],
    queryFn: ({ pageParam = 1 }) => getLookbooks(pageParam),
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.lookbooks.length ? allPages.length + 1 : undefined;
    },
  });
};

무한 스크롤 관련 쿼리를 지원하기 때문에 다음처럼 작성했다.

  • pageParams: 페이지들을 fetch하는데 필요한 Page Params가 담겨있는 배열이다.
  • getNextPageParam: 불러올 데이터가 더 있는지 여부와 fetch할 정보를 결정할 때 사용한다.
    • undefined가 아닌 다른 값을 반환하면 hasNextPage는 true이다.
    • lastPage에는 받아온 데이터들이 배열로 담기는데 만약 받아온 데이터가 있다면 allPages.length + 1를 통해 다음 페이지의 데이터들을 받아오고, 빈 배열이라면 더 이상 받아올 배열이 없기 때문에 undefined를 반환하도록 작성했다.

useLookbooksInfiniteQuery 적용

// src/pages/Lookbooks.tsx
export const Lookbooks = () => {
  const {
    fetchNextPage,
    hasNextPage,
    data: lookbooks,
  } = useLookbooksInfiniteQuery();

  const intObserver = useRef<IntersectionObserver | null>(null);
  const lastLookbookRef = useCallback(
    (lookbook: HTMLLIElement) => {
      if (!hasNextPage) return;
      
      if (intObserver.current) intObserver.current.disconnect();

      intObserver.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      });
      
      if (lookbook) intObserver.current.observe(lookbook);
    },
    [fetchNextPage, hasNextPage],
  );
  
  const lookbooksViewProsp: TLookbookViewProps = {
    lookbooks,
    lastLookbookRef,
  };

  return <LookbooksView {...lookbooksViewProsp} />;
};
  • fetchNextPage는 다음 페이지의 결과를 받아올 수 있도록 해주는 함수이다.
  • hasNextPage는 앞서 언급한 것처럼 위 getNextPageParam에서 반환한 것을 통해 다음 페이지가 있는지 여부를 확인할 수 있는 boolean 값이다.

callback ref

const lastLookbookRef = useCallback(
  (lookbook: HTMLLIElement) => {
    if (!hasNextPage) return;
    
    // 기존 ref가 있는 경우에는 먼저 disconnect한다.
    if (intObserver.current) intObserver.current.disconnect();

    intObserver.current = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && hasNextPage) {
        fetchNextPage();
      }
    });
    
    // 다시 받아온 마지막 li 요소에 ref를 적용할 수 있도록 다시 observe한다.
    if (lookbook) intObserver.current.observe(lookbook);
  },
  [fetchNextPage, hasNextPage],
);

lookbooks 컴포넌트에서 다음처럼 useCallback을 사용한 것을 확인할 수 있다.
공식문에서 React가 DOM 노드에 ref를 attach하거나 detach할 때 어떤 코드를 실행하고 싶다면 대신 콜백 ref를 사용하세요.라고 나와있다.

공식 문서 - DOM 노드를 측정하려면 어떻게 해야 합니까?

위 공식 문서를 통해 작성하여 lookbook을 작성하면 다음처럼 콘솔에 찍히는 것을 확인할 수 있다. 이를 통해 마지막 li 요소가 사용자 viewport에 intersecting하게 되면 true로 바뀌면서 다음 페이지의 데이터들을 받아올 수 있도록 만들었다.
이를 통해 useEffect와 useRef를 사용하지 않고 좀 더 가독성있게 작성할 수 있었다.

출처

post-custom-banner

0개의 댓글