구) React Query / 현) Tanstack Query 이제 제대로 사용해보자

박영은·2024년 9월 20일
0

TanStack Query v4 기준으로 수정된 글입니다.


yarn add @tanstack/react-query @tanstack/react-query-devtools

🌳 React Query (Tanstack Query)

  • React 앱에서 비동기 로직을 쉽게 다루게 해주고, 서버 상태를 효율적으로 관리할 수 있게 도와주는 데이터 페칭 및 상태 관리 라이브러리
    • 서버에서 데이터를 가져오고 동기화하는 과정이 크게 단순화 됨.
  • 낙관적 업데이트를 지원
    • 서버의 데이터 업데이트가 성공하기 전에도 UI를 업데이트할 수 있게 함
    • 서버와의 데이터 동기화를 신경쓰지 않고 먼저 사용자에게 성공 시 UI를 보여준 후, 요청의 결과가 오면 성공/실패 여부에 따라 UI 업데이트(아래 7번항목 참고)
      • 사용자 = 서버 통신 여부와 관계 없이 UI를 확인할 수 있음
  • Query (useQuery) : GET, 서버의 데이터 조회(Fetching), 데이터의 비동기 로딩, 데이터 캐싱 및 자동 리프레시, 상태 관리 (로딩, 에러, 성공) 등.
  • Mutate (useMutation) : POST, PUT, DELETE, PATCH, 데이터 생성, 수정 삭제, 서버에 변경사항 전송, 낙관적 업데이트 지원, 에러 핸들링, 재요청은 수동으로 트리거 설정 필요.



🌱 장점

1. 동기화

  • 서버와 데이터를 동기화 => 오래된 데이터 새로고침 or 백그라운드에서 자동 갱신
    • 컴포넌트가 마운트되거나 사용자 초점이 앱으로 돌아올 때 오래된 데이터를 자동으로 새로고침하여 업데이트함.
    • 데이터 업데이트를 최대한 신속하게 반영

2. 성능 최적화

  • 캐싱과 백그라운드 새로고침, 불필요 요청 방지 등의 기능
    • 페이지네이션 및 데이터 지연 로드와 같은 성능 최적화

3. 데이터 자동 캐싱 기능

  • API에서 가져온 데이터를 캐싱 = 동일한 데이터의 불필요한 중복 요청 줄여줌.
    • 캐싱된 데이터를 사용하면서 백그라운드에서 새 데이터를 받아와 최신 상태를 유지

4. Server State를 관리함.

5. 서버 데이터 복잡하게 연동 or 실시간 데이터 필요 or 빈번한 데이터 페칭이 필요한 경우에 특히 유용함.

6. 로딩 상태, 오류 상태, 성공 상태 등 복잡한 상태 관리를 자동으로 처리해줌.

7. 서버 응답이 오기 전에 UI를 먼저 업데이트해 빠르게 반응하도록 할 수 있음.

8. 요청이 실패하면 특정 조건에 따라 자동으로 재시도를 수행할 수 있음.



🌱 단점

  • 번들 크기가 조금 늘어남
  • 서버 상태 관리가 복잡하지 않은 간단한 앱에서는 굳이이긴 함.





🌴 Life Cycle

1. fresh (데이터가 프레시한 상태)

  • active 상태의 시작
  • staleTime을 늘려주면 => fresh한 상태가 유지됨.
    - 쿼리가 다시 마운트 되어도 페칭이 발생하지 않고 기존 fresh한 값을 반환함

2. fetching (데이터 요청 상태)

  • 요청을 수행 중인 쿼리

3. stale (데이터가 만료된 상태)

  • 인스턴스가 존재하지만 이미 패칭이 완료된 쿼리.
  • 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도하면 => 캐싱된 데이터 반환하며 리패칭 시도함.

4. inactive (사용하지 않는 상태)

  • active 인스턴스가 하나도 없는 쿼리.
  • inactive된 이후에도 gcTime 동안 캐시된 데이터 유지됨.
    - gcTime이 지나면 캐시에서 제거됨.

5. delete (캐시에서 제거된 상태)





🌴 사용법

🍋 최상단에 Provicer로 감싸기

  • react 앱 최상단인 App.js에 Context Provicer로 하위 컴포넌트를 감싸고 queryClient를 넘겨
    => 이 Context가 비동기 요청을 알아서 처리하는 background 계층임.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// 👉 React Query 기본 옵션 설정
const queryClient = new QueryClient(); // queryClient가 리렌더되면 Cache 다시 백지화되니까 App 바깥에 선언

const App = () => {
  return (
    <>
      <QueryClientProvider client={queryClient}>
        <Router /> //  <Component {...pageProps} />
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </>
  );
};

export default App;
  
  
  
