ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React Query] (3) useQuery() 고급 - Parallel, Dependent, Paginated, Infinite
    Front-End(Web)/React - 라이브러리들 2022. 8. 2. 01:07
    반응형

    🧐 서론

    앞에서는, React Query의 가장 기본 개념이 되는 useQuery() 메서드에 대해 알아보았다. (이는, GET 요청을 담당)

    이번 포스팅도 useQuery() 에서 이어지는 내용들이며, 이를 사용하는 다양한 방법들 그리고 확장된 기능들을 구현하기 위해 지원되는 useQuery() 외의 메서드들을 집필해보려고 한다.

     

    * https://www.youtube.com/watch?v=2s2iJLLDwgk&list=PLC3y8-rFHvwjTELCrPrcZlo6blLBUspd2&index=14 

     


     

    🌺 useQuery() 더욱 잘 쓰기

     

    - Custom Hook으로 분리하기

    아래 예시는 이전에 useQuery() 로 구현한 리스트 페이지이다. 

    import { useQuery } from "react-query";
    import axios from "axios";
    
    const fetchSuperHeroes = () => {
      return axios.get("http://localhost:4000/superheroes");
    };
    
    export const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error } = useQuery(
        "super-heroes",
        fetchSuperHeroes,
        {
          staleTime: 30000,
          onSuccess,
          onError,
        }
      );
      
      const onSuccess = (data) => {
        console.log("Data Fetching Success!", data);
      };
    
      const onError = (error) => {
        console.log("Data Fetching Fail..", error);
      };
    
    
      if (isLoading) return <h2>Loading...!!</h2>;
    
      if (isError) return <h2>{error.message}</h2>;
    
      return (
        <>
          <h2>React Query Super Heroes Page</h2>
          {data?.data.map((hero) => (
            <div key={hero.name}>{hero.name}</div>
          ))}
        </>
      );
    };

    여기서, useQuery()로 패치하는 부분을 Custom Hook으로 분리하는 방법에 대해 알아보자!

    useQuery()를 설정하고 데이터를 불러오는 로직을 별도로 분리하면서, 컴포넌트 코드량을 줄이고 패치로직의 재활용성이 향상되는 등의 이점이 있다.

     

    1) 커스텀 훅 분리

    // scr/hooks/apis/useSuperHeroesQuery.js
    
    import { useQuery } from "react-query";
    import axios from "axios";
    
    const fetchSuperHeroes = () => {
      return axios.get("http://localhost:4000/superheroes");
    };
    
    export const useSuperHeroesQuery = ({ onSuccess, onError }) => {
      return useQuery("super-heroes", fetchSuperHeroes, {
        staleTime: 30000,
        onSuccess,
        onError,
      });
    };

    먼저 useSuperHeroesQuery() 라고 하는 커스텀 훅을 만든다. 훅은 useQuery() 자체를 반환하면 된다.

    옵션을 인자로 넘겨받을지(onSuccess, onError), 직접 설정할지(staleTime) 등은 상황에 맞게 분배하면 될 것이다.

    * fetchSuperHeroes() API 패칭로직을 위 파일에 같이 썼으나, 이 역시도 apis, utils 디렉토리로 다시 분리하기도 한다.

     

    2) useSuperHeroesQuery 커스텀 훅 사용하기

    import { Link } from "react-router-dom";
    import { useSuperHeroesQuery } from "../hooks/apis/useSuperHeroesQuery";
    
    export const RQSuperHeroesPage = () => {
      const onSuccess = (data) => {
        console.log("Data Fetching Success!", data);
      };
    
      const onError = (error) => {
        console.log("Data Fetching Fail..", error);
      };
    
      const { isLoading, data, isError, error, refetch } = useSuperHeroesQuery({
        onSuccess,
        onError,
      });
    
      if (isLoading) return <h2>Loading...!!</h2>;
    
      if (isError) return <h2>{error.message}</h2>;
    
      return (
        <>
          <h2>React Query Super Heroes Page</h2>
          <button onClick={refetch}>refresh</button>
          {data?.data.map((hero) => (
            <div key={hero.name}>
              <Link to={`/rq-super-hero/${hero.id}`}>{hero.name}</Link>
            </div>
          ))}
        </>
      );
    };

    useSuperHeroesQuery() 훅을 사용하는 부분을 참고하면 된다.

    useQuery()를 반환하므로, 해당 반환값인 isLoading, data, isError, error 등의 프로퍼티들을 사용할 수 있다. 어렵지 않다!

     

     

    - useQuery By Id

    GET 요청을 할 경우, 특정 인자를 URI params 혹은 query에 담아서 보내는 경우가 종종 있다. (상세 페이지에서 id를 보내는 등)

    이를 useQuery() 를 사용하면 어떻게 구현할 수 있는지 알아보겠다.

     

    이를 구현하기 위해서, 먼저 useQuery() 의 매개변수인 queryKey와 queryFn 을 작성하는 다양한 방법을 먼저 알아보자.

    useQuery(['todos'], fetchAllTodos)
    useQuery(['todos', todoId], () => fetchTodoById(todoId))
    useQuery(['todos', todoId], async () => {
      const data = await fetchTodoById(todoId)
      return data
    })
    useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))

    * 출처 : TanStack Query 공식문서 (https://tanstack.com/query/v4/docs/guides/query-functions)

     

    맨 마지막 예시처럼, 두 번째 인자인 queryFn의 매개변수는 객체이며 안에는 queryKey(쿼리 키 값들)을 포함한 값들을 받을 수 있다.

    queryKey로 전달한 배열 중 id를 queryFn에도 사용하기 위해 위처럼 queryKey의 1번째 인덱스를 넘겨주는 것이다.

    // src/hooks/apis/useSuperHeroQuery.js
    
    const fetch = ({ queryKey }) => {
      const id = queryKey[1];
      return axios.get(`http://localhost:4000/superheroes/${id}`);
    };
    
    export const useSuperHeroQuery = (heroId) => {
      const queryClient = useQueryClient();
      return useQuery(["super-hero", heroId], fetch, {
        initialData: () => {
          const hero = queryClient.getQueryData("super-heroes")?.data?.[0] || null;
          return hero ? { data: hero } : undefined;
        },
      });
    };

     

    최종적으로 사용한 예시는 위와 같이 작성될 것이다.

     


     

    🌺 다양한 Query 메서드들

     

    1. useQueries()

    복수의 id로 정보를 조회하는 등, 같은 Query에 인자만 다른 중복호출을 진행하는 경우가 있다. (Dynamic Parallel Queries)

    Promise.all() 등으로도 구현이 가능하겠지만, React Query에서도 이를 구현하기 위해 useQueries() 라는 훅을 제공한다.

    const res = useQueries([
        useQuery1,
        useQuery2,
        useQuery3,
        ...
    ]);

    기본적인 문법은 위처럼, useQueries() 훅 안에 useQuery 들을 배열 형태로 전달한다.

     

    useQueries() 를 작성하는 다양한 방법을 알아보자!

    const res = useQueries([
        {
            queryKey: ['persons'],
            queryFn: () => axios.get('http://localhost:8080/persons'),
        },
        {
            queryKey: ['person'],
            queryFn: () => axios.get('http://localhost:8080/person', {
                params: {
                    id: 1
                }
            }),
        }
    ]);
    const fetchSuperHero = (heroId) => {
      return axios.get(`http://localhost:4000/superheroes/${heroId}`);
    };
    
    const RQDynamicParallelQueriesPage = ({ heroIds }) => {
      const queryResults = useQueries(
        heroIds.map((heroId) => ({
          queryKey: ["super-hero", heroId],
          queryFn: () => fetchSuperHero(heroId),
        }))
      );
      console.log("res", queryResults);
      return <div>RQParallelQueries.page</div>;
    };

    예제1 처럼, 연관성이 없는 복수의 API 콜을 진행하는 경우 useQueries() 인자로 각각의 쿼리를 설정한 배열을 전달하면 된다.

    예제2 는 heroIds라는 배열 Props를 받는데, 이를 map() 한 배열을 전달하면 동적인 처리도 가능하다.

     

     

    2. Dependent Query

    데이터를 호출할 때, API를 호출한 뒤 이 결과값으로 다른 API를 조회하는 경우가 있다.

    이를 각각의 useQuery()로 구현하면, 첫 번째 쿼리가 끝나기 전에 두 번째 쿼리가 실행되어 오류가 발생하는 문제가 생길 것이다.

    이를 구현하는 코드는 아래와 같다.

    const fetchUser = (email) => axios.get(`http://localhost:4000/users/${email}`);
    
    const fetchCourse = (channelId) =>
      axios.get(`http://localhost:4000/channels/${channelId}`);
    
    const RQDependentQueriesPage = ({ email }) => {
      const { data: user } = useQuery(["user", email], () => fetchUser(email));
      const channelId = user?.data.channelId;
      const { data: courses, isLoading } = useQuery(
        ["courses", channelId],
        () => fetchCourse(channelId),
        {
          enabled: !!channelId,
        }
      );
    
      console.log(isLoading, courses);
      return <div>RQDependentQueries.page</div>;
    };

    첫 번째 쿼리 데이터의 channelId 값으로 두 번째 쿼리를 실행하는 로직이다.

    위처럼 두 번째 쿼리의 실행옵션에 enabled를 설정하며, 이 조건을 첫 번째 쿼리에서 필요한 값인 channelId의 존재여부로 설정한다.


    * 참고링크 : TanStack Query 공식문서 (https://tanstack.com/query/v4/docs/guides/dependent-queries?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/dependent-queries)

     

     

    3. Paginated Queries

    다음으로 Pagination을 구현하는 방법이다. 이 역시 useQuery() 와 그 옵션설정을 기반으로 작성된다.

    const fetchColors = (page) => {
      return axios.get(`http://localhost:4000/colors?_limit=2&_page=${page}`);
    };
    
    const RQPaginatedQueryPage = () => {
      const [page, setPage] = useState(1);
      const { isLoading, isError, error, data } = useQuery(
        ["colors", page],
        () => fetchColors(page),
        {
          keepPreviousData: true,
        }
      );
    
      if (isLoading) return <h2>Loading...!!</h2>;
      if (isError) return <h2>{error.message}</h2>;
    
      return (
        <>
          <button onClick={() => setPage((prev) => prev - 1)}>-</button>
          <div>
            {data?.data.map((color) => (
              <h2 key={color.id}>
                {color.id}. {color.label}
              </h2>
            ))}
          </div>
          <button onClick={() => setPage((prev) => prev + 1)}>+</button>
        </>
      );
    };
    
    export default RQPaginatedQueryPage;

    먼저, 패치함수인 fetchColors() 는 page 값을 인수로 받아 url의 query값으로 반영한다.

    page값은 useState() 로 관리되며, keepPreviousData 옵션을 true를 주어서 이전 페이지 정보를 저장해놓는다.

     

     

    4. useInfiniteQuery()

    인피니트 스크롤을 구현할 때, 특정 조건(스크롤이 바닥에 닿는 등) 하에서 다음 데이터 목록을 패치하는 것이 일반적이다.

    이 로직을 구현하기 위해 React Query는 useInfiniteQuery() 라는 훅을 별도로 지원한다.

    import React, { Fragment } from 'react'
    import { useInfiniteQuery } from "react-query";
    import axios from "axios";
    
    const fetchColors = ({ pageParam = 1 }) => {
      return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageParam}`)
    }
    
    const RQInfiniteQueriesPage = () => {
      const { isLoading, isError, error, data, hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = useInfiniteQuery(
        'colors', 
        fetchColors, 
        { 
          getNextPageParam: (lastPage, pages) => {
            return pages.length < 5 ? pages.length + 1 : undefined
          }
        }
      );
      if (isLoading) return <h2>Loading...!!</h2>;
      if (isError) return <h2>{error.message}</h2>;
    
      return (
        <>
          <div>
          {data?.pages.map((group, index) => (
            <Fragment key={index}>
              {group.data.map(color => (
                <h2 key={color.id}>
                  {color.id}. {color.label}
                </h2>
              ))}
            </Fragment>
          ))}
          </div>
          <button disabled={!hasNextPage} onClick={() => fetchNextPage()}>more</button>
          <div>{isFetching && !isFetchingNextPage ? 'Fetching..' : 'none'}</div>
        </>
      )
    }

     

    1) 인자(매개변수) 설정

     

    먼저, useInfiniteQuery() 를 사용하면 되며 아래와 같은 설정들을 해준다.

    • 2번째 인자인 패치함수는, pageParam 이라는 페이지 값을 지정한다. 기본값을 1로 설정해서 데이터를 불러온다.
    • 3번째 인자인 옵션의 getNextPageParam() 함수다음 API요청에 사용할 pageParam 값을 정하는 것이다.
      (예제는 pages.length로 설정했으나, 통상 첫 번째 인자인 lastPage에 nextCursor를 같이 보내주므로 이로 설정)

     

     

    2) data 객체 다루기


    JSX에서 data 다루는 부분도 다른 메서드들과는 다르다. 하나의 data 객체가 아닌, 아래와 같은 형태로 내려온다.

    data는 pageParams(pageParam 정보들), pages(페이지 별 데이터) 2가지 프로퍼티를 가진 객체로 내려온다.

    여기서 다시, pages의 data 프로퍼티들만 맵핑해서 보여주면 되는 것이다.

     

     

    3) 쿼리 반환값 활용

     

    예제를 보면 hasNextPage, fetchNextPage, isFetchingNextPage 등을 활용한 것을 볼 수 있다. (각 값은 Previous도 존재)

    • hasNextPage : getNextPageParam() 에 따른 다음 페이지 존재 여부(Boolean)
    • fetchNextPage : 다음 데이터를 불러오는 메서드(Function). data의 pages 배열의 제일 끝에 새로운 데이터를 담는다.
    • isFetchingNextPage : 다음 데이터를 패치중인지 여부(Boolean)

    * 참고링크 : TanStack 공식문서 (https://tanstack.com/query/v4/docs/guides/infinite-queries)


    React Query에서 useQuery() 를 더욱 잘 쓸 수 있는 다양한 기법과 메서드들을 알아보았다.

    Parallel(동시 호출), Dependent(연계 호출), Pagination, Infinite 등 모두 현업에서 자주 쓰이는 기법으로 이 글이 유용한 레퍼런스가 됬으면 하는 바램이다.

     

    이전 글에서도, useQuery() 는 HTTP 요청의 GET을 담당하고 있다고 했다.

    하지만, 알다시피 외에도 POST, PUT, DELETE 요청을 다루는 경우도 적잖이 발생한다. 이를 어떻게 구현하는지 다음 포스팅에 정리해보겠다!

     

    📎 출처

    - [공식] React Query 공식문서 : https://react-query-v3.tanstack.com/overview  

    - [강의] Codevolution 유튜브 강의 : https://www.youtube.com/watch?v=2s2iJLLDwgk&list=PLC3y8-rFHvwjTELCrPrcZlo6blLBUspd2&index=14

     

    - [React Query 개념과 예제] DevKkiri 님의 블로그 : https://devkkiri.com/post/f14703ea-a105-46e2-89e8-26282de36a3a  

     

    - [useQuery 기본] jfori 님의 블로그 : https://jforj.tistory.com/243

    - [useQueries] jfori 님의 블로그 : https://jforj.tistory.com/245

    - [Paginated Queries] tkdals0978 님의 블로그 : https://velog.io/@tkdals0978/React-Query-Pagination%EA%B3%BC-Prefetching  

    - [Infinite Queries] jfori 님의 블로그 : https://jforj.tistory.com/246

    반응형
Designed by Tistory.