Archive

[React] React Query로 서버 상태 관리 기초 개념

manon_e 2022. 7. 28. 01:42
반응형

 

 

 

     


     

    React Query -  Stale time / Cache time / Refetching / Prefetching

     

     

    1) React Query 사용 기본
    2) isFetching vs isLoading
    3) Devtools 개발자 도구
    4) Stale Time
    5) Refetching
    6) Prefetching

     

    React Query란?

    • server data cache를 관리
    • React 코드에 server data가 필요할 때 fetch나 axios를 통해 서버로 바로 이동하지 않고 React Query cache를 요청

     

     

    ✔️ react query의 역할

    클라이언트를 어떻게 구성했느냐에 따라 해당 캐시의 데이터를 유지관리

    데이터를 관리하는 것은 react query이지만, 서버의 새 데이터로 캐시를 업데이트하는 시기를 설정하는 것은 사용자의 몫

     

     

    ✔️ 추가 기능

    서버에 대한 모든 쿼리의...

    Loading

    Error states

    pagination

    infinite scroll

    prefetch

    Mutation

    De-duplication of requests

    Retry on error

    Callbacks

     

     

     

     

     


     

     

     

     

    React Query 사용 기본

     

     

     

     

    QueryClient, QueryClientProvider를 만들어 React Query hook을 사용할 수 있게 한다.

    import { QueryClient, QueryClietnProvider } from "react-query";
    
    const queryClient = new QueryClient();
    
    function App() {
      return (
        <QueryClietnProvider client={queryClient}>
          <div className="App">
            <h1>Blog Posts</h1>
          </div>
        </QueryClietnProvider>
      );
    }
    
    export default App;

     

     

     

     

    useQuery는 몇 가지 인수를 사용한다. ("쿼리이름", 쿼리함수)

     
     // useQuery("Querykey=쿼리이름", 쿼리함수=데이터 가져오는 비동기함수);
      const { data } = useQuery("posts", fetchPosts);
      
      async function fetchPosts() {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0");
      return response.json();
    }

     

     

     

     

     

     

     


     

     

     

     

     

     

    isFetching   VS   isLoading

     

     

    isFetching

    : the async query function hasn't yet resolved

      (캐시의 유무와 상관없이) 비동기 쿼리가 아직 해결되지 않음 을 의미한다. (아직 데이터를 가져오는 중..)

     

     

    isLoading

    : isFetching의 하위 집합으로, 가져오고 있는 상태에 있음을 의미하며, 쿼리함수가 아직 해결되지 않은 상태이다.

      캐시 된 테이터도 없다. 이 쿼리를 만든 적이 없다는 의미이다.

     = 서버에서 데이터를 가져오는 중 이고(isFetching이 항상 참이다. isFetching의 부분집합) 표시할 캐시 데이터가 없다.

     

     

     

     

    ex) Pagination을 진행할 때 캐시된 데이터가 있을 때와, 없을 때를 구분해야 한다.

          주로 isLoading을 활용하여, 캐시가 있을 때를 구분하여준다.

    //현재페이지를 불러올때 다음 페이지도 같이 불러와서 cache로 저장
    useEffect(() => {
        if (currentPage < maxPostPage) {
          const nextPage = currentPage + 1;
          queryClient.prefetchQuery(['posts', nextPage], () =>
            fetchPosts(nextPage)
          );
        }
      }, [currentPage, queryClient]);
    
    
      const { data, isError, error, isLoading, isFetching } = useQuery(
        ['posts', currentPage],
        () => fetchPosts(currentPage)
      );
    
    //캐시의 유무와 상관없이 fetch할때마다 isFeching이 true로 fetching문구가 노출
      if (isFetching) return <h3>fetching...</h3>;
      
    //캐시가 없을경우만 isLoading이 true
      if (isLoading) return <h3>loading...</h3>;

     


     

     

    isError

    error인지 아닌지 boolean 값으로 반환

    react-query는 기본값으로 3번 시도하고 해당 데이터를 가져올 수 없다고 판단한다.

     

     

    error

    쿼리 함수에서 전달하는 오류

    export function Posts() {
      const { data, isError, error, isLoading } = useQuery("posts", fetchPosts);
      if (isLoading) return <h3>Loading...</h3>;
      if (isError) return <p>{error.toString()}</p>;

    error.toString()

     

     

     

     

     

     

    https://tanstack.com/query/v4/docs/reference/useQuery?from=reactQueryV3&original=https://react-query-v3.tanstack.com/reference/useQuery 

     

    useQuery | TanStack Query Docs

    const { data,

    tanstack.com

     

     

     

     


     

     

     

     

     

    Devtools 개발자 도구

     

     

    • 개발자 도구는 앱에 추가할 수 있는 컴포넌트로 개발중인 모든 쿼리의 상태를 표시해 준다.
    • 마지막 업데이트된 타임스탬프 표시
    • 쿼리에 의해 반환된 데이터를 확인할 수 있는 데이터 탐색기
    • 쿼리를 볼 수 있는 쿼리 탐색기

    By default,

    React Query Devtools are only included in bundles when process.env.NODE_ENV === 'development',

    so you don't need to worry about excluding them during a production build.

     

    $ npm i @tanstack/react-query-devtools
    # or
    $ pnpm add @tanstack/react-query-devtools
    # or
    $ yarn add @tanstack/react-query-devtools
    //App.js
    import {ReactQueryDevtools} from 'react-query/devtools'
    
    function App() {
      const queryClient = new QueryClient()
      
      return (
        <QueryClientProvider client={queryClient}>
          ...
          <ReactQueryDevtools/>
        </QueryClientProvider>
      )
    }

     

    활성 active, 비활성 inactive, 만료 stale

     

     

     


     

     

     

     

    StaleTime

     

     

    stale : 신선하지 않은

    useQuery를 통하여 가져와 캐싱된 데이터는 기본적으로 stale한 상태로 여겨진다.

    • 서버에서 조회한 데이터는 그 때 당시의 데이터 snapshot이고,
      그 후 서버의 데이터가 변경되었다면 캐싱되어진 데이터는 변경사항이 반영되지 않은 상태이므로 stale하다고 여겨진다.
    • 따라서 stale한 데이터는 최신화가 필요하는 의미이므로, stale 상태로 넘어가면 다음은 refetch가 된다.

     

     

     

    staleTime

     : 받아온 data가 fresh에서 stale 상태로 변경되는데 걸리는 시간 (데이터를 허용하는 '최대 나이')

     

    • staleTime의 default 값은 0이다.
      따라서 staleTime을 따로 설정해주지 않으면, 기본적으로는 데이터를 받아오는 즉시 stale 상태가 된다.
    • fresh 상태에서는 query instance가 새롭게 mount 되어도 re-fetch가 발생하지 않는다.
    • 클라이언트에게 만료된 데이터를 실수로 제공할 가능성을 줄이기 위해 늘 최신상태를 유지한다.

     

    기본값이 0ms인 이유? react-query개발자의 답변

     

     

     

    cacheTime

    : inactive 상태일 때 캐싱된 상태로 남아있는 시간

    • query instance가 unmount되면 data는 inacitve 상태로 변경되고, cacheTime만큼 유지된다.
      (cacheTime이 지나면 garbage collection로 수집)
    • cacheTime 지나기 전 다시 mount되면, data fetch하는 동안 cache data를 보여준다.

     

     

     

     

    staleTimecacheTime

    • staleTime은 refetching할 때 고려사항이다.
    • cache는 나중에 다시 필요할 수도 있는 데이터용이다.
      • 특정 쿼리에대한 활성 useQuery가 없는 경우, 해당 데이터는 'cold storage'로 이동한다.
      • 구성된 cacheTime이 지나면 캐시의 데이터가 만료된다. (기본값: 5분)
        cacheTime이 관찰하는 시간은 특정쿼리에 대해 useQuery가 활성화된 후 경과한 시간이다.
      • 캐시가 만료되면 해당 데이터는 garbage collected되고, 클라이언트는 데이터를 사용할 수 없다.
    • 데이터가 캐시에 있는 동안에는 fetching할 때 사용될 수 있다.
      (서버의 최신 데이터로 새로고침 하는동안 빈 페이지 대신 약간 오래된 데이터를 표시하는 편이 나으니까)
    • 만료된 데이터를 보여주고 싶지 않다면, cacheTime을 0으로 설정한다.

     

     

     

     

     

     


     

     

     

     

     

    Refetching

     

     

     

    쿼리 이름이 동일할 때 update가 되지않고 같은 데이터를 불러오는 문제 발생 시 (refetching이 되지않음)

    : 모든 쿼리가 동일한 query key를 사용하고 있기 때문이므로, 첫번째 인자에 유니크한 key값을 추가해준다.

       or 같은 query key를 사용해야할 경우 query key에 문자열 대신 배열을 전달한다.

       (두번째 인자가 query에 대한 의존성 배열로 취급하게 된다.)

     

    ex) "comment"라는 같은 query key를 가질 경우 post.id를 배열에 두번째 인자로 넣어준다.

          post.id 가 업데이트되면 react query가 새 쿼리를 생성해서 staleTime과 cacheTime을 가지게되고,

         의존성 배열이 다르면 완전히 다른 것으로 간주하게 된다.

         (데이터를 가져올 때 사용하는 쿼리 함수에 있는 값을 쿼리키에 포함해야 한다.)

    const { data, isError, error, isLoading } = useQuery(["comment", post.id], () =>
        fetchComments(post.id)
      );

     

    이 경우, 해결방안으로 기존 데이터는 무효화 시켜서 새로운 데이터를 가져오는 방법은 옳지않다.

       → ex) 게시물1의 댓글을 보고, 게시물2의 댓글을 볼 때 캐시에서 게시물1의 댓글은 제거하지 않는 것이 좋다.

                 같은 쿼리를 실행하는 게 아니므로, 같은 캐시공간을 차지하지 않고

                 게시물1을 클릭 했을 때, 1에대한 캐시 데이터를 활용하는 것이 좋다. (게시물2의 댓글로 덮어쓰는 방법은 좋지않음)

     

     

    unique key값을 지정하지 않았을 때 - 동일한 comment만 가져옴
    key값 추가시, post의 id값에따라 다른 comment data가 들어오고, 새 데이터가 들어오면 기존 데이터는 비활성되어 cache로 존재

     

     

     

     

     

    Re-fetch options

     

    stale query는 아래의 조건에따라 자동적으로 re-fetch된다.

    • 새로운 쿼리 인스턴스가 mount될 때 (page이동)
    • react component mounts
    • window refocused
    • network reconnected
    • refetchInterval 설정 시

     

     

    Re-fetching options

    • refetchOnMount (boolean)
    • refetchiOnWindowFocus (boolean)
    • refetchOnReconnect (boolean)
    • refetchInterval (time)
    export function useTest() {
      const { data } = useQuery(queryKeys.treatment, queryFn, {
        staleTime: 600000, // 10 minutes
        cacheTime: 900000, // doesnt' make sense for staleTime to exceed cacheTime
        refetchOnMount: false,
        refetchOnWindowFocus: false,
        refetchOnReconnect: false,
      });
    }

     

    staleTime 적용전 : 바로 stale 상태가 됨
    staleTime적용 후 : fresh상태에서 10분 후 stale상태가 됨

     

     

     

     

     


     

     

     

     

     

     

    Prefetching

     

     

     

    Prefetching 이란?

    → data를 미리 cache에 추가 하여 stale(만료)상태로 두고,

        data가 필요할 시에 cache에 저장되어있는 stale 상태의 data를 가져와서 사용한다. (캐시가 만료되지 않았다는 가정하에)

        데이터를 가져오는 로딩시간이 없어져 UX개선에 도움을 준다.

     

     

     

    Prefetching 사용 예시

    Pagination을 구현할 시 1페이지에서 2페이지로 넘어가는 순간 로딩을 기다려야 한다.

    로딩을 개선하기 위하여, 현재페이지의 데이터를 불러올 때 다음 페이지의 데이터를 미리 불러와서 캐시에 저장한다.

     

     

    • prefetch query는 queryClient의 메서드이다.
    • useEffect를 사용하여 의존성 배열에 currentPage를 넣어서, 현재 페이지가 변경될 때마다 다음 함수를 실행한다.
    import {useQuery, useQueryClient} from 'react-query';
    
    export function Post() {
      const queryClient = useQueryClient();
      
      useEffect(()=> {
       if(currentPage < maxPostPage){
        const nextPage = currentPage +1;
        queryClient.prefetchQuery(['posts', nextPage], ()=> fetchPosts(nextPage));
       }
      },[currentPage, queryClient]);

     

     

    1페이지 일 때, 2페이지의 데이터가 캐시로 저장되어 있다.

     

     

     

     

     

     

     



     


     

     

     

    useIsFetching

     

     

    In smaller apps

     - useQuery의 isFetching을 사용한다.

     

    In a larger apps

     - 모든 query에서 isFetching 상태일 때 Loading spinner 등 을 사용하여 fetching중임을 나타낸다.

     - useIsFetching을 사용하면 각각의 useQuery call에 isFetching을 사용할 필요가 없다.

     

     

     

    fetching중이 아니라면 0을 반환하고, fetching중이라면 진행중인 query의 number를 반환한다.

     

     

     

    [Loading spinner 컴포넌트에서 useIsFetching 이용예시]

    import { useIsFetching } from 'react-query';
    
    export function Loading(): ReactElement {
      const isFetching = useIsFetching();
    
      const display = isFetching ? 'inherit' : 'none';
    
      return (
        <Spinner
          thickness="4px"
          speed="0.65s"
          emptyColor="olive.200"
          color="olive.800"
          role="status"
          position="fixed"
          zIndex="9999"
          top="50%"
          left="50%"
          transform="translate(-50%, -50%)"
          display={display}
        >
          <Text display="none">Loading...</Text>
        </Spinner>
      );
    }

     

     

     

     


     

     

     

     

     

    error 처리

     

    React Query에서 에러를 핸들링 방법 3가지

    1. useQuery로부터 반환한 error property
    2. onError 콜백 (query에서 직접 선언하거나 global QueryCache / MutationCache)
    3. Error bounderies 사용

     

     

    - 개별 query마다 에러처리

      const { data = fallback } = useQuery(queryKey, queryFn, {
        onError: (error) => {
          const title = error instanceof Error ? error.message : '에러다';
          toast({ title, status: 'error' });
        },

     

     

    - 전역 error handler : QueryClient의 option으로 추가

    // queryClient.ts
    
    function queryErrorHandler(error: unknown): void {
      const title =
        error instanceof Error ? error.message : 'error connecting to server';
    
    
    export const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          onError: queryErrorHandler,
        },
      },
    });

     

     

     

     

    - Error boundary 사용시

     

    🔽 Error Boundary란?

    더보기

    React v16에 도입된 에러를 핸들링할 수있는 React 합성 컴포넌트

    Error Boundary는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하고, 에러가 발생한 컴포넌트 트리 대신 fallback UI를 보여준다.

    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Login />
    </ErrorBoundary>

    Login 컴포넌트에서 에러 발생시 ErrorFallback 컴포넌트가 보여진다.
    Error Boundary는 트리의 아래에 있는 컴포넌트의 오류만 포착한다.

     

     

    defaultOptions의 useErrorBoundary를 true로 지정한 곳에서는 ErrorBoundary에서 지정한 FallbackComponent를 보여주게 된다.

    export const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          onError: queryErrorHandler,
          useErrorBoundary : true,
        },
      },
    });

     

     

     

     

     

     

    반응형