react query

zmin·2022년 10월 5일
0

useQuery, useMutation 문서 읽기


리액트 쿼리는 데이터를 캐싱하고 신선하게 유지될 수 있도록 서버의 상태와 동기화해주는 작업을 한다.

기존의 상태관리 라이브러리들은 로컬의 전역 상태를 쉽게 다룰 수 있도록 만들어졌다면 react-query는 서버의 상태에 집중한다. (대부분의 데이터를 서버에서 받아오는 것을 생각한다면 나는 사실 이쪽이 상태관리라는 것엔 더 적합하다고 생각한다.)

서버의 데이터를 캐싱할 때는 그 데이터가 stale하진 않은지 잘 확인하는 과정이 꼭 필요하다. 그렇지 않으면 사용자는 old 데이터만 계속 확인하게 된다.

리액트 쿼리는 이런 과정을 간편하게 사용할 수 있게 만들어 두었고 과한 보일러플레이트 없이 바로 사용할 수 있다. 각 쿼리에 대한 처리와 상태를 가져오는 것은 물론이고 QueryClient를 이용하면 전체 쿼리와 캐시되어있는 값에 대한 상태를 확인할 수 있다.

isLoading 등과 같은 사용자 경험을 향상시킬 수 있는 상태도 규격화하여 제공한다


react-query를 사용함에 있어서 유의해야할 것

  • 기본적으로 캐시를 stale하다고 여기며 만약 이 stale 기간을 조절하고 싶다면 staleTime 값을 설정해주면 된다.
  • 네트워크가 끊겼다 다시 연결되거나 위와같이 staleTime 을 설정하거나 창이 다시 포커싱되면 요청을 새로 보내는 대신 캐싱되어있는 값을 가져와 사용한다.
  • 쿼리 결과들은 이후 사용될 가능성이 있기 때문에 더이상 이를 참조하는 인스턴스가 없게 되면 비활성 상태가 되고 5분 뒤에 GC의 대상이 된다. time out이 되기 전에 캐시를 참조하는 인스턴스가 생기면 다시 활성상태가 된다. → 이는 cacheTime 으로 설정 가능
    • staleTime과 cacheTime이 헷갈린다면 이 가이드 문서를 읽어보면 좋겠다.
  • 실패한 쿼리는 알아서 3번 재시도한다. → 조절하고 싶다면 retry 값이나 retryDelay 값을 지정
  • 쿼리 결과는 JSON data인 경우 구조적으로 비교되며 데이터가 변경되었는지 감지한다. → 성능 향상에 도움

react-query hook은 크게 두 가지로 나뉜다

useQuery

HTTP에서 GET 요청을 다룰 때 사용. 서버에서 값을 받아오기만 할 때

  • return value
    • {status, isLoading, isError, isSuccess, isIdle, error, **data**, isFetching}(etc…)
  • 요청 API마다 Unique key(Query Key)라는 걸 부여하게 되는데 이를 이용해서 데이터를 캐싱하고 업데이트하게 된다. 자동으로 부여해주지는 않고 react-query의 메소드를 사용하게 되면 첫 번째 파라미터로 이를 넘겨주게 된다. (아래 예제에선 ‘data’)
    • string또는 array key
    • array key에서 객체값을 사용할 경우 내용과는 관계 없이 동일한 키로 여겨진다
    • array key에서 중요한건 키의 순서
    • 쿼리 함수에서 사용되는 변수를 쿼리키로 지정해주는 것이 좋다. → 고유한 값을 가지게 됨
      hook의 deps 역할
  • 이를 이용해서 저장해둔 캐시를 확인하게 되는데 기본적으로 저장된 쿼리 인스턴스들은 old한 데이터로 여겨진다. 이를 조절하려면 staleTime 의 값을 global or per-query마다 설정해주는 것이 필요하다.
    const info = useQuery('data', () => axios.get(url));
  • Query functions : useQuery의 두 번째 파라미터 promise를 반환
    (보통 데이터 요청 등의 비동기 작업을 이곳에 작성하게 됨)
    - promise가 error를 반환하게 되는 경우 무조건 query function은 error를 반환해야한다. 그래야 useQuery의 반환 값 중 error에 이 값이 담긴다. axios같은 유틸들은 알아서 error를 반환하지만 fetch같은 경우 직접 확인해서 error를 반환하도록 설정해줘야한다.
    - 함수 식별자만 전달할 경우 쿼리 키가 알아서 인수로 전달된다.
        ```tsx
        function Todos({ status, page }) {
           const result = useQuery(['todos', { status, page }], fetchTodoList);
        }
         
        // Access the key, status and page variables in your query function!
        function fetchTodoList({ queryKey }) {
        	 const [_key, { status, page }] = queryKey;
        	 return new Promise();
        }
        ```
        
  • useQuery의 인수로 위 값들을 전달 할 수도 있고 하나의 {queryKey, queryFn, ...config} 객체로 전달 할 수도 있다.

