안녕하세요. J4J입니다.
이번 포스팅은 react-query v5 변경점 알아보기 첫 번째인 공통 기능 변경에 대해 적어보는 시간을 가져보려고 합니다.
들어가기에 앞서
최근 react-query의 major 버전이 변경되어 5 버전이 등장하게 되었습니다.
react-query 공식 문서를 확인해 보면 5 버전이 등장하면서 변경된 다양한 것들을 확인해 볼 수 있습니다.
이 중 주요 변경점들에 대해 서로 관련 있는 것들끼리 묶어 정리를 해보려고 합니다.
가장 먼저 공통 기능 변경점에 대해 적어보겠습니다.
Options 설정 방법 변경
useQuery, useMutate 등을 사용할 때 options 설정을 하는 방법이 변경되었습니다.
먼저 5버전 이전까지는 options를 설정할 때 다음과 같이 object를 활용하여 설정하거나 parameter값을 나열하여 설정할 수 있었습니다.
// prev react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function Options() {
// (1) not use object
const res = useQuery<string[]>(['strings'], async () => (await axios.get('http://localhost:8080/strings')).data);
// (2) use object
// const res = useQuery<string[]>({
// queryKey: ['strings'],
// queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
// });
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
5버전 이후부터는 options를 설정할 때 object만 사용할 수 있도록 변경되었습니다.
그래서 기존에 parameter 값을 나열하여 설정되던 방식은 에러가 발생되고 object만을 활용한 설정 방식만이 올바르게 동작합니다.
// react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function Options() {
// (1) only use object
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
});
// (2) not use object error!!
// const res = useQuery<string[]>(['strings'], async () => (await axios.get('http://localhost:8080/strings')).data);
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
cacheTime 설정 방법 변경
cacheTime은 key에 매핑된 캐싱된 데이터가 GC에 의해 수집되는 것을 의미합니다.
처음 react-query에 대해 공부를 할 때 cacheTime은 key에 매핑된 데이터가 캐싱되는 시간을 의미하는 것으로 생각하고 있었습니다.
그리고 공식 문서에서도 다음과 같이 많은 사람들이 저와 같이 생각했었다고 얘기해주고 있었습니다.
Almost everyone gets cacheTime wrong. It sounds like "the amount of time that data is cached for", but that is not correct.
사실 cacheTime은 query 데이터가 더 이상 사용되지 않는 순간부터 시간이 시작되며, 해당 시간이 cacheTime의 설정 값을 넘어갈 때 GC에 의해 수집되어 캐시가 커지는 것을 방지하도록 도와주는 것입니다.
즉, query 결과를 활용하여 사용자들에게 캐싱된 데이터를 보여주는 것과 전혀 관련이 없다는 소립니다. (staleTime만 연관)
react-query에서는 이처럼 단어로 인해 발생되는 잘못된 이해를 바로잡고자 하는 것으로 보입니다.
그래서 cacheTime의 설정 방법을 gcTime이라는 더 명확한 새로운 이름으로 변경하여 설정하도록 수정했습니다.
코드로 살펴보면 5버전 이전에는 다음과 같이 cacheTime을 설정한 것을 볼 수 있습니다.
// prev react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function CacheTime() {
// if want to set cacheTime, use cacheTime
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
cacheTime: 30000, // set cacheTime
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
하지만 5버전 이후부터는 다음과 같이 cacheTime (=gcTime)을 설정하는 것을 볼 수 있습니다.
// react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function CacheTime() {
// if want to set cacheTime, use gcTime
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
gcTime: 30000, // set cacheTime
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
상태별 Status값 변경
5버전 이전에서 확인해 볼 수 있는 statue 값은 크게 다음과 같이 있습니다.
- loading
- fetchIng
- initialLoading
먼저 loading은 key값에 캐싱되어 있는 데이터가 없을 때 설정되는 status 값을 의미합니다.
fetching은 query function에 설정된 fetch 요청을 보냈지만 아직 응답이 오지 않은 경우의 status 값을 의미합니다.
initialLoading은 loading과 fetching이 모두 해당될 때의 status 값을 의미하며, 일반적으로 key값에 캐싱되어 있는 데이터도 없고 API를 호출해야 되는 상황인 처음 페이지가 마운트 될 때 볼 수 있는 상태입니다.
그리고 이들에 대한 상태 값을 코드에서는 다음과 같이 확인해 볼 수 있습니다.
// prev react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function Status() {
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
});
return (
<main>
<div>
<h2>Strings</h2>
{/* 캐시된 데이터가 없는 경우 : isLoading */}
<p>isLoading: {String(res.isLoading)}</p>
{/* API 호출이 응답되지 않은 경우 : isFetching */}
<p>isFetching: {String(res.isFetching)}</p>
{/* isLoading && isFetching의 결과 : isInitialLoading */}
<p>isInitialLoading: {String(res.isInitialLoading)}</p>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
5버전 이후부터는 status 값의 이름들이 변경되며 확인할 수 있는 status들은 다음과 같습니다.
- pending (= 5버전 이전 loading)
- fetching (= 5버전 이전)
- loading (= 5버전 이전 initialLoading)
상태 값 자체의 기능이 변경된 것은 없으며 단순하게 이름만 변경되었습니다.
loading→ pending으로, initialLoading → loading으로, fetching은 동일합니다.
그래서 5버전 이전과 동일한 코드를 5버전 이후에서는 다음과 같이 작성할 수 있습니다.
// react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function Status() {
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
});
return (
<main>
<div>
<h2>Strings</h2>
{/* 캐시된 데이터가 없는 경우 : isPending */}
<p>isPending: {String(res.isPending)}</p>
{/* API 호출이 응답되지 않은 경우 : isFetching */}
<p>isFetching: {String(res.isFetching)}</p>
{/* isPending && isFetching의 결과 : isLoading */}
<p>isLoading: {String(res.isLoading)}</p>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
useErrorBoundary 설정 방법 변경
5버전 이전에 사용할 수 있었던 useErrorBoundary는 에러가 발생되었을 때 ErrorBoundary 컴포넌트에 에러를 전달하여 fallback이 보일 수 있도록 도와주는 설정입니다.
여기서 use가 prefix로 되어 있는 것은 보통 hook에 많이 사용되는데 useErrorBoundary도 hook으로 생각하는 상황을 피하고자 5버전 이후에 throwOnError로 이름을 변경했습니다.
5버전 이전에는 코드를 다음과 같이 작성할 수 있었습니다.
// prev react-query-v5
import { useQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function ThrowOnError() {
// use useErrorBoundary for throw error
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => {
throw 'force exception';
},
useErrorBoundary: true, // set useErrorBoundary
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
export default function ErrorBoundaryThrowOnError() {
return (
<ErrorBoundary fallback={<p>ErrorBoundary Error...</p>}>
<ThrowOnError />
</ErrorBoundary>
);
}
하지만 5버전 이후부터는 useErrorBoundary 대신 다음과 같이 throwOnError 값으로 사용할 수 있습니다.
// react-query-v5
import { useQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function ThrowOnError() {
// use throwOnError for throw error
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => {
throw 'force exception';
},
throwOnError: true, // set useErrorBoundary
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
export default function ErrorBoundaryThrowOnError() {
return (
<ErrorBoundary fallback={<p>ErrorBoundary Error...</p>}>
<ThrowOnError />
</ErrorBoundary>
);
}
TypeScript Default Error Type 변경
TypeScript를 사용할 경우 확인할 수 있는 default error type이 5버전 이전에는 unknown 이었지만 5버전 이후부터는 Error 타입으로 변경되었습니다.
그래서 5버전 이전에는 다음과 같이 Result 타입의 기본 error 값이 unknown으로 되어 있습니다.
// prev react-query-v5
import { UseQueryResult, useQuery } from '@tanstack/react-query';
export default function ErrorDefaultType() {
// error type is unknown
const res: UseQueryResult<string[], unknown> = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => {
throw 'force exception';
},
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
하지만 5버전 이후에는 기본 error 값이 Error 타입으로 변경된 것을 볼 수 있습니다.
// react-query-v5
import { UseQueryResult, useQuery } from '@tanstack/react-query';
export default function ErrorDefaultType() {
// error type is Error
const res: UseQueryResult<string[], Error> = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => {
throw 'force exception';
},
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
React 최소 버전 변경
5버전 이전에는 React 최소 버전은 16.8이었습니다.
하지만 5버전 이후부터는 React 최소 버전은 18.0으로 변경되었습니다.
RefetchOnWindowFocus 변경
5버전 이전에는 refetchOnWindowFocus 설정을 하게 되면 다음과 같은 상황들이 발생될 때 refetch를 수행했었습니다.
- 다른 브라우저 탭으로 넘어갔다가 돌아오는 경우
- 다른 화면을 클릭했다가 다시 focus를 한 경우
이 중 "다른 화면을 클릭했다가 다시 focus를 한 경우"는 사용자가 페이지를 떠나지 않았지만 다시 refetch가 되는 경우를 의미합니다.
react-query는 5버전 부터 이런 상황에 대해 refetch가 되는 경우를 삭제시켰습니다.
즉, 사용자가 애플리케이션 사용을 중지했다가 다시 돌아온 경우에만 refetch가 되는 것입니다.
5버전 이전에는 다음과 같이 설정할 경우 페이지를 떠나지 않아도 refetch가 되는 것을 볼 수 있습니다.
// prev react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function WindowFocus() {
const res = useQuery<string[]>({
// refetchOnWindowFocus work screen focus event
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
refetchOnWindowFocus: true, // set refetchOnWindowFocus
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
하지만 5버전 이후에는 다음과 같이 설정할 경우 페이지를 떠나지 않은 상태에서는 refetch가 발생되지 않은 것을 볼 수 있습니다.
// react-query-v5
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export default function WindowFocus() {
// refetchOnWindowFocus not work screen focus event
const res = useQuery<string[]>({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
refetchOnWindowFocus: true, // set refetchOnWindowFocus
});
return (
<main>
<div>
<h2>Strings</h2>
{res.data?.map((string) => <p key={string}>{string}</p>)}
</div>
</main>
);
}
Hydrate 설정 방법 변경
이번엔 자연스럽게 Next 얘기를 해보겠습니다.
5버전이 등장하면서 react-query의 SSR 동작을 위해 사용하는 Hydrate 컴포넌트의 이름이 변경되었습니다.
5버전 이전에는 다음과 같이 Hydrate 컴포넌트를 사용하여 dehydrateState 값을 등록할 수 있었습니다.
// prev react-query-v5
import { Hydrate, QueryClient, dehydrate } from '@tanstack/react-query';
import axios from 'axios';
import { cache } from 'react';
import Hydration from './Hydration';
export default async function Home() {
const queryClient = cache(() => new QueryClient())();
await queryClient.prefetchQuery({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
});
const dehydratedState = dehydrate(queryClient);
return (
// dehydratedState 등록을 위해 Hydrate 사용
<Hydrate state={dehydratedState}>
<main>
<div>
<h2>Strings</h2>
<Hydration />
</div>
</main>
</Hydrate>
);
}
하지만 5버전 이후부터는 Hydrate 컴포넌트가 HydrationBoundary 컴포넌트로 변경되어 다음과 같이 사용될 수 있습니다.
// react-query-v5
import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query';
import axios from 'axios';
import { cache } from 'react';
import Hydration from './Hydration';
export default async function Home() {
const queryClient = cache(() => new QueryClient())();
await queryClient.prefetchQuery({
queryKey: ['strings'],
queryFn: async () => (await axios.get('http://localhost:8080/strings')).data,
});
const dehydratedState = dehydrate(queryClient);
return (
// dehydratedState 등록을 위해 HydrationBoundary 사용
<HydrationBoundary state={dehydratedState}>
<main>
<div>
<h2>Strings</h2>
<Hydration />
</div>
</main>
</HydrationBoundary>
);
}
이상으로 react-query v5 변경점 알아보기 첫 번째인 공통 기능 변경에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > React' 카테고리의 다른 글
[React] react-query v5 변경점 알아보기 (3) - useQueries 기능 변경 (2) | 2023.12.03 |
---|---|
[React] react-query v5 변경점 알아보기 (2) - query 공통 기능 변경 (1) | 2023.12.01 |
[React] react-i18next로 다국어 처리하기 (1) | 2023.11.23 |
[React] Jest로 테스트 커버리지 확인하기 (2) - 커버리지 비율 한계점 (4) | 2023.11.20 |
[React] Jest로 테스트 커버리지 확인하기 (1) - 개념과 설정 방법 (2) | 2023.11.16 |
댓글