// 아니면 이렇게 state로 적용하는 방법도 있엉
// state로 하면 상태값 변경하지 않는 한 한번 선언했으니 변화 없음
const App = () => {
  const { setLoadingStore } = useLoading();
  const queryCache = new QueryCache({
    onError: (error: Error) => {
      console.error('🚨 ', error);
    },
    onSettled: () => {
      setLoadingStore(false);
    },
  });

  const [queryClient] = useState(
    new QueryClient({
      queryCache,
      defaultOptions: {
        mutations: {
          onMutate: () => {
            setLoadingStore(true);
          },
          onSuccess: () => {},
          onError: (error: Error) => {
            console.error('🚨', error);
          },
          onSettled: () => {
            setLoadingStore(false);
          },
        },
      },
    })
  );

  return (
    <>
      <QueryClientProvider client={queryClient}>
  ...
  

🌿 useQuery

  • 능동적으로 데이터 페칭 시
  • 쿼리는 Promise를 리턴하는 함수와 unique key로 구성됨.
  • useQuery로 데이터를 호출한뒤 ReactQueryDevtools를 보면 항상 fresh한 상태임.
    => refetch의 전제조건은 데이터가 stale한 상태여야 하기 때문임
    => 현 상태에서는 gcTime이 지나더라도 refetch 하지 않음.
import { useQuery } from '@tanstack/react-query'

const TodoList = ()=>{
  //const { data, isPending, isError, ... } = useQuery(queryKey, queryFn, options)
  const { isPending, isError, data, error } = useQuery('todos', getTodoList)
  
  if(isPending){
  	return <span>로딩중...</span>
  }
  
  if(isError){
  	return <span>Error : {error.message}</span>
  }
  
 return (
 	<ul>
   		{data.map(todo => (
         	<li key={todo.id}>{todo.title}</li>
         )}
    </ul>
 )
}


// refetch 시키기
export const useGetUserList = (userId: string) => {
  const { data, refetch } = useQuery<userList[], Error>({
    queryKey: ['userList', userId],
    queryFn: async () => {
      const res = await Api.get('/api/user/list', {
        params: {
          userId,
        },
      });
      return res.data;
    },
    enabled: !!vendorId,
  });
  return { data, refetch };
};

// 사용하는 곳
  const { data, refetch: refetchUserList } = useGetUserList(userId); 
  useEffect(() => {
    const userId = selectedUser.userId;
    if (userId) {
      refetchUserList(); // refetch를 호출하여 새로운 데이터를 가져옴 (queryKey로 하는 방법을 우선쓰자!)
    }
  }, [selectedUser.userId]);

🔴 상태 업데이트가 비동기로 발생 👉 userId가 변경되는 순간에 refetch가 호출되지만, 그 결과가 반영되기까지 시간차가 발생할 수 있음.
🔴 useEffect 안에서 refetch를 호출 👉 더 많은 boilerplate 코드가 필요
🟢 코드 간결성을 위해 queryKey에 담고 바뀌면 재호출하게 하자.
🟢 react-query의 캐싱 및 데이터 관리 기능에 더 효과적
	👉 동일한 userId로 여러 번 호출하더라도 캐시된 데이터를 재사용
🟢 쿼리 키가 바뀔 때마다 새로운 데이터를 가져오기 때문에, userId가 변할 때마다 새로운 API 호출이 보장
🔵 useEffect를 사용하는 경우 👉 특정한 로직이 필요한 경우(e.g. API 호출 외에 다른 side effect를 처리할 때)로 제한하는 것이 좋음.


// useKBOTeamList.ts. (hook) // 응용) 한번 불러온 뒤 오랫동안 저장해두고 사용할 데이터
const { data: KBOTeamList } = useQuery(['TeamList'], fetchCmmCode, {
  staleTime: Infinity,
});
return {  ..., KBOTeamList }

// useKBOTeamList 사용 부분
import useKBOTeamList from '@/hook/useKBOTeamList'
const KBOTeamListPage = () => {
    const { TeamList } = useKBOTeamList();
    return (
    	<>
        	{TeamList && <UpperRankTeam />}
        </>
    )
}



💥 defaultOptions & queryCache & queryClient.setQueryDefaults()

defaultOptions

  • 모든 querymutation에 대해 기본 옵션을 지정할 수 있는 방식으로, 주로 query 또는 mutation에 기본적으로 적용되는 로직을 전역으로 설정할 때 사용됨.
  • QueryClient 인스턴스를 생성할 때 초기 기본 옵션을 설정하며, 불변(immutable)한 속성임.(queryClient.setDefaultOptions()와 비교해봐)
  • 쿼리(queries)와 뮤테이션(mutations)을 별도로 분리해서 각기 다른 옵션을 줄 수 있음.
  • 🟡 queries에서 사용 가능한 핸들러는 onSuccess, onError, onMutate
    => onSettleduseQuery에만 제공되는 옵션임
    => defaultOptions.queries에서 직접 사용 불가.
    => 쓰려면 queryCache를 추가해서 해.
  • 사용되는 모든 mutation에 loading 페이지를 추가해야 했고, 일일이 요청 완료시 처리를 써주면 같은 코드를 여기저기 계속 쓰게 되니까 한꺼번에 처리해주기 위해 아래와 같이 작성했다.
 defaultOptions: {
    queries: {
      // 🌳 옵션들은 개별 쿼리와 전역 쿼리에서 모두 설정 가능.
      
      placeholderData: keepPreviousData,
        	// 데이터를 받기 전까지 어떤 값을 보여줄 것인지.
            // (쿼리에서 isPlaceholderData return 필요함.)
        	// 값 :
        	// 1) keepPreviousData : 이전 데이터를 placeholder로 사용.
        	// 2) defaultData : 사용자가 지정한 "리턴값과 동일한 구조"의 정적 데이터. 
            // 3) (previousData) => previousData : 함수로 동적 설정.
        		// 3-1) 동적설정 예시 : 
        		placeholderData: (previousData) => {
                  	// 이전 데이터 없으면 defaultData로 설정.
  					if (!previousData) return defaultData;			
                  	// 이전 데이터를 기반으로 새로운 상태 생성.
  					return {
      					...previousData,
      					content: previousData.content.map(item => ({
        					...item,
        					isUpdating: true
      					}))
    				};
				}
        
      // 데이터 신선도 관리 관련 -----------
      // staleTime과 gcTime은 독립적으로 작동함.
      staleTime: 0,
        	 // 데이터가 즉시 'stale(신선한)' 상태 = 다음 렌더링 시 항상 refetch를 고려.
        	 // 0이면 - 매 요청시 새 데이터 패치 + 항상 최신 데이터 유지 + 네트워크 요청 많음.
        	 // 시간 지정하면 - 지정 시간동안 캐시 데이터 사용 + 네트워크 요청 감소 + 데이터 지연 가능성.
        	 // 자주 변경되면 기본값 0
        	 // 자주 변경되지 않으면 5분정도
        	 // 거의 변경되지 않는다면 24시간으로 해주는게 평균적임.
      gcTime: 1000 * 60 * 5,
      		// 캐시 보관 기간, 기본값은 5분.
      		// 쿼리가 비활성화 되면 gcTime 시작됨. - 만료시 캐시 삭제
        	// 자주 변경되지 않으면 30분 정도
        	// 거의 변경되지 않는다면 7일으로 해주는게 평균적임.
      		// 영구유지 = Infinity 
        			  // 장점 : 재요청 감소, 빠른 데이터 접근, 네트워크 트래픽 감소
        			  // 단점 : 메모리 사용량 증가, 오래된 데이터 누적, 메모리 누수 가능성.
         	// 사용시 고려할 점 : 
        	   // 데이터의 변경 빈도+중요도+신선도 / 메모리 사용량 / 네트워크 요청 최적화 / UX
        
      // 네트워크 관련 -----------
      retry: 0, // 요청 재시도 횟수
        		// axios API요청에서 Retry 횟수만큼 에러 발생시 쿼리에서 에러로 넘김
        	   // => 재시도 횟수 채워지기 전까지는 에러 X, pending 상태 O
      retryDelay: 3000, // 재시도 간격 (3초설정)
      networkMode: 'online', // 기본값 online
        	// 👉 네트워크 상태에 따라 쿼리 실행 방식을 제어하는 모드.
        	// 1) 'online'
        		- '네트워크가 연결된 상태'에서만 쿼리가 실행
                - '연결 끊기면 쿼리 중단' & 오류 반환 (재연결시 다시 실행).
        		- '항상 최신 데이터가 필요하고, 네트워크가 반드시 필요할 때' 유용
            // 2) 'always'
        		- '네트워크 상태에 상관없이' 항상 쿼리 실행
                - 네트워크 '끊겨도 캐시된 데이터가 있다면 활용'
                - 네트워크 복구시 = '즉시 재시도하여 최신 데이터 가져옴'
                - '모바일 or 오프라인 지원''실시간으로 동기화'가 중요한 애플리케이션에서 활용하기 좋음. 
                - '네트워크가 불안정해도' 캐시를 우선으로 동작하도록 할 수 있음.
            // 3) 'offlineFirst'
                - '네트워크 없으면 = 쿼리 요청 아예 X'
      				- 캐시된 데이터 있으면 그것을 제공함.
                - 네트워크 복구시 = '네트워크 연결 시에만 요청'
                - '네트워크 연결 시 최신 데이터로 동기화'.
                - 네트워크가 불안정해도 원활한 데이터 접근이 필요할 때 사용. ex) 오프라인 지원 필요 (캐시된 데이터 보여주고 필요할 때만 요청 시도하는 경우)
                - 캐시에서 우선 데이터를 제공 = '빠른 응답 보장'
      			- 네트워크 제약이 많아 캐시 우선으로 사용하는 앱에서 활용.
         	 'always', 'offlineFirst'가 비슷하게 보이는데, 
           		- 오프라인 상태에서 캐시된 데이터를 사용하는 방식과 네트워크가 연결된 순간 데이터를 어떻게 갱신하는지가 다름.
                

                
                
                
               
      
      // 리페칭 관련 -----------
      refetchOnWindowFocus: false, // 윈도우가 다시 포커스되었을때 데이터를 호출할 것인지
      refetchOnReconnect: false, // 네트워크 재연결 시 자동 refetch 여부
      refetchOnMount: false, // 컴포넌트 마운트 시 자동 refeth 여부
        
      suspense: true // Suspense 모드 설정
      	// 전역 or 특정 컴포 사용 가능.
      	// 👉 데이터를 가져오는 동안 로딩 상태를 제공
      	// 👉 사용시 아래처럼 사용할 컴포넌트(App에서 전역도 가능.(provider 안쪽에))
      	// 한 파일 내에서 여러 Suspense 사용 가능.
      	<Suspense fallback={<HeaderSkeleton />}>
        	<Header /> 
      	</Suspense>
      
      useErrorBoundary: true
      	// 전역 or 특정 컴포 사용 가능.
      	// 비동기 쿼리 중 오류 발생 시 Error Boundary로 전파
      	// 쿼리 재시도 시 ErrorBoundary 보여지며 재시도 됨.
      	// 컴포넌트 트리에서 특정 컴포넌트가 오류를 발생할 경우, 트리 전체가 중단되지 않고 해당 오류를 지정된 Error Boundary에서 처리하게 해주는 React 기능
      	// 👉 적절한 오류 메시지나 대체 UI를 출력 = UX 굿 + 안정적 + 예측 가능 오류처리 제공
      	// 👉 사용시 아래처럼 사용할 컴포넌트를 ErrorBoundary로 감싸줘.
      		<ErrorBoundary fallback={<ErrorMessage />}>
      			<Suspense fallback={<LoadingSpinner />}>
        			<PlaceList />
      			</Suspense>
    		</ErrorBoundary>
      
      
      select: (data) => data.name(예시), // 특정 필드만 선택함.
              // 메모이제이션된 선택자를 값으로 사용 가능함.
        	  // React.useCallback( (data) => data.선택값, [] )
				const selectPlaceNames = React.useCallback((data) => {
  					return data.places.map(place => place.name);
				}, []);

				const { data: placeNames } = useQuery({
                  queryKey:~ ,
                  queryFn: ~,
                  ...
                  select: selectPlaceNames
				});
      

      meta: {
        // v4에서 추가된 메타데이터 옵션 (mutation도 사용 가능)
        // 각 쿼리에 대한 '추가적인 메타데이터를 저장'할 수 있음.
        errorMessage: '쿼리 불러오기 실패ㅠㅠ',
        successMessage: '쿼리 불러오기 성공~~!'
      }
      	// UX 굿(성공/실패 상태에 따른 피드백)
      	// 쿼리 실행에 필요한 부가정보(ex, 우선순위, 분류 등)를 meta에 포함할 수 있음.
      	// 쿼리 종류별로 조건 나누거나, 로깅이나 에러 추척 시 필요 정보를 meta에 저장하고 활용 가능.
      	// 사용 예시
      		 onSuccess: (data, variables, context) => {
    			alert(mutation.meta.successMessage);
  			 },
            onError: (error, variables, context) => {
              alert(mutation.meta.errorMessage);
            }
},

queryCache

  • 쿼리 캐시의 저장소역할을 하며, 이를 통해 전역적으로 발생하는 이벤트에 대한 핸들링이 가능함.
  • 주로 쿼리 관련 이벤트(onError, onSuccess, onSettled)에 사용되며, 캐시된 쿼리들에 대한 이벤트를 관리함.
  • 특정 쿼리 캐시가 변할 때, 그 변화를 전역적으로 감지하고 처리할 수 있음.
  • 전역적으로 캐시 이벤트 추적 : 캐시에 저장된 모든 쿼리에 대해 에러나 성공을 추적할 수 있음.
  • 처음에 queryCache도 전역 이벤트 가능이라고만 생각하고 아래와 같이 작성했었다.
const queryCache = new QueryCache({
    onError: (error) => {
      console.error('🚨', error);
    },
    onSuccess: () => {
      setLoadingStore(false);
    },
    onSettled: () => {},
  });
 const queryClient = new QueryClient({queryCache})
  • 이렇게 queryCache에 적용하는 방법은 queryCache가 query에 대한 이벤트 처리가 중심이기 때문에, mutation과 관련된 전역 이벤트를 처리하는 데는 적합하지 않다고 함.
    -> 나는 mutation의 요청들에 주로 적용하려 했기 때문에 실행되지 않았음.ㅎㅎ;; 그래서 처음의 방법대로 queryClient 안에 defaultOptions를 작성해서 처리했다.
    • 결론
      • 특정 queryCache에 대한 전역적인 관리 필요할 때 = queryCache
      • mutationquery 둘 다 관리할 때 = defaultOptions
  const queryCache = new QueryCache({
    onError: (error: Error) => {
      console.error('🚨 ', error);
    },
    onSettled: () => {
      setLoadingStore(false);
    },
  });
  const queryClient = new QueryClient({
    queryCache,
    defaultOptions: {
      mutations: {
        onMutate: () => {
          setLoadingStore(true);
        },
        onSuccess: () => {},
        onError: (error: Error) => {
          console.error('🚨', error);
        },
        onSettled: () => {
          setLoadingStore(false);
        },
      },
    },
  });

🍋 queryClient.setQueryDefaults()

  • 모든 query에 대한 기본 옵션을 설정하는 메서드
  • React Query v3 및 이후 버전에서는 defaultOptions.queries에 onError, onSuccess, onSettled를 직접 사용할 수 없음
  • 두 개의 인자 필요 : 첫 번째는 쿼리 키(queryKey), 두 번째는 옵션 객체.
queryClient.setQueryDefaults({
  onError: (error: unknown) => {
    console.error('🚨 Global query error:', error);
  }
});

🍋 queryClient.setDefaultOptions()

  • 모든 쿼리와 뮤테이션에 대한 전역 기본 옵션을 설정
  • QueryClient 인스턴스가 생성된 후에 기본 옵션을 동적으로 변경할 수 있게 해주는 메서드이며 이 메서드를 호출하면 기존 옵션과 병합됨.
// setQeuryDefaults와 setDefaultOptions 사용법 비교.
queryClient.setQueryDefaults(['todos'], {
  staleTime: 5 * 60 * 1000,
  gcTime: 10 * 60 * 1000, 
});

// setDefaultOptions 예시
queryClient.setDefaultOptions({
  queries: {
    staleTime: 5 * 60 * 1000,
    gcTime: 10 * 60 * 1000,
  },
  mutations: {
    retry: 3,
  },
});

💥 DefaultOpions로 queries에 onError를 설정해도 똑같은 에러가 발생한다. 좀 더 찾아봐야겠다.



🍋 쿼리 결과 객체 종류

  1. isLoading : 이미 데이터가 있는 상태에서 백그라운드 리프레시 중일 때 true. (캐시된 데이터가 있지만 다시 데이터를 가져오는 중일 때 true) => isPending으로 변경 됨.(v5)

  2. isPending : 새로운 데이터 로딩중, 아직 데이터를 한 번도 불러오지 않은 상태, 캐시된 데이터가 없을 때 true 👉 v4 신규, isLoading 대체용으로 권장함.

  3. isError : 쿼리 오류 발생.
    2-1. error : if(isError){error 객체를 통해 오류 발생시킴}

  4. isSuccess : 쿼리 성공, 데이터 사용 가능.

  5. isIdle : 쿼리 비활성화 상태.

  6. isFetching : 백그라운드 리패칭 중

  7. isPlaceholderData : placeholder 데이터 사용 중인지

  8. isRefetchError : 리패칭 중 에러 발생

  9. isLoadingError : 초기 로딩 중 에러 발생

  10. data : if(쿼리 === success) { data를 통해 데이터 사용 가능.}


🍋 주요 쿼리 옵션

  • enabled : true 값으로 설정하면 => 쿼리 함수가 자동으로 요청되지 않음.
  • placeholderData : 쿼리 키에 페이지 정보를 포함했을 때, 쿼리 키가 변경되었더라도 새 데이터가 요청되는 동안 마지막으로 성공적으로 가져온 기존 데이터를 유지 사용함.
    - 이전 버전에서의 keepPreviousData과 동일함.
    - 새 데이터 로딩 중 화면이 깜빡이는 것을 방지 (UX에 굿)
    - 페이지네이션이나 사용자 데이터를 조건에 따라 업데이트할 때 유용함.
    • tanstackQuery를 실사용해보니 거의 대부분의 경우 사용되는 기능이다.
const {data} = useQuery("getUserData", getUserDataAPI, {
  enabled: !!userData,       // userData가 있을 때만 쿼리 실행
  placeholderData: keepPreviousData,    // 쿼리가 다시 실행될 때 이전 데이터를 유지(이전 데이터를 플레이스홀더로 사용)
})

🍋 queryKey

  • 문자열 : 구별되는 문자열로 키를 줄 수 O. 인자가 1개인 배열로 convert됨.
  • 배열 : 문자열과 함께 숫자를 주면 같은 문자열로 같은 key를 쓰면서도 id로도 구별 가능함. (꼭 배열로 전달해야 함.)
  • 뭐리 기능이 변수에 의존하는 경우, 쿼리 키에 포함됨.
// 문자열 예시 ) queryKey === ['todo', 5, {preview : true}]
useQuery(['todo', 5, {preview : true}], ...)
         
const Todos = ()=>{
  	const result = useQUery(['todos', todoId], ()=> getTodoList(todoId)) // => 🟠todoId 바뀔 때마다 재호출
}

🍋 Query Functions

  • 쿼리 함수는 Promise를 리턴하는 함수의 형태를 가짐.
useQuery(['todos'], getAllTodoList)
useQuery(['todos', todoId], ()=> getTodoListById(todoId))
useQuery(['todos', todoId], async () => {
	const data = await getTodoListById(todoId)
    return data
})
useQuery(['todos', todoId], ({ queryKey }) => getTodoListById(queryKey[1]))

// 에러 처리 시 
const { error } = useQuery(['todos', todoId], async ()=>{
	if(todoError){
    	throw new Error("비상~~ 이멀전시!! 이멀전씨~!!!!") // 무조건 throw 통해서 예외처리
    }
 	return data
})

🍋 Query getData

  • App 최상단의 Provider를 통해 Context API와 유사한 형태로 상태에 접근 가능
  • getQueryClient에 unique key를 전달하면 해당 key로 생성된 쿼리데이터를 로드 가능함.
// ========== 전체 유저 목록 가져오기 hook
const useUserList = () => {
  return useQuery(["userList"], async () => {
    // 'userList' 👉 쿼리 키. 해당 데이터를 캐시할 때 사용 함.
    
    const { data } = await axios.get("https://dummy.API.com/userList");
    // 👉 이 함수가 호출되면 'userList' 키에 해당하는 데이터가 쿼리 클라이언트에 캐시됨.
    return data;
  });
}

const UserList = ( setUserId ) => {
  const queryClient = useQueryClient(); // 👉 react-query의 쿼리 클라이언트를 사용하는 훅
  const { status, data, error, isFetching } = useUserList();
  // 👉 useUserList 훅을 호출해서 전체 유저 목록을 불러옴.

  return (
    <>
    	<User></User>
    </>
  );
}


// ========== 특정 유저 데이터 가져오기 hook
const useUser = (userId) => {
  return useQuery(["user", userId], () => getUser(userId), {
    enabled: !!userId,
  });
  // 👉 user마다 다른 userId를 가짐 = 유니크한 캐시 키가 됨
}

const User = ( userId, setUserId ) => {
  const { status, data, error, isFetching } = useUser(userId);
  const queryClient = useQueryClient();
  console.log(queryClient.getQueryData(['userList'])); 
	// 👉 queryClient로 캐시된 userList쿼리 데이터 가져옴.
  	// 👉 타 컴포넌트에서 요청된 쿼리도 getQueryData를 통해 접근가능

  return (
    <div>
      ...
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

💥 Error 발생) react-query, Type ‘string[]’ has no properties in common with type ‘InvalidateQueryFilters’

...
queryClient.invalidateQueries(['myQueryKey']);

이렇게 했더니 
Type 'string[]' has no properties in common with type 'InvalidateQueryFilters'.
이러한 에러가 발생했다. // 배열로 넣기만 하믄 되는 줄 ..알앗지..

queryClient.invalidateQueries({ queryKey: ['myQueryKey'] }) 


...







🍋 병렬 쿼리

  • React Query에서 여러 개의 데이터를 동시에 가져올 때 사용하는 방법
    - 쿼리가 여러개 선언된 상황 = 쿼리 함수는 병렬로 요청되어 처리됨.
    • 요청 동시 실행 = 전체 데이터를 더 빠르게 get

👉 수동 병렬 쿼리

const App = ()=>{
	const userDataQuery = useQuery('userData', getUserData)
  	const useGroupQuery = useQuery('group', getGroup)
    const useLGTwinsQUery = useQuery('LGTwins', getLGTwins)
    ...
}

👉 동적 병렬 쿼리

const App = () => {
	const KBOQueries = useQueries( // 👉 렌더링이 계속 되는 사이에 쿼리가 계속 수행되어야 할 경우 사용.
    	KBOList.map(kbo =>{
        	return {
            	queryKey : ['team', team.id],
              	queryFn : () => getMyTeam(team.id),
            }
        })
    )
}







🌿 Mutations

  • 수동적으로 데이터 페칭 시
  • create, update, delete 동작을 수행함.
  • Server State에 사이드 이펙트 발생시키는 경우 사용
  • onSuccess, onError, onSettled : useMutation 정의할 때와 mutate에서 모두 사용 가능.
    • 비동기 작업을 처리하는 코드가 더 간결해짐.
    • 별도의 try-catch-finally 구조 없이도 성공, 실패, 완료 시의 처리를 설정할 수 있음.
    • onSettled와 같은 콜백을 사용하여 요청 완료 후에 항상 실행해야 하는 로직을 쉽게 관리할 수 있음.
  • 🌱 mutationsFn
    • promise 처리가 이루어지는 mutation function으로, axios를 이용해 API 요청하는 부분
  • 🌱 mutate
    • useMutation을 정의해준 후, 이벤트 발생 시 사용
    • useMutation을 이용해 작성한 내용들이 실제로 실행될 수 있도록 돕는 trigger 역할
  • 속성과 작성 예시
 mutations: {
      	onMutate: () => { setLoadingStore(true)}, // 원하는 작업 추가.(아래 예시)
          - mutation이 시작되기 전에 호출
          - '낙관적 업데이트'를 처리하거나, '로딩 상태를 표시'하는 등의 작업을 수행할 때 유용
     	  - API Call 전에 실행되는 함수
     	  - 성공 시 현재 데이터 캐시를 업데이트하거나 UI를 변경 
          - 실패 시 사용할 수 있는 rollback 매커니즘도 제공
     	  - onMutate callback 함수에서 UI를 업데이트		
          - setQueryData 함수로 이전 데이터 업데이트

        onMutate: (variables) => { console.log('계좌 생성 시작:', variables)},
        onMutate: async (newPlace) => { // 이전 쿼리 데이터 백업
    		const previousPlaces = queryClient.getQueryData(['places']);
    
    		// 낙관적으로 새 데이터 업데이트
   			 queryClient.setQueryData(
               	['places'], (old: Place[]) => [...old, newPlace]
             );
     		return { previousPlaces };
  		},
        
          
        // 타입까지 고려해 더 안전한 낙관적 업데이트
        onMutate: async (newPlace: Place) => {
          await queryClient.cancelQueries({ queryKey: ['places'] });
          const previousPlaces = queryClient.getQueryData<Place[]>(['places']);

          if (previousPlaces) {
            queryClient.setQueryData<Place[]>(['places'], old => 
              old?.map(place => 
                place.id === newPlace.id ? newPlace : place
              )
            );
          }

          return { previousPlaces };
        },
          
      
      
        retry: 1, //재시도
        retryDelay: 기본값은 1000ms // 재시도 간격 설정
        	(attempt) => attempt * 1000, //재시도 횟수에 따라 간격 증가
        onSuccess: (result, variables) => { // 성공 시 로직 설정
         		   // ex) 성공 시 캐시 업데이트
   					 queryClient.setQueryData(
                       ['places'], (old: Place[]) => old.map(place => 
        				place.id === variables.id ? result.data : place))},
                       
                       
        onError: (error) => { console.error('🚨', error)},
  		onError: (err, newPlace, context) => {
          			// 에러 발생 시 콜백함수에서 이전 데이터로 rollback
    				queryClient.setQueryData( 
                      	['places'],context.previousPlaces
                    )},
                      

        onSettled: () => {
          setLoadingStore(false); // 임의 추가
          // mutation이 요청 응답 오면 실행됨 (결과는 성공/실패 상관없음. 무조건 응답만 오면 실행.)
        },
          
         variables: { name: 'New Account' },
           // mutation에 전달할 데이터를 설정할 때 사용하는 옵션
           // 보통 mutate 함수 호출 시 인자로 직접 전달하지만, variables 옵션을 사용해도 좋음.
          
          
          
        useInfiniteMutation : // TanStack Query v4
      		-  👉'무한 스크롤'처럼 '특정 요청이 발생할 때마다 데이터를 추가적으로 가져오는 데' 사용됨 
            -  useMutation과 같은 방식으로 mutation 요청을 처리하면서도 자동으로 다음 페이지 데이터를 로드하고 데이터를 갱신하는 기능을 제공
            - 💥 현재 TanStack Query에서는 useInfiniteMutation이라는 훅이 제공되지 않음
            	-> 현재로써는 useMutation과 useInfiniteQuery를 조합해 구현하는 것이 일반적.
                	-> 무한 스크롤에서 데이터를 추가하거나 로드하는 동작은 useInfiniteQuery를 통해서 처리
                	-> 추가적인 수정이나 삭제 등과 같은 동작은 useMutation을 이용해 개별적으로 구현
                -> useInfiniteQuery : 서버에서 데이터를 여러 페이지로 나눠서 로드할 때, 각 페이지의 데이터를 불러와 한 번에 합쳐서 보여주는 기능을 제공
            // 사용 예시
            	// 무한 스크롤을 위한 데이터 가져오기 함수
                const fetchPlaces = async ({ pageParam = 1 }) => {
                  const response = await axios.get(`/api/places?page=${pageParam}`);
                  return response.data;
                };

                // 새로운 장소를 추가하는 함수
                const createPlace = async (newPlace) => {
                  const response = await axios.post(`/api/places`, newPlace);
                  return response.data;
                };

                const PlacesComponent = () => {
                  const queryClient = useQueryClient();

                  // 무한 스크롤로 장소 데이터를 가져오는 useInfiniteQuery
                  const {
                    data,
                    fetchNextPage,
                    hasNextPage,
                    isFetchingNextPage,
                  } = useInfiniteQuery({
                    queryKey: ['places'],
                    queryFn: fetchPlaces,
                    getNextPageParam: (lastPage, allPages) => {
                      return lastPage.nextPage ?? false; // 다음 페이지 정보 반환
                    }
                  });

                  // 새로운 장소 추가를 위한 useMutation
                  const mutation = useMutation(createPlace, {
                    onSuccess: () => {
                      queryClient.invalidateQueries({ queryKey: ['places'] }); // 캐시 무효화
                    }
                  });

                  const handleAddPlace = () => {
                    mutation.mutate({ name: '새로운 장소' });
                  };

                  return (
                    <div>
                      <button onClick={handleAddPlace}>장소 추가하기</button>

                      <div>
                        {data?.pages.map((page, pageIndex) => (
                          <React.Fragment key={pageIndex}>
                            {page.places.map((place) => (
                              <div key={place.id}>{place.name}</div>
                            ))}
                          </React.Fragment>
                        ))}
                      </div>

                      {hasNextPage && (
                        <button
                          onClick={() => fetchNextPage()}
                          disabled={isFetchingNextPage}
                        >
                          {isFetchingNextPage ? '로딩 중...' : '더 불러오기'}
                        </button>
                      )}
                    </div>
                  );
                };
                export default PlacesComponent;
      },
        
 
        
        
  // 아래처럼 컴포넌트단에서 직접 작성해도 됨.
  const useUserMutate = useMutation({
  	mutationFn : (user) => {
    	return api('/user', user)
    }
  })
  
  const userMutate = useUserMutate()
  
  return (
    <>
    	{userMutate ? (
        	<p>유저 정보 저장 중...</p>
        ):(
    		<>
    			{userMutate.isError ? <p>에러발생</p> : null}
    			{userMutate.isSuccess ? <p>유저 정보 저장 성공</p> : null}
                 <button
                 	onClick={()=>{
  						userMutate.mutate({name:'뚜뚜', age:3})
  					}}
                 >유저 저장하기</button>
                 
    		</>
    	)
        }
    </>
  )
  
  
  
  
 // 낙관적 업데이트 
  const updateTodoMutation = useMutation({
  mutationFn: updateTodo,
  // 실제 서버 요청 전 즉시 UI 업데이트
  onMutate: async (newTodo) => {
    // 기존 할 일 목록 쿼리 취소
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // 이전 할 일 상태 저장
    const previousTodos = queryClient.getQueryData(['todos'])

    // 즉시 낙관적 업데이트
    queryClient.setQueryData(['todos'], (old) => 
      old.map(todo => 
        todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
      )
    )

    // 롤백을 위해 이전 상태 반환
    return { previousTodos }
  },
  // 실패 시 롤백
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 성공 시 최종 동기화
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  }
})
        
        
        

const queryClient = useQueryClient(() => axios.post(`api/like/${id}`), {
	onMutate: async (id) => {
      // 'queryKey'로 진행 중인 refetch 취소하여 낙관적 업데이트를 덮어쓰지 않도록 함
		await queryClient.cancleQueries({
        	queryKey: ['queryKey]
        })
          
      // 이전 데이터를 받아옴
        const previousData = queryClient.getQueryData(['queryKey']);
      
      // 새로운 값으로 낙관적 업데이트
      	queryClient.setQueryData(['queryKey'], (prev) => !prev)
      
      	return { previousData }
    },
  // mutation이 실패한 경우
  onError: (err, newData, context) => {
    // onMutate로부터 반환된 context를 사용하여 rollback
    queryClient.setQueryData(['queryKey'], context.previousData)
  },
  onSettled: () => {
    // 성공, 실패 여부에 관계 없이 refetch
   	queryClient.invalidateQueries({queryKey: ['queryKey']}) 
  }
});

const deleteData = useMutation({...})
const teamNoteMutation = useMutation({
	🌱 mutationsFn : data => 
  		customAxios.createMyTeamNote(TypedJSON.stringify(data, TeamData), {
          // 데이터를 위 형식으로 변환.
        	headers : { "..." }
     	},
	}),
  	onSuccess : (data, variables, context) => {
  		console.log("요청 성공", data)
  	}, 
  	onError : (error, variables, context) => {
  		console.log("요청 에러", error)
  	},
  	onSettled : (data, error, variables, context) => {
    	console.log("응답 완료") // 👉 성공, 실패 여부 무관하며, '완료' 기준임
  	}
})

const deleteTeam = useMutation(() => axios.delete(`api/delete/${teamId}`));
const deleteTeam = useMutation({
  mutationFn: (id) => axios.delete(`api/delete/${teamId}`)
})


// 타 컴포넌트
...
const { mutate } = () => deleteTeam() 🌱 
const deleteFn = () => {
	deleteTeam.mutate(teamId)
}


// 응용1) fn key 값 생략
const deleteTeam = useMutation((teamId) => axios.delete(`api/delete/${teamId}`), {
	onSuccess: () => { console.log('팀 삭제 성공') },
  	onError: () => { console.error('팀 삭제 중 에러 발생') },
  	onSettled: () => { console.log('팀 삭제 요청 응답 받음') }
})

// 응용2) fn key 값 명시
const deleteTeam = useMutation({
  	🌱 mutationFn: (teamId) => axios.delete(`api/delete/${teamId}`),
	onSuccess: () => { console.log('팀 삭제 성공') },
  	onError: () => { console.error('팀 삭제 중 에러 발생') },
  	onSettled: () => { console.log('팀 삭제 요청 응답 받음') }
})



🍋 Mutation을 통해 서버에 새 데이터가 추가됐을 때

const queryClient = useQueryClient()
const mutation = useMutation(editMyTeam, { // 👉 팀정보 수정하는 mutation 생성
	onSuccess : updateData => queryClient.setQueryData(['team', {id : 'twins'}], updateData), 
  	// 👉 서버에서 팀 정보 수정 api 요청 성공하면 -> 로컬 캐시에서 해당 팀의 정보를 updateData로 업데이트함.
  	// 👉 setQueryData 아래 설명 참고.
})

mutation.mutate({
	id: 'twins',
  	name :  'LG Twins',
})

// 👉 mutation의 response 값으로 업데이트된 data 사용 가능함.
const { status, data, error } = useQuery(['team', {id: 'twins'}], getMyTeamById)




🍋 mutate와 mutateAsync

  • 에러처리 : 일반 비동기 함수 = try-catch 권장 / React Query useMutation= 내장 에러 핸들러 활용 권장
  • mutate
    • 콜백 기반의 명령형 호출 방식
    • 내부적으로 비동기 처리를 하지만, 호출자는 비동기 작업의 상태를 직접 기다릴 수 없음 (void를 반환함.)
      • 비동기 작업의 결과를 처리하기 위해 콜백 함수를 사용
      • 즉시 mutation을 트리거하며 반환 값이 없음
      • 성공/실패 핸들러를 인수로 전달
    • 사용 상황 :
      • 간단한 mutation
      • 즉시 실행 후 별도의 복잡한 후속 처리 불필요한 경우
      • 컴포넌트 내 간단한 상호작용
    • 장점 :
      • 간단하고 직관적인 사용법
      • 인라인으로 success/error 핸들링 가능 (onError 등의 핸들러 제공)
      • 컴포넌트 내에서 즉시 사용하기 좋음
      • 다른 비동기 함수와 연계하여 사용하기 좋음
	const {mutate : createUser} = usePostUser();

	const onSubmit = ()=>{
    	createUser(userParams, {
        	onSuccess : (data) =>{
            	console.log("등록 성공", data);
            }
          	onError : (error) => {
        		console.error("등록 실패", error);
        	}
        })
    }

  • mutateAsync
    • Promise 기반의 비동기 호출 방식 (Promise를 반환함.)
    • async/await 구문과 함께 사용 가능 > 가독성과 흐름 제어 간편
    • mutation의 결과를 직접 반환받아 처리 가능 (비동기 작업의 완료 또는 실패를 명시적으로 기다릴 수 있음)
    • try/catch 구문으로 결과 값에 대한 처리 가능. (onSuccess같은 핸들러 X)
    • 사용 상황 :
      • 복잡한 비동기 워크플로우
      • 여러 비동기 작업의 연계 필요
      • 명시적인 에러 핸들링 요구
      • async/await 패턴 선호
    • 장점 :
      • 더 세밀한 에러 핸들링
      • 동기식 코드 흐름 유지
      • 복잡한 비동기 로직 구현에 용이
      • 다른 비동기 함수와 연계하여 사용하기 좋음
	const {mutateAsync : createUser} = usePostUser();

	const onSubmit = async () => {
    	const response = await createUser(userParams);
        console.log("등록 성공", response)
    }

  • mutate와 mutateAsync의 차이점
    1. 에러 처리 방식
      • mutate: 콜백 함수 내에서 에러 처리
        • mutateAsync: try/catch 블록을 통한 에러 처리
      1. 코드 가독성
      • mutate: 콜백 중첩으로 인해 복잡해질 수 있음
        • mutateAsync: 플랫한 코드 구조 유지 가능
    2. 흐름 제어
      • mutate: 비동기 작업 이후의 로직을 콜백으로 처리
        • mutateAsync: await를 통해 순차적인 로직 구현 가능



  • mutate와 mutateAsync의 사용 예시
// mutate 
const handleSimpleUpdate = () => {
  userNameUpdateMutation({
    newUsername: 'simple',
    onSuccess: () => alert('업데이트 완료')
  });
};

// mutateAsync 
const handleComplexUpdate = async () => {
  try { 
    const updateResult = await userNameUpdateMutation({
      newUsername: 'complex'
    });

    // 추가 작업 수행
    await someOtherAsyncTask(updateResult);

    // 최종 처리
    alert('모든 작업 완료');
  } catch (error) {
    // 통합 에러 핸들링
    console.error('업데이트 중 오류 발생:', error);
  }
};




🍋 setQueryData

  • 특정 쿼리 데이터를 수동으로 업데이트 설정할 때 사용.
    - refetch를 일으켜 네트워크 요청을 기다릴 필요 없이 내가 원하는 데이터로 수동으로 변경 할 수 있음.
    - 사용자 입장에선 내가 원하는 화면이 이벤트 실행 직후 빠르게 반영되는것으로 보여짐 = gooooood!
  • API 요청이 성공적으로 완료된 후, 해당 쿼리의 데이터를 업데이트하고 싶을 때 사용




🍋 staleTime, gcTime

staleTime

  • fresh한 데이터가 stale한 데이터로 변경되는 데 걸리는 시간 (=데이터의 유통기한)
  • 기본값 0
    - 다른 옵션을 설정하지 않으면, 호출 즉시 stale 상태로 변함.
  • refetch가 일어나는 조건과 일치하게되면 데이터가 패치됨.

gcTime

  • 데이터가 inactive상태일 때를 기준으로 캐싱된 상태로 남아있는 시간
  • staleTime과 별개로 기준 시점으로부터 데이터의 삭제가 결정되고, gcTime이 지나면 가비지 콜렉터로 수집됨.





🌴 refetch가 일어나는 조건

  1. 1~3번까지의 세 기능은 기본값이 모두 true임.
  2. refetchOnMount : 마운트 될 때 (런타임에 stale인 특정 쿼리 인스턴스가 다시 만들어진 경우)
  3. refetchOnWindowFocus : window에 포커스 된 경우 (사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행할 것인지에 대한 옵션.)
  4. refetchOnReconnect : 네트워크가 재연결된 경우
  5. refetch interval이 있을 때
    • 요청 실패한 쿼리는 기본 값으로 백그라운드에서 3번 더 호출됨
    • retry, retryDelay 옵션으로 간격과 횟수 변경 가능.
  6. queryKey와 함께 State값을 같이 넘겨준 경우 State값이 변경된 경우





🍋 useInfiniteQuery

  • React Query에서 제공하는 GET방식의 데이터를 호출시 사용하는 함수
  • 무한 스크롤이나 페이징 기능 구현시 사용
  • 사용방법은 useQuery와 비슷하지만 차이점이 있음.
    1) 두 개의 배열 속성을 포함하는 객체 {pageParams: [], pages: []}로 반환
    2) 이전/다음 페이지를 가져오기 위한 fetchPreviousPage, fetchNextPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage 등의 속성으로 이전/다음 페이지가 있는지를 확인하고 이전/다음 페이지를 가져올때 확인 할 수 있음.
import { useInfiniteQuery } from '@tanstack/react-query';

useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
  ...options,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

🍋 hasNextPage를 좀 더 잘 사용해 보기

pageParam

useInfinityQuery를 사용하게 되면 기본적으로 pageParam이라는 값을 제공.
첫 페이지는 기본적으로 0 이라는 값이 제공되고, 이후에는 개발자가 계산하여 사용해야 함.

getNextPageParam

요청이 완료된 후 최신 응답 값으로 몇 페이지 인지 계산해서 반환해주는 옵션. 값을 반환을 해주지 않게되면 hasNextPage는 계속해서 undefined 상태이므로 활용을 할 수 없게 됨.

다음 페이지가 있다면 pageParam + 1을 해주고, 없다면 undefined를 반환해주면 됨 => hasNextPage의 값은 true 또는 false가 됨.

// hasNextPage 상태값에 따른 UI 컨트롤 응용 예시
import { useInfiniteQuery } from '@tanstack/react-query';
import { getTeamGoodsListAPI } from '@/apis/search';

const TeamGoodsListPage = () => {
    /* 팀 굿즈목록 조회 */
    const getTeamGoodsList = async ({ page = 1 }) => {
        const res = await getTeamGoodsListAPI({ ...params, page: page });

        if (res.status === 200) {
            const { count, goods } = res.data.result;
            const isLast = count / params.pageSize <= pageParam;

            return {
                count: count,
                goods: goods,
                nextPage: isLast ? undefined : pageParam + 1,
            };
        }
    };

    const { isLoading, data, hasNextPage, fetchNextPage, isFetchingNextPage } =
        useInfiniteQuery([queryKey, params], getTeamGoodsList, {
        staleTime: 60 * 1000,
        getNextPageParam: (lastPage) => lastPage.nextPage,
    });

    return (
        <>
            // 리스트 하단 더보기 버튼
            {hasNextPage && <ListMoreButton />}
        </>
    )
}





🌴 무효화 (Invalidation)

  • 데이터 자체를 무효화 시켜 리패치를 일어나게 함.
  • 쿼리의 캐시 데이터를 삭제하거나 업데이트하기 위해 사용됨.(useQuery에서 사용된 queryKey의 유효성을 제거해줄 목적으로 사용)
  • 쿼리의 데이터가 요청을 통해 서버에서 변경됐다면, 클라이언트에서 보유하고있는 데이터는 과거 데이터 = 쓸모 없어짐!!! 서버에서 다시 조회해!
    -> invalidateQueries 메소드 사용해서 쿼리를 즉시 stale(만료) 처리 후 리패칭 동작 시도함.
  • 쿼리 등록 시 특정 키가 공통 적용되어 있다면 한번에 invalidation 가능함.
  • mutation이 발생해서 성공한다면, mutation 내부에서 쿼리를 invalidate 처리해 주는 게 좋아용.
  • 무효화된 쿼리는 다음번에 해당 쿼리를 요청할 때 서버로부터 새로 데이터를 가져옴 => 데이터 최신상태 보장 굿굿!!
    = 불필요한 데이터 요청을 줄이고 성능을 최적화!
...
const queryClient = useQueryClient();

// 1. 캐시가 있는 모든 쿼리를 무효화 함.
queryClient.invalidateQueries();

// 2-1. 'team'으로 시작하는 모든 쿼리 무효화 > 넓은 범위의 쿼리 무효화
queryClient.invalidateQueries('team') 
//'team', 'team/twins', 'team/tigers'... 모두 무효화 됨
// => 필요 시 모든 관련 데이터를 새로 가져올 수 있음. 


// 2-2. team'으로 시작하는 모든 쿼리 무효화 > 특정 조건을 가진 쿼리들만 무효화
queryClient.invalidateQueries({
	predicate : query =>  
  		query.queryKey[0] === 'team' && query.queryKey[1]?.name === 'LG Twins',
  		// 쿼리 키의 첫 번째 요소가 'team'인지 확인.
  		// &&  쿼리 키의 두 번째 요소가 존재하고, name 속성 값이 'LG Twins'인지 확인.
  		//  👉 위 두 조건을 모두 만족하는 쿼리들만 무효화 됨.
  
  	// 👉 predicate 함수를 통해 주어진 조건을 만족하는 쿼리만 무효화 함.
    // predicate 함수는 각 쿼리에 대해 호출 되고, 쿼리의 queryKey를 검사함.
})

// 2-3 응용
const deleteTeam = useMutation((id) => axios.delete(`api/delete/${teamId}`), {
	onSuccess: () => { 
      console.log('요청 성공');
      // 요청 성공 시 해당 queryKey 유효성 제거
      queryClient.invalidateQueries('queryKey')
    },
  	...
})
  
const { mutate } = useMutation(updateGoods, {
  onSuccess: () => queryClient.invalidateQuries(['goods-list'])
})
// 👉 => 전체 쿼리 데이터의 효율적인 관리 가능





🟡 react-query-devtools

... 작성중 ...


참고0 (docs)
참고1
참고2

profile
Front-end

0개의 댓글