TanStack Query v4 기준으로 수정된 글입니다.
yarn add @tanstack/react-query @tanstack/react-query-devtools
gcTime
동안 캐시된 데이터 유지됨.gcTime
이 지나면 캐시에서 제거됨.
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}>
...
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
- 모든
query
와mutation
에 대해 기본 옵션을 지정할 수 있는 방식으로, 주로query
또는mutation
에 기본적으로 적용되는 로직을 전역으로 설정할 때 사용됨.- QueryClient 인스턴스를 생성할 때 초기 기본 옵션을 설정하며, 불변(immutable)한 속성임.(queryClient.setDefaultOptions()와 비교해봐)
- 쿼리(
queries
)와 뮤테이션(mutations
)을 별도로 분리해서 각기 다른 옵션을 줄 수 있음.- 🟡
queries
에서 사용 가능한 핸들러는onSuccess
,onError
,onMutate
=>onSettled
는useQuery
에만 제공되는 옵션임
=>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)에 사용되며, 캐시된 쿼리들에 대한 이벤트를 관리함.
- 특정 쿼리 캐시가 변할 때, 그 변화를 전역적으로 감지하고 처리할 수 있음.
- 전역적으로 캐시 이벤트 추적 : 캐시에 저장된 모든 쿼리에 대해 에러나 성공을 추적할 수 있음.
const queryCache = new QueryCache({
onError: (error) => {
console.error('🚨', error);
},
onSuccess: () => {
setLoadingStore(false);
},
onSettled: () => {},
});
const queryClient = new QueryClient({queryCache})
queryCache
에 적용하는 방법은 queryCache가 query
에 대한 이벤트 처리가 중심이기 때문에, mutation
과 관련된 전역 이벤트를 처리하는 데는 적합하지 않다고 함.
- 결론
- 특정
queryCache
에 대한 전역적인 관리 필요할 때 =queryCache
mutation
과query
둘 다 관리할 때 =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를 설정해도 똑같은 에러가 발생한다. 좀 더 찾아봐야겠다.
isLoading : 이미 데이터가 있는 상태에서 백그라운드 리프레시 중일 때 true. (캐시된 데이터가 있지만 다시 데이터를 가져오는 중일 때 true) => isPending으로 변경 됨.(v5)
isPending : 새로운 데이터 로딩중, 아직 데이터를 한 번도 불러오지 않은 상태, 캐시된 데이터가 없을 때 true 👉 v4 신규, isLoading 대체용으로 권장함.
isError : 쿼리 오류 발생.
2-1. error : if(isError){error 객체를 통해 오류 발생시킴}
isSuccess : 쿼리 성공, 데이터 사용 가능.
isIdle : 쿼리 비활성화 상태.
isFetching : 백그라운드 리패칭 중
isPlaceholderData : placeholder 데이터 사용 중인지
isRefetchError : 리패칭 중 에러 발생
isLoadingError : 초기 로딩 중 에러 발생
data : if(쿼리 === success) { data를 통해 데이터 사용 가능.}
placeholderData
: 쿼리 키에 페이지 정보를 포함했을 때, 쿼리 키가 변경되었더라도 새 데이터가 요청되는 동안 마지막으로 성공적으로 가져온 기존 데이터를 유지 사용함.keepPreviousData
과 동일함.const {data} = useQuery("getUserData", getUserDataAPI, {
enabled: !!userData, // userData가 있을 때만 쿼리 실행
placeholderData: keepPreviousData, // 쿼리가 다시 실행될 때 이전 데이터를 유지(이전 데이터를 플레이스홀더로 사용)
})
// 문자열 예시 ) queryKey === ['todo', 5, {preview : true}]
useQuery(['todo', 5, {preview : true}], ...)
const Todos = ()=>{
const result = useQUery(['todos', todoId], ()=> getTodoList(todoId)) // => 🟠todoId 바뀔 때마다 재호출
}
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
})
// ========== 전체 유저 목록 가져오기 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);
...
queryClient.invalidateQueries(['myQueryKey']);
이렇게 했더니
Type 'string[]' has no properties in common with type 'InvalidateQueryFilters'.
이러한 에러가 발생했다. // 배열로 넣기만 하믄 되는 줄 ..알앗지..
queryClient.invalidateQueries({ queryKey: ['myQueryKey'] })
...
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: {
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('팀 삭제 요청 응답 받음') }
})
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)
const {mutate : createUser} = usePostUser();
const onSubmit = ()=>{
createUser(userParams, {
onSuccess : (data) =>{
console.log("등록 성공", data);
}
onError : (error) => {
console.error("등록 실패", error);
}
})
}
const {mutateAsync : createUser} = usePostUser();
const onSubmit = async () => {
const response = await createUser(userParams);
console.log("등록 성공", response)
}
// 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);
}
};
staleTime
- fresh한 데이터가 stale한 데이터로 변경되는 데 걸리는 시간 (=데이터의 유통기한)
- 기본값 0
- 다른 옵션을 설정하지 않으면, 호출 즉시 stale 상태로 변함.- refetch가 일어나는 조건과 일치하게되면 데이터가 패치됨.
gcTime
- 데이터가 inactive상태일 때를 기준으로 캐싱된 상태로 남아있는 시간
- staleTime과 별개로 기준 시점으로부터 데이터의 삭제가 결정되고,
gcTime
이 지나면 가비지 콜렉터로 수집됨.
import { useInfiniteQuery } from '@tanstack/react-query';
useInfiniteQuery({
queryKey,
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
useInfinityQuery를 사용하게 되면 기본적으로 pageParam이라는 값을 제공.
첫 페이지는 기본적으로 0 이라는 값이 제공되고, 이후에는 개발자가 계산하여 사용해야 함.
요청이 완료된 후 최신 응답 값으로 몇 페이지 인지 계산해서 반환해주는 옵션. 값을 반환을 해주지 않게되면 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 />}
</>
)
}
...
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
... 작성중 ...