본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.
참고: React Query as a State Manager - TkDodo's blog
refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 등)queryClient.invalidateQueries를 사용하면 수동으로 데이터를 무효화 할 수 있다.
애플리케이션 시작시 QueryClient의 인스턴스를 생성하여 QueryClientProvider로 컴포넌트에 배부하면 React Query 라이브러리를 사용할 수 있다.
QueryClient로 생성된 인스턴스는 QueryCache, MutationCache의 컨테이너로, query와 mutation을 조작하는 메서드와 캐시 작업이 가능하다.
QueryCache는 안정적이고 직렬화된 queryKeys(queryKeyHash) 버전의 key와 Query 클래스의 인스턴스이자 메모리 내 객체(in-memory object)인 value로 이뤄져있다.
persistQueryClient를 이용할 수 있다.Query는 Observer를 통해 누가 쿼리 데이터를 구독하고 있는지 알고, 해당 관찰자에게 모든 변경사항을 알릴 수 있다.Observer는 useQuery를 호출할 때 생성되며 useQuery에 queryKey로 전달된 단 하나의 쿼리만을 구독한다. 즉, Observer는 QueryCache에 있는 query를 구독한다.Observer는 Query 업데이트를 컴포넌트에게 알려야 하는지 결정한다.Observer는 컴포넌트가 사용 중인 query의 속성들을 알고 있기 때문에 관련 없는 변경 사항을 알릴 필요가 없다. 예를 들어, 데이터 필드만 사용하는 경우 백그라운드 refetch에서 isFetching이 변경될 때 컴포넌트를 다시 렌더링할 필요가 없다. 이렇듯 Observer는 많은 작업을 수행하며, 대부분의 최적화가 이뤄지는 곳이기도 하다.Observer가 없는 쿼리를 비활성 쿼리라고 한다. 이 쿼리는 여전히 캐시에 있지만 컴포넌트에서 사용되고 있지 않는 것을 말한다. React Query 개발 도구에서는 쿼리를 구독하고 있는 Observer의 개수를 표기하고, 비활성 쿼리의 경우 회색으로 표시해두었다.QueryCache와 MutationCache의 컨테이너로, 모든 query 및 mutation에 대해 설정할 수 있는 몇 가지 기본값을 소유하고 있으며 캐시 작업을 위한 편리한 방법을 제공한다. (대부분의 경우 cache에 직접 접근하지 않고 QueryClient를 통해 접근한다.)QueryClientProvider를 이용하면 React Context를 이용하여 애플리케이션에 QueryClient를 배부할 수 있다.QueryClient를 호출할 때 인수에 객체를 전달하여 옵션값을 설정할 수 있다. (queryCache, mutationCache, defaultOptions)
{
queryCache: {
queries: {
/**
* 기본값: 0
* 얼마나 지나야 데이터를 stale하다고 판단할 지 결정하는 옵션으로, 지정한 시간 전까지는 stale한 데이터가 아니다.
* stale data: 신선하지 않은, 즉 업데이트가 필요한 데이터를 말한다. === 캐시가 만료된 데이터
* `query`가 최신 상태인 데이터는 항상 캐시에서만 읽히며 네트워크 요청이 발생하지 않는다.
* stale 데이터 또한 캐시에서 가지오지만, 특정조건에서 refetch가 일어날 수 있다.
*/
staleTime: '데이터가 캐시에 저장된 이후 다음 요청을 보낼 때까지 기다리는 시간(밀리초)',
/**
* 기본값: Infinity
* 비활성 쿼리가 캐시에서 제거될 때까지의 시간이다.
* `query`는 등록된 관찰자(`Observer`)가 없을 경우 데이터는 즉시 비활성 상태로 전환된다. 이때 해당 쿼리를 사용하는 모든 구성 요소는 언마운트(unmounted)된다.
* `cacheTime`이 지나면 `query`는 가비지 컬렉터에 의해 삭제된다.
*/
cacheTime: '캐시에 저장된 데이터의 유효 시간(밀리초)',
/** 기본값: false */
refetchInterval: '주기적으로 데이터를 다시 가져오는 시간(밀리초)',
/** 기본값: false */
refetchIntervalInBackground: '창이 비활성화되었을 때에도 refetchInterval을 계속 실행할지 여부',
/** 기본값: true */
refetchOnWindowFocus: '창이 활성화되었을 때에도 refetchInterval을 계속 실행할지 여부',
/** 기본값: true */
refetchOnMount: '컴포넌트가 처음 마운트될 때마다 데이터를 다시 가져올지 여부',
/** 기본값: true */
refetchOnReconnect: '인터넷 연결이 다시 활성화될 때마다 데이터를 다시 가져올지 여부',
/** 기본값: 3 */
retry: '요청이 실패할 경우 최대 재시도 횟수',
/** 기본값: (attempt) => Math.min(1000 \* 2 \*\*attempt, 30000)) */
retryDelay: '재시도 간격을 계산하는 함수',
/** 기본값: true */
retryOnMount: '컴포넌트가 처음 마운트될 때마다 요청을 다시 시도할지 여부',
/** 기본값: true */
retryOnWindowFocus: '창이 활성화되었을 때에도 요청을 다시 시도할지 여부',
/** 기본값: false */
suspense: '컴포넌트가 데이터를 가져올 때까지 대기하는 대신, Suspense를 사용하여 로딩 상태를 처리할지 여부',
/** 기본값: false */
useErrorBoundary: 'ErrorBoundary를 사용하여 요청이 실패했을 때 에러를 처리할지 여부',
/** 기본값: undefined */
queryFnParamsFilter: '쿼리 함수에 전달되는 인수를 필터링하는 함수',
},
},
mutationCache: {
mutations: {
/** 기본값: {} */
mutateOptions: 'mutate() 함수에 전달되는 옵션',
/** 기본값: false */
throwOnError: '서버 오류 발생 시 예외를 던질지 여부',
/** 기본값: false */
useErrorBoundary: 'ErrorBoundary를 사용하여 요청이 실패했을 때 에러를 처리할지 여부',
},
},
defaultOptions: {
/** queryKey에 대한 기본 옵션이다. */
queries: {
/** 기본값: 0 */
staleTime : '데이터를 갱신하기 전에 만료되어야 하는 시간 (밀리초 단위)을 지정한다',
/** 기본값: 0 */
cacheTime: '데이터를 캐시에 저장할 시간 (밀리초 단위)을 지정한다',
/** 기본값: true */
retry: '서버에서 오류가 발생하면 자동으로 재시도 여부를 지정한다',
/** 기본값: attempt => Math.min(attempt _ 1000, 30 _ 1000) */
retryDelay: '서버에서 오류가 발생한 후 재시도 간격을 지정한다',
/** 기본값: true */
refetchOnWindowFocus: '윈도우 포커스가 되면 새로고침 여부를 지정한다',
/**
* 기본값: false
* 값이 false이면 주기적으로 새로고침하지 않는다.
* number 타입의 값이면 해당 값(밀리초)마다 주기적으로 새로고침 한다.
*/
refetchInterval: '주기적으로 새로고침 할지 여부를 지정한다',
/**
* 기본값: params => params
* 이를 사용하면 쿼리 함수에 필요한 매개 변수만 전달할 수 있다
*/
queryFnParamsFilter: 'queryFn에 전달될 매개 변수를 필터링하는 함수를 지정한다',
},
/** mutation에 대한 기본 옵션이다. */
mutations: {
/** 기본값: true */
retry: '서버에서 오류가 발생하면 자동으로 재시도 여부를 지정한다',
/** 기본값: attempt => Math.min(attempt _ 1000, 30 _ 1000) */
retryDelay: '서버에서 오류가 발생한 후 재시도 간격을 지정한다',
/** 기본값: error => console.error(error) */
onError: '오류가 발생했을 때 호출할 함수를 지정한다',
},
},
}
getQueryData()const data = queryClient.getQueryData(queryKey);
undefined를 반환한다.getQueriesData()를 사용해야 한다.filters?: QueryFilters: Query filter를 허용하는 프로퍼티data: TQueryFnData | undefinedsetQueryData()queryClient.setQueryData(queryKey, updater);
setQueryData()는 비동기로 동작하는 fetchQuery()와 달리 쿼리의 캐시된 데이터를 즉시 업데이트하는데 사용할 수 있는 동기 함수로, 쿼리가 없으면 생성된다.cacheTime 5분 동안 Query Hook에서 쿼리를 사용하지 않으면 쿼리가 가비지 수집된다.setQueriesData()를 사용해야 한다.updater가 undefined일 경우 쿼리 데이터는 업데이트되지 않는다.setQueryData() 내에서 onSuccess는 무한루프가 발생할 수 있기 때문에 호출할 수 없다. (v4 마이그레이션)setQueryData()는 순수해야 한다. 즉, 내부에서 getQueryData()를 호출하여 즉각적으로 값을 변경시키면 안된다.queryKey: QueryKey: Query keyupdater: TQueryFnData | undefined | ((oldData: TQueryFnData | undefined) => TQueryFnData | undefined): updater로 함수가 아닌 값이 전달되면 해당 값으로 데이터가 업데이트되고, 함수가 전달되면 이전 데이터 값을 수신하고 새로운 값을 반환한다.cancelQueries()<QueryClientProvider>는 QueryClient 컴포넌트 provider로, 하위 클라이언트에게 QueryClient 컴포넌트를 JSX로 제공할 수 있다.client (필수): 제공할 QueryClient의 인스턴스contextSharing (기본값: false): context를 공유할 것인지를 선택하는 옵션useQuery는 비동기로 동작한다.useQuery는 렌더링 시점에 데이터를 가져오는 비동기 함수이다. 따라서 첫 렌더링에는 데이터를 가져오지 못하고, undefined를 반환할 수 있다.useQuery는 첫 렌더링 때 데이터를 캐시하고, 이후에는 캐시된 데이터를 사용한다. 첫 렌더링 이후에 useQuery의 비동기 함수가 실행되고 데이터를 새롭게 캐시했기 때문에 리렌더링이 발생되며, data에는 캐시된 데이터에서 값을 읽어온다.useQuery 요청의 중복을 제거한다. 따라서 동일한 QueryClientProvider 내부에서 동일한 렌더 주기에 호출되는 동일한 data fetching은 무시되어 한번의 네트워크 요청만 일어난다.queryKey를 고유하게 식별하기 때문에 동일한 QueryClientProvider 내부에서 useQuery()를 사용하면 어떤 컴포넌트라도 동일한 data를 fetch 받을 수 있다.useQuery() 하는 data fetch 함수에 접근하는 것보다 custom hooks를 만들어서 사용하는 것이 효율적이다.useQuery(queryKey, queryFunction): UseQueryResult;
queryKey: (string, number, object)[]: query를 관리하는데 사용되는 unique keyqueryFunction: api 호출을 하는 promise 함수useMutation은 mutation 상태를 추적하는 API로, 비동기로 동작한다.useQuery의 콜백과 동일하게 onSuccess, onError, onSettled를 사용할 수 있다.useQuery와 달리 컴포넌트간의 상태를 공유하지 않으며, mutation(변형)이 자동으로 수행되지 않는다.mutate)를 제공한다.useMutation가 반환하는 mutate와 mutateAsync는 비슷하지만 mutate는 반환값이 없고, mutateAsync는 Promise를 반환한다는 차이가 있다.mutate를 사용하고 mutation의 응답에 접근해야 하는 경우에만 mutateAsync를 사용하는 것이 좋다.mutateAsync는 promise 객체를 반환하기 때문에 try/catch나 then/catch 체이닝을 이용한 에러 핸들링이 가능하다.useMutation의 콜백은 mutate의 콜백 함수보다 먼저 실행되기 때문에 useMutation의 콜백에 UI와 관련된 로직을 실행한다면 mutate의 콜백이 실행되기 전에 컴포넌트가 언마운트 될 수 있다. 따라서 useMutation의 콜백에는 쿼리 무효화 등 필수적이고 논리적인 작업을 수행하고, mutate의 콜백에는 리다이렉트나 토스트 알림 같은 UI 관련 작업을 수행하는 것이 좋다.onMutate?: (variables: unknown, mutation: Mutation) => Promise<unknown> | unknownonMutate에서 반환하는 값은 onSuccess, onError, onSettled의 매개변수 context로 전달된다.onSuccess?: (data: unknown, variables: unknown, context: unknown, mutation: Mutation) => Promise<unknown> | unknown: 일부 mutate가 성공하면 호출되는 함수로, mutate 이후 서버에서 반환하는 값이 첫번째 매개변수인 data로 들어온다.onError?: (error: unknown, variables: unknown, context: unknown, mutation: Mutation) => Promise<unknown> | unknownonMutate에서 반환한 context를 이용하여 이전값으로 돌리는 롤백 로직을 구현할 수 있다.onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown, mutation: Mutation) => Promise<unknown> | unknown: 일부 mutate가 settled, 즉 성공이든 실패든 결과를 반환하면 호출되는 함수useMutation이 query에 대해 변경한 내용을 반영하는 방법mutation은 query와 직접적으로 연결되지 않는다. mutation이 query에 대해 변경한 내용을 반영하기 위해서는 추가 구현이 필요하다.
invalidateQueries()을 사용하여 기존의 데이터를 stale data로 변경하고 refetch 받는다.
invalidateQueries 호출 결과
useQuery를 통해 렌더링되거나 비슷한 Hooks를 사용하고 있다면 데이터를 refetching한다.클라이언트에서 사용자의 액션에 의해 어떤 데이터가 변경되면 서버 데이터를 동기화할 필요가 있는데, 이런 경우에 많이 사용한다.
인수로 QueryFilters를 전달하여 query 요청의 조건을 지정할 수 있다.
queryClient.invalidateQueries({
queryKey: [QueryKeys.PRODUCTS],
exact: false,
refetchType: "all",
});
exact?: boolean: 쿼리 키로 쿼리를 검색할 때, 포괄적인 검색을 원하면 false를 전달하고 정확한 쿼리 키를 검색하려면 true를 전달한다.refetchType?: 'active' | 'inactive' | 'all' | 'none': 기본값은 active로, 어떤 타입의 쿼리를 다룰 것인지 지정할 수 있다.setQueryData()를 사용하면 쿼리 캐시를 직접 업데이트 할 수 있다.setQueryData()를 통해 데이터를 직접 캐시에 넣으면 데이터가 서버에서 반환된 것처럼 동작하므로 해당 쿼리를 사용하는 모든 컴포넌트가 리렌더링된다.useMutation의 onMutate 콜백을 이용하여 Optimistic Update를 제공하고, onError, onSettled 콜백을 이용하여 롤백 로직을 구현할 수 있다.cancelQueries() 사용refetchOnMount 옵션의 기본값은 true이기 때문에, React Query는 컴포넌트가 마운트될 때 데이터를 최신으로 업데이트 해주기 위해 refetch를 한다. 이때 refetch 시점은 정확하게 알 수 없기 때문에 타이밍이 꼬이면 optimistic update 데이터가 먼저 보이고, 나중에 응답된 refetch 데이터(예전 데이터)가 이를 override 되어 화면에는 예전 데이터가 그대로 뿌려지는 현상이 일어날 수 있다. 이를 막기 위해서는 cancelQueries()를 이용하여 refetch를 취소해야 한다.onMutate 내부에 구현한 로직은 변경 전 데이터를 기준으로 업데이트를 하고 UI에 출력한다. 이 작업은 서버와의 통신 없이 이뤄지기 때문에 서버의 최종 응답과 다른 결과를 보여줄 수 있어 안정화 처리가 필요하다.onMutate 이후 onSuccess 콜백에 새로운 데이터로 캐시를 업데이트하는 작업이 필요하다.onSuccess 콜백은 서버로부터 실제 응답을 받았을 때, 즉 Optimistic Update를 수행하고 난 뒤 서버와 동기화된 데이터를 사용하여 UI를 업데이트할 때 사용한다.onSuccess 콜백을 사용하지 않고 onMutate만 작성했을 경우, 서버 응답이 실패한다면 UI와 실제 데이터가 동기화되지 않을 수 있다. 따라서 onSuccess 내부에 setQueryData()를 이용하여 새로운 데이터로 캐시를 업데이트 해주는 것이 좋다.function useInfiniteQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
}): UseInfiniteQueryResult<TData, TError>;
useInfiniteQuery는 해당 쿼리에 대한 데이터 수신(useQuery)을 파라미터 값만 변경하여 무한정 호출할 때 사용하는 메서드이다.getNextPageParam, getPreviousPageParam으로 다음 수신에 필요한 파라미터 값을 전달할 수 있다.