하나의 컴포넌트에서 여러 useQuery를 쓰고 싶다면 쓰면 된다. parallel하게 동작

config

https://react-query-v3.tanstack.com/reference/useQuery 전체 config option은 api문서에서 다룬다.

  • enabled
    • 이전 query의 값을 이용하여 요청을 보내야하는 경우 config 에 enabled 값을 설정해주면 enabled가 true가 될 때까지 query가 실행되지 않는다
      const result = useQuery({
      	queryKey: 'key',
      	queryFn: () => {},
      	{
      		enabled: !!이전값이존재하는지,
      	}
      });
  • refetchOnWindowFocus
    • react-query는 사용자가 창에서 focus를 넘기는 경우 자동으로 refetch하게 되는데 이게 싫은 경우 config에서 refetchOnWindowFocusfalse를 지정해주면 된다.
    • 또는 창에 focus가 왔을 때 특정 이벤트를 설정해주고 싶다면 focusManager.setEventListener(콜백함수) 를 전달해줄 수 있다. iframe을 이용할 때 window focusing이 두 번 발생하게 되는 상황을 제어할 수 있다. 하지만 이 이벤트 리스너도 alert()<input type=’file’>로 인한 창을 껐음에도 focusing되어 refetcing 될 수 있다. file upload의 경우 의도와는 다르게 두 번 요청될 수도 있다는 것. 관련이슈
  • placeholderData
    • 이미 쿼리 결과가 있는 것처럼 행동하도록 하는 일종의 fake값 → caching X
    • 일반적인 값이나 memoization된 값을 사용할 수도 있으며 다른 cache된 값을 가져와서 사용할 수도 있다.
    • 하나의 캐시를 구독하는 여러 관찰자들 마다 다르게 설정할 수 있다
  • initialData
    • cache가 비어있을 경우 임시로 사용할 값
    • loading 상태를 건너뛰게 함
    • 이 config를 가지고 query를 사용하게 되면 initial 값은 실제 값처럼 바로 caching된다
      • 그래서 동일한 캐시에 대해 다른 initial 값을 가지고 접근했는데 여전히 cache가 fresh하다고 여겨지면 그 다른 initial값은 무시된다.
      • staleTime을 설정해주지 않으면 바로 다시 refetch
      • 하지만 이런 상황이 있을 수 있다. initial data자체는 0초에 만들어졌지만 query를 생성한게 20초째일 때 staleTime을 30초라고 생각하면 react-query는 query 생성 시점에서부터 캐시의 statleTime을 계산한다. 그래서 initial data가 실제로 만들어진지 50초만에 업데이트된다
        → 이게 싫다면 initialDataUpdatedAt 에 staleTime을 추가로 지정해서 initial data에 대한 처리를 앞당길 수 있다. 다른 캐시를 이용하여 initial data를 지정할 때 유리

useMutation

HTTP에서 POST, DELETE 등 서버에 변화를 발생시키는(side effect) 작업

  • return value
    • {isIdle, isLoading, isError, isSuccess, status, error, data, **mutate**}(etc…)
  • 필요한 인수는 반환된 mutate 함수 hook으로 전달하면 된다 → promise 반환
    const { mutate } = useMutation(
        (value) => axios.post(url, { value }),
    );
    
    ...
    
    mutate({name: "kim"});
    • mutation과 실제 mutate함수의 상태에 따라 실행할 on~~ 속성에 실행하고 싶은 콜백함수를 할당하면 해당 이벤트 발생시 콜백함수를 실행할 수 있다 useMutation과 mutate에 동시에 config 된 경우엔 useMutation이 더 우선시된다.
  • mutation의 경우 서버에 side effect를 발생시키기 때문에 일반적으로 로컬에 저장된 캐쉬값을 stale로 만드는 작업을 하게된다 → 보통 mutation이 onSuccess가 됐을 때 queryClient.invalidateQueries(쿼리 키) 로 만들어서 이후 해당 키에 대한 요청을 했을 때 캐시를 사용하는 것이 아니라 새로운 값을 받아올 수 있도록 한다.
  • mutation의 data, 즉 서버에서 받아온 응답은 일반적으로 side effect를 수행한 결과값인 경우가 많다.(하지만 이건 서버개발자와 정해야하는 부분)
    onSuccess에 queryClient.setQueryData(key, data) 로 cache를 업데이트 할 수도 있다.

보통 서버에 side effect를 발생 시킬 때는 client 단에 있는 캐시를 무효화 시키는 방법을 많이 쓴다. 그러다보니 반복적인 캐시 무효화 코드를 줄이기 위해 아래와 같이 custom hook을 작성해서 쓰는 경우가 많다

const useMutationCustom = () => useMutation(
	어떠한비동기작업콜백,
	{
		onSuccess: () => queryClient.invalidateQueries(쿼리 키);
	}
);

///------------------

const { mutate } = useMutationCustom();
...

