• React Query v3 (updated : 23.03.15)

    2023. 2. 27.

    by. 옛슬

    요즘은 리액트쿼리를 조금씩 공부중인데 (진짜로 조금씩)

    이왕 느리게 공부할거 한번 공부할 때 깊게 공부하는게 좋을거 같아서

    공부하면서 조금씩 정리한거 올리기 🍏

     

    참고사이트가 기재되어있지 않은 모든 정보는 React Query 공식문서를 기반으로 합니다.

     

    Updated : 23.03.15


    Quick Start

    Installation

     yarn add @tanstack/react-query

    QueryClient, QueryClientProvider, Devtools

    import { Data } from "./Data";
    import { QueryClient, QueryClientProvider } from 'react-query';
    import { ReactQueryDevtools } from 'react-query/devtools';
    import "./App.css";
    
    const queryClient = new QueryClient();
    
    function App() {
      return (
        <QueryClientProvider client = {queryClient}>
          <div className="App">
            <h1>This is Example!</h1>
            <Data />
          </div>
          <ReactQueryDevtools/>
        </QueryClientProvider>
      );
    }
    
    export default App;

    QueryClient :

    - 쿼리클라이언트는 캐시와 상호작용할 때 사용함

    - 생각보다 관련 method가 엄청나다. (method는 이곳에서 확인 가능)

    - 단독으로 사용 가능하고, 만약에 QueryClientProvider와 함께 사용 시 사용하는 앱에 queryClient를 연결할 수 있음

     

    QueryClientProvider :

    - 컴포넌트로 앱에 QueryClient를 제공하며 연결하는 역할을 함.

    - client <필수> : 제공할 QueryClient 인스턴스

    - contextSharing <선택> : 컨텍스트를 공유할 것인지 ( 😥 요건 아직 무슨뜻인지 이해를 ...)

     

    DevTools :

    yarn add @tanstack/react-query-devtools

    - development 모드에서만 볼 수 있음.

    - Options도 다양함 (options는 이곳에서 확인 가능)

     

    유용하다고 생각되는 옵션

    1. position : 위치 변경가능

    2. panelPositon : 패널 위치 변경 가능

     

    useQuery

    const {
      data,
      dataUpdatedAt,
      error,
      errorUpdatedAt,
      failureCount,
      failureReason,
      isError,
      isFetched,
      isFetchedAfterMount,
      isFetching,
      isPaused,
      isLoading,
      isLoadingError,
      isPlaceholderData,
      isPreviousData,
      isRefetchError,
      isRefetching,
      isInitialLoading,
      isStale,
      isSuccess,
      refetch,
      remove,
      status,
      fetchStatus,
    } = useQuery({
      queryKey,
      queryFn,
      cacheTime,
      enabled,
      networkMode,
      initialData,
      initialDataUpdatedAt,
      keepPreviousData,
      meta,
      notifyOnChangeProps,
      onError,
      onSettled,
      onSuccess,
      placeholderData,
      queryKeyHashFn,
      refetchInterval,
      refetchIntervalInBackground,
      refetchOnMount,
      refetchOnReconnect,
      refetchOnWindowFocus,
      retry,
      retryOnMount,
      retryDelay,
      select,
      staleTime,
      structuralSharing,
      suspense,
      useErrorBoundary,
    })

    Returns

    - data : 데이터 (default : undefined)

    - isLoading<boolean> : 로딩 중인지 

    - isFetching<boolean> : data fetching 중인지

    - isError<boolean>: error인지

    - errror<object | null> : 에러 객체 → 해당 객체를 사용하기 위해서는 string으로 변환 후 사용

    - status <string> : 'loading' | 'error' | 'success' 상태를 리턴.

      - loading : 캐시 데이터가 없고, query가 끝나지 않았을 때

      - error : query가 에러일 때

      - success : 에러가 없고, 데이터를 성공적으로 받아왔을 때

    - fetchStatus <FetchStatus> : 'fetching' | 'paused' | 'idle' 상태를 리턴

      - fetching : queryFn이 실행될 때 마다 (초기 loading 포함), 

      - paused : query는 fetch를 하고 싶었지만 정지인 상태

      - idle : query가 fetching을 안할 때 

     

    Options

    - queryKey <필수> : query에 사용할 key이다.  (하단에 설명있음)

    - staleTime <number | Infinity > : 초깃값은 0. 해당 값이 언제부터 stale 데이터인지 시간을 적어주는 옵션

     

    isFetching과 isLoading의 차이점

    - isLoading:  returns 중 status를 참고해서 리턴하는 값이다.

    - isFetching:  returns 중 fetchStatus를 참고해서 리턴하는 값이다.

     

    이곳에서 답을 찾을 수 있었다

    status는 data가 있냐 없냐로 확인 후 상태를 리턴하고, fetchStatus는 queryFn이 실행되고 있는지 아닌지 확인한다.

    즉, fetchStatus가 status를 포함하는 개념이다.

    처음 로딩 시에는 데이터 유무 확인을 통해 status도 리턴되지만, 이때 fetching도 당연히 이루어지기 때문에 fetchStatus도 true이다.

    그러나, refocus시에 refetching만 이루어지기 때문에 이때는 fetchStatus로 확인해야 한다.

     

    Query Keys

    ⭐ 매우매우 중요함 

    - 앞서 말했듯이 queryKey는 query에 사용할 key로 리액트에서 key는 unique해야한다. 그 이유는 다들 알겠지만 이를 통해 해당 데이터를 인지할 수 있기 때문

    - 그렇기 때문에 언제든 서로 다른 데이터는 다른 queryKey를 가지고 있어야한다.

    - 참고로 queries는 어떤 트리거에 의해서만 리패칭이 된다.

     

    ❓ 어떤 트리거의 예시

    1.  컴포넌트 재렌더링 (remount)

    2.  윈도우 재포커스

    3.  refetch function을 실행할 때

    4.  automated refetch

    5.  값 조작(mutation)

     

    - 이게 문제될 수도 있는 부분은 예를 들면 [추천 게시글]과 같은 컴포넌트가 있다고 했을 때, 각 게시글은 post.id를 통해 추천 게시글을 받아온다고 가정하면 추천 게시글은 새로운 useQuery를 만들것이다.

    // Post.jsx
    
    function Posts () {
    	const {data:posts, isLoading, isError } = useQuery('posts', fetchPosts);
        
        return (
    		<main>
            	{
            		posts.map(post => (
            			<section key={post.id}>
            				<h2>{post.title}</h2>
            				<p>{post.text}</p>
            				<RecommededPosts postId={post.id}/>
            			</section>
            		))
            	}
    		</main>
        )
    };
    
    export default Posts;
    // RecommededPosts.jsx
    
    function RecommededPosts ({postId}) {
    	const {data:recommededPosts, isLoading, isError } = useQuery('recommededPosts',() => fetchRecommededPosts(postId));
        
        return (
    		<main>
            	{
                    recommededPosts.map(recommededPost => (
                    	<Link to={recommededPost.href} key={recommededPost.id}>
                    		<h2>{recommededPost.title}</h2>
                            <img src={recommededPost.previewImg} />
                        </Link>
                    ))
                }
           	</main>
        )
    }

    - 이때 약간 착각할 수도 있는게 리액트의 경우 props가 변경되면(postId) 리액트가 재렌더링 되기 때문에 당연히도 값이 변경될 것이라고 생각되지만 추천게시글은 변경되지 않는다. 그 이유는 모든 추천 게시글이 동일하게 recommededPosts라는 키를 가지고 있기 때문이다.

     

    해결방법

    const {data, isLoading, isError } = useQuery(
    	['recommendPosts', postId],
    	() => fetchRecommededPosts(postId)
    );

    - key를 array로 주면됨. 

    - 참고자료는 이곳에 있음.

    - 리액트를 배운 사람이면 약간 useEffect의 deps를 생각하면 쉽다.

    - 참고로 v3에서는 String-Only Query Keys가 존재했는데, v4부터는 Simple Query Keys라고 해서 그냥 array에 값을 하나 주는 방향으로 바뀐듯하다. (v4는 이곳에)

    - 즉, query 함수가 변수에 의해 재렌더링이 되어야하는 경우에는 무조건 해당 변수를 queryKey에 넣어줘야한다.

    Query Retries

    import { useQuery } from '@tanstack/react-query'
    
    const result = useQuery({
      queryKey: ['todos', 1],
      queryFn: fetchTodoListPage,
      retry: 10, // retry를 설정할 수 있음.
    })

    - useQuery가 실패할 경우, 리액트쿼리는 자동으로 총 3번까지(이게 초깃값) 재요청을 날린다. 

    - Retry Delay : retryDelay로 retry를 지연할 수 있음.

     

    Prefetching

    - 유저의 행동을 미리 예측하고 해당 데이터를 미리 fetching 해오는 것

    - prefetchQuery 메소드를 활용함

    const prefetchTodos = async () => {
      // The results of this query will be cached like a normal query
      await queryClient.prefetchQuery({
        queryKey: ['todos'],
        queryFn: fetchTodos,
      })
    }

    - 해당 값은 보통의 쿼리처럼 캐시된다.

    - 만약에 해당 값이 변경되지 않았을 경우, 해당 데이터는 다시 fetching되지 않음

    - 만약에 prefetchQuery에 staleTime을 지정했을 경우 해당 시간이 지나면 fetching함

    - 만약 미리 가져온 쿼리에 대한 useQuery 인스턴스가 없다면, cacheTime에서 지정한 시간이 지난 후에 삭제되어 가비지 콜렉터 대상이 됨

     

    Mutations

    - 데이터 생성 / 업데이트 / 삭제시 사용, 혹은 서버의 side-effects를 위해 사용

    - useMutation 훅을 사용

     

    function App() {
      const mutation = useMutation({
        mutationFn: (newTodo) => {
          return axios.post('/todos', newTodo)
        },
        retry: 3 // 재시도
      })
    
      return (
        <div>
          {mutation.isLoading ? (
            'Adding todo...'
          ) : (
            <>
              {mutation.isError ? (
                <div>An error occurred: {mutation.error.message}</div>
              ) : null}
    
              {mutation.isSuccess ? <div>Todo added!</div> : null}
    
              <button
                onClick={() => {
                  mutation.mutate({ id: new Date(), title: 'Do Laundry' })
                }}
              >
                Create Todo
              </button>
            </>
          )}
        </div>
      )
    }

    mutation의 states

    1. isIdle / status === 'idle' : mutation이 idle 상태거나 리셋/최신 상태에 사용

    2. isLoading / status === 'loading' : 로딩중

    3. isError / status === 'error' : 에러

    4. isSuccess / status === 'success' : 성공

     

    * 더 많은 정보를 error 혹은 data를 통해 받을 수 있으며 이는 에러 혹은 성공시 사용이 가능함.

     

    mutation 함수 사용 방법

    - mutate() 함수를 통해 인수를 전달할 수 있음.

    - mutate() 함수는 비동기 함수이기 때문에 리액트 16버전 혹은 이전 버전은 꼭 다른 함수로 한번 감싸줘야됨. (리액트 이벤트 풀링 때문)

     

    mutation 함수 리셋 방법

    - reset() 함수를 통해 함수 리렌더링 가능.

     

    상태에 따른 추가 콜백

    - 헬퍼 옵션으로 mutation의 lifecycle 상태에 따라 콜백을 호출 할 수 있음.

    useMutation({
      mutationFn: addTodo,
      onSuccess: (data, variables, context) => {
        // I will fire first
      },
      onError: (error, variables, context) => {
        // I will fire first
      },
      onSettled: (data, error, variables, context) => {
        // I will fire first
      },
    })
    
    mutate(todo, {
      onSuccess: (data, variables, context) => {
        // I will fire second!
      },
      onError: (error, variables, context) => {
        // I will fire second!
      },
      onSettled: (data, error, variables, context) => {
        // I will fire second!
      },
    })

     

    Promise 방법으로 작성 시

    const mutation = useMutation({ mutationFn: addTodo })
    
    try {
      const todo = await mutation.mutateAsync(todo) // resolve success or throw an error
      console.log(todo)
    } catch (error) {
      console.error(error)
    } finally {
      console.log('done')
    }

     

     

     

    댓글