다른 hooks

useQueryClient

QueryClient 객체를 사용할 수 있다. new QueryClient와 다른 것은 현재 query client 객체를 반환한다는 것, 즉 위에서 provide해준 queryClient 객체를 사용할 수 있다.

useQueries

한 번에 여러개의 요청을 보내고 싶을 때 사용

쿼리 객체를 배열로 전달하며 쿼리 결과가 배열로 반환되는 것만 제외하면 useQuery와 동일

useInfiniteQuery

query params를 이용하여 데이터를 계속 불러오는 경우 사용할 수 있으며 이를 활용해서 무한 스크롤과 pagination이 가능하게 된다.

현재 페이지의 query params를 나타내는 pageParams

다음페이지, 이전 페이지의 query params를 받아오는 getNext/PreviousPageParam 를 이용하여 쿼리가 포함된 api요청을 보내게 된다

https://react-query-v3.tanstack.com/reference/useInfiniteQuery

const {fetchNextPage, fetchPreviousPage} = useInfiniteQuery(
	key,
	({pageParams = 0}) => params를이용한어떠한비동기호출(pageParams),
	{
		getNextPageParam: (lastPage, allPages) => fetchNextPage호출시 사용할 api 파라미터,
		getPreviousPageParam: (firstPage, allPages) => fetchPreviousPage호출시 사용할 api 파라미터,
	}
);

useIsFetching

개별 요청에 대한 indicator가 아니라 화면 전체에 대한 global indicator가 필요할 경우 useIsFetching hook을 이용해서 하나의 query라도 fetching상태에 있는지를 확인하여 나타낼 수 있다.


QueryClient

react-query의 기본 객체(처럼 보인다). useQuery와 useMutation는 사용하는 방법을 좀 더 규격화해둔 느낌

각 component에서 query client instance를 만들어서 사용하는 것도 가능하지만
가장 상위의 컴포넌트에서 global config를 정의하여 <QueryClientProvider client={인스턴스}> 를 통해 아래로 전달해주는 형태로도 사용한다. 이때 하위 컴포넌트에서 hook을 이용하여 인스턴스에 접근할 경우 (useQuery, useMutation, useQueryClient) global config가 적용된다.

const queryClient = new QueryClient({
	defaultOption: {
		queries: {
			staleTime: Infinity,
		},
		mutations: {
			onSuccess: () => console.log('success!')
		}
	}
});

...

<QueryClientProvider client={queryClient}>
	...
</QueryClientProvider>

getQueryCache, getMutationCache로 각 캐시 값에 접근할 수도 있다.

메소드 중 가장 많이 쓰일 것 같다고 생각한건 위에서도 말한 QueryClient.invalidateQueries
캐시가 stale하다는 것을 잘 전달해줘야 데이터를 확실하게 잘 받아올 수 있을 것

https://react-query-v3.tanstack.com/reference/QueryClient


suspense

react에서 제공하는 error boundary 와 suspense를 바로 사용할 수 있다. status나 error 객체를 전달하는 것과는 관계 없이 바로 가장 가까운 error boundary로 전파된다.

error boundary를 reset하고 싶다면 <QuryErrorResetBoundary> 로 전체를 감싸 reset을 <ErrorBoundary> 에의 onReset에게 전달해주거나 useQueryErrorResetBoundary() 훅을 이용할 수도 있다.

<QueryErrorResetBoundary>
	{({ reset })=>
		<ErrorBoundary onReset={reset} ... >
			...
		</ErrorBoundary>
	}
</QueryErrorResetBoundary>

// 또는

const { reset } = useQueryErrorResetBoundary();
...
<ErrorBoundary onReset={reset} ... >
	...
</ErrorBoundary>

하지만 fetch on render(렌더링 하면서 데이터가 필요하면 패칭을 함 → 각 비동기 작업들이 병렬로 수행X)는 추가적인 config없이 잘 작동하지만 render as you fetch(데이터를 패치하는 것에 따라 render, 되는대로 렌더링 하겠다는 것)로 구현하고자 한다면 각 렌더링을 시작하는 상호작용 이벤트에 대해 prefetching을 미리 설정해주는 것이 좋다.

SSR

https://react-query-v3.tanstack.com/guides/ssr

ssr에서 데이터를 채워서 넘어와야하는 경우 initial data를 통해 우선적으로 렌더링을 진행한 다음 client에 왔을 때 다시 cache를 rehydrate 하여 작동할 수 있도록 한다.

특히 next js의 경우는 getStaticProps 내부에서 첫 비동기 작업을 시행하도록 한 뒤 그 값을 props로 넘겨서 initialData로 지정할 수 있다

export async function getStaticProps() {
   const posts = await getPosts();
   return { props: { posts } };
}

function Posts({ posts }) {
   const { data } = useQuery('posts', getPosts, { initialData: posts });
   ...
}
profile
308 Permanent Redirect

0개의 댓글