๐ @tanstack/react-query ์์ ์์ฃผ ์ฌ์ฉ๋๋ ๊ฐ๋ ์ ๋ฆฌ
1,709
194 commits
updated Sep 20, 2026
Pull Request, Issue๋ก ์์ ๋กญ๊ฒ ๋จ๊ฒจ์ฃผ์ธ์. ๊ฒํ ํ์ ๋ฐ์ํ๊ฒ ์ต๋๋ค.v5๊ฐ 23.10.17์ ๋ฆด๋ฆฌ์ฆ๋์ต๋๋ค. ํด๋น ๋ฌธ์๋ v5 ๊ธฐ์ค์ผ๋ก ์์ฑ๋์ด ์์ต๋๋ค.v4 ๋ฌธ์๋ react query tutorial v4 ๋ฌธ์๋ฅผ ํ์ธํด ์ฃผ์ธ์.v3 -> v4, v4 -> v5 ๋ง์ด๊ทธ๋ ์ด์
์ ๋ฆฌ๋ ์๋ ๋ฌธ์๋ฅผ ํ์ธํด ์ฃผ์ธ์.
์๋ฒ ์ํ ๊ฐ์ ธ์ค๊ธฐ, ์บ์ฑ, ๋๊ธฐํ ๋ฐ ์
๋ฐ์ดํธ๋ฅผ ๋ณด๋ค ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ํด๋ผ์ด์ธํธ ์ํ์ ์๋ฒ ์ํ๋ฅผ ๋ช
ํํ ๊ตฌ๋ถํ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋ค.redux, mobX๊ฐ ํด๋ผ์ด์ธํธ ์ํ ์์
์ ์ ํฉํ์ง๋ง, ๋น๋๊ธฐ ๋๋ ์๋ฒ ์ํ ์์
์๋ ๊ทธ๋ค์ง ์ข์ง ์๋ค๊ณ ์ธ๊ธํ๋ค.import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
// ...
},
},
});
์บ์์ ์ํธ ์์ฉํ ์ ์๋ค.query ๋๋ mutation์ ๊ธฐ๋ณธ ์ต์
์ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ์ข
๋ฅ๊ฐ ์๋นํ๋ฏ๋ก ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ๋ณด์.import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({ /* options */ });
function App() {
return (
<QueryClientProvider client={queryClient}>
<div>๋ธ๋ผ๋ธ๋ผ</div>
</QueryClientProvider>
);
}
QueryClientProvider๋ฅผ ์ต์๋จ์์ ๊ฐ์ธ์ฃผ๊ณ QueryClient ์ธ์คํด์ค๋ฅผ client prop์ผ๋ก ๋ฃ์ด ์ ํ๋ฆฌ์ผ์ด์
์ ์ฐ๊ฒฐํด์ผ ํ๋ค.background ๊ณ์ธต์ด ๋๋ค.
์ ์ฉ devtools๋ฅผ ์ ๊ณตํ๋ฉฐ ๋ณ๋์ ํจํค์ง ์ค์น๊ฐ ํ์ํ๋ค.์๊ฐํํ๋ ๋ฐ ๋์์ด ๋๋ฉฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋๋ฒ๊น
์๊ฐ์ ์ ์ฝํ ์ ์๋ค.process.env.NODE_ENV === "development"์ธ ๊ฒฝ์ฐ์๋ง ์คํ๋๋ค. ์ฆ, ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์๋ํ๋๋ก ์ค์ ๋์ด ์์ผ๋ฏ๋ก ํ๋ก์ ํธ ๋ฐฐํฌ ์์ Devtools ์ฝ์
์ฝ๋๋ฅผ ์ ๊ฑฐํด ์ค ํ์๊ฐ ์๋ค.$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
# or
$ bun add @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
true์ด๋ฉด ๊ฐ๋ฐ ๋๊ตฌ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ ค ์๋๋ก ์ค์ ํ ์ ์๋ค.bottom-rightrelative์ผ ๋ ๋ฒํผ์ devtools๋ฅผ ๋ ๋๋งํ๋ ์์น์ ๋ฐฐ์น๋๋ค.* Query Instances with and without cache data(์บ์ ๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ์๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค)
* Background Refetching(๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฆฌํจ์นญ)
* Inactive Queries(๋นํ์ฑ ์ฟผ๋ฆฌ)
* Garbage Collection(๊ฐ๋น์ง ์ปฌ๋ ์
)
const result = useQuery({
queryKey: ["A"],
queryFn,
staleTime: 0, // (๊ธฐ๋ณธ๊ฐ 0์ด)
gcTime: 1000 * 60 * 5, // (๊ธฐ๋ณธ๊ฐ 5๋ถ)
});
A๋ผ๋ queryKey๋ฅผ ๊ฐ์ง ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ mount๋ฉ๋๋ค.A๋ผ๋ queryKey๋ก ์บ์ฑ๋ฉ๋๋ค.staleTime์ด 0์ด๋ฏ๋ก, ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์๋ง์ ๋ฐ๋ก stale ์ํ๋ก ์ ํ๋ฉ๋๋ค.unmount๋๋ฉด, TanStack Query๋ ํด๋น ์บ์๋ฅผ gcTime ๋์ ์ ์งํฉ๋๋ค.A ์ฟผ๋ฆฌ๊ฐ ๋ค์ mount๋๋ค๋ฉด, ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ๋ฐํํ๊ณ , ๋์์ queryFn์ background์์ ์คํ๋ฉ๋๋ค.
queryFn์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋๋ฉด ์บ์๋ฅผ ์ต์ ๋ฐ์ดํฐ๋ก ์ฑ์ฐ๋ฉฐ, gcTime์ ๋ค์ 5๋ถ์ผ๋ก ์ด๊ธฐํ๋ฉ๋๋ค.mount๋์ง ์์ผ๋ฉด, ํด๋น ์บ์๋ ๊ฐ๋น์ง ์ปฌ๋ ์
์ ์ํด ์ญ์ ๋ฉ๋๋ค.v5๋ถํฐ ์ธ์๋ก ๋จ ํ๋์ ๊ฐ์ฒด๋ง ๋ฐ์ผ๋ฉฐ, ๊ทธ์ค queryKey์ queryFn์ด ํ์ ๊ฐ์ด๋ค.const result = useQuery({
queryKey, // required
queryFn, // required
// ...options ex) gcTime, staleTime, select, ...
});
result.data;
result.isLoading;
result.refetch;
// ...
// ์ค์ ์์
// ๐ก queryFn์ ๋ฐํ ํ์
์ ์ง์ ํด์ฃผ๋ฉด useQuery์ ํ์
์ถ๋ก ์ด ์ํํฉ๋๋ค.
const getAllSuperHero = async (): Promise<AxiosResponse<Hero[]>> => {
return await axios.get("http://localhost:4000/superheroes");
};
const { data, isLoading } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
1. queryKey
// (1) queryKey๋ ๋ฐ์ดํฐ๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ ๋ฟ ์๋๋ผ, ์๋์ ๊ฐ์ด ์ฟผ๋ฆฌ ํจ์์ ํธ๋ฆฌํ๊ฒ ์ ๋ฌํ ์๋ ์๋ค.
const getSuperHero = async ({
queryKey,
}: {
queryKey: ["super-hero", number];
}): Promise<AxiosResponse<Hero>> => {
const heroId = queryKey[1]; // ex) queryKey: ["super-hero", "3"]
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: getSuperHero, // (*)
});
};
๋ฐฐ์ด๋ก ์ง์ ํด ์ค์ผ ํ๋ค.
// An individual todo
useQuery({ queryKey: ["todo", 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ["todo", 5, { preview: true }], ...})
queryKey๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฟผ๋ฆฌ ์บ์ฑ์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ํต์ฌ์ด๋ค.
์์กดํ๋ค๋ฉด ๋ฐฐ์ด์ ์ด์ด์ ์ถ๊ฐํด์ผ ํ๋ค. ex: ["super-hero", heroId, ...]queryClient.setQueryData ๋ฑ๊ณผ ๊ฐ์ด ํน์ ์ฟผ๋ฆฌ์ ์ ๊ทผ์ด ํ์ํ ๋ ์ด๊ธฐ์ ์ค์ ํด๋ ํฌ๋งท์ ์ง์ผ์ค์ผ ์ ๋๋ก ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค.["super-hero", heroId]์ด๋ค. ๊ทธ๋ ๋ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์ฉํ ๋ ๋๊ฐ์ด ["super-hero", heroId] ํฌ๋งท์ ๊ฐ์ ธ์ผ ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ํ๋ ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค.2. queryFn
Promise๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ๋ฃ์ด์ผ ํ๋ค.// (2) ์๋จ์ queryKey ์์ ์ ๋ฐ๋๋ก queryFn ์์ฒด์ ์ผ๋ก ์ธ์๋ฅผ ๋ฐ๋ ํํ
const getSuperHero = async (heroId: string): Promise<AxiosResponse<Hero>> => {
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId), // (*)
});
};
3. options
options์ ๋ง์ด ์ฐ์ด๋ ์ต์
๋ค์ ์ฐจ๊ทผ์ฐจ๊ทผ ์ดํด๋ณผ ์์ ์ด๋ค. ๋ฌธ์ ์ธ์ ๋์ฑ ์์ธํ ์๊ณ ์ถ๋ค๋ฉด ์ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์.const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
gcTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
retry: 1,
// ... options
});
};
const {
data,
error,
status,
isPending,
fetchStatus,
isLoading,
isFetching,
isError,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
Promise์์ resolved๋ ๋ฐ์ดํฐdata, ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ์ ๋ํ ์ํ๋ฅผ ํํํ๋ status๋ ๋ฌธ์์ด ํํ๋ก 3๊ฐ์ง์ ๊ฐ์ด ์กด์ฌํ๋ค.
{ enabled: false } ์ํ๋ก ์ฟผ๋ฆฌ๊ฐ ํธ์ถ๋๋ฉด ์ด ์ํ๋ก ์์๋๋ค.true๋ฅผ ๋ฐํํ๋ค.
queryFn์ ๋ํ ์ ๋ณด๋ฅผ ๋ํ๋
์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์์ ๋ ์ฆ, ์ฒ์ ์คํ๋ ์ฟผ๋ฆฌ์ผ ๋ ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ true/false๋ก ๋ฐํ๋๋ค.
false๋ฅผ ๋ฐํํ๋ค.isFetching && isPending๊ณผ ๋์ผํ๋ค.true๋ฅผ ๋ฐํํ๋ค.
truetruesuccess ์ํ๋ผ๋ฉด ์ฃผ๋ก fetchStatus๋ idle ์ํ์ง๋ง, ๋ฐฑ๊ทธ๋ผ์ด๋์์ re-fetch๊ฐ ๋ฐ์ํ ๋ fetching ์ํ์ผ ์ ์๋ค.pending ์ํ์ผ ๋ fetchStatus๋ ์ฃผ๋ก fetching ์ํ์ง๋ง, ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋์ด ์์ง ์์ ๊ฒฝ์ฐ paused ์ํ๋ฅผ ๊ฐ์ง ์ ์๋ค.data๊ฐ ์๋์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.queryFn ์์ฒญ์ด ์งํ ์ค์ธ์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.useQuery({ queryKey, queryFn, enabled: false });
// isPending: true, isLoading: false
์ค๋๋์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๊ฐ ์๋๋ผ๋ ์๋ฏธ์ด๋ค.์ ์ ํ์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๋ผ๋ ์๋ฏธ์ด๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
gcTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
});
(number | Infinity)
fresh์์ stale ์ํ๋ก ๋ณ๊ฒฝ๋๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ, ๋ง์ฝ staleTime์ด 3000์ด๋ฉด fresh ์ํ์์ 3์ด ๋ค์ stale๋ก ๋ณํfresh ์ํ์ผ ๋๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ์๋กญ๊ฒ mount ๋์ด๋ ๋คํธ์ํฌ ์์ฒญ(fetch)์ด ์ผ์ด๋์ง ์๋๋ค.0์ด๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก fetch ํ์ ๋ฐ๋ก stale์ด ๋๋ค.(number | Infinity)
inactive ์ํ์ผ ๋ ์บ์ฑ ๋ ์ํ๋ก ๋จ์์๋ ์๊ฐ(๋ฐ๋ฆฌ์ด)์ด๋ค.inactive ์ํ๋ก ๋ณ๊ฒฝ๋๋ฉฐ, ์บ์๋ gcTime๋งํผ ์ ์ง๋๋ค.๊ฐ๋น์ง ์ปฌ๋ ํฐ์ ์ํด ์์ง๋๋ค.inactive ๋ ์์ ์ ๊ธฐ์ค์ผ๋ก ์บ์ ๋ฐ์ดํฐ ์ญ์ ๋ฅผ ๊ฒฐ์ ํ๋ค.5๋ถ์ด๋ค. SSR ํ๊ฒฝ์์๋ Infinity์ด๋ค.0์ด์ 5๋ถ์ด๋ผ๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ staleTime์ ์ด๋ ํ ์ค์ ๋ ํ์ง ์์ผ๋ฉด ํด๋น ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ(Observer)๊ฐ mount ๋์ ๋ ๋งค๋ฒ ๋ค์ API๋ฅผ ์์ฒญํ ๊ฒ์ด๋ค.staleTime์ gcTime๋ณด๋ค ์๊ฒ ์ค์ ํ๋ ๊ฒ์ด ์ข๋ค.๋ ์๊ฒฌ์ ๋์ํ์ง ์๋๋ค๊ณ ํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnMount: true,
});
boolean | "always" | ((query: Query) => boolean | "always")stale ์ํ์ผ ๊ฒฝ์ฐ, mount๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์
์ด๋ค.true์ด๋ค.always๋ก ์ค์ ํ๋ฉด ๋ง์ดํธ ์๋ง๋ค ๋งค๋ฒ refetch๋ฅผ ์คํํ๋ค.false๋ก ์ค์ ํ๋ฉด ์ต์ด fetch ์ดํ์๋ refetch ํ์ง ์๋๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnWindowFocus: true,
});
boolean | "always" | ((query: Query) => boolean | "always")stale ์ํ์ผ ๊ฒฝ์ฐ ์๋์ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์
์ด๋ค.true์ด๋ค.always๋ก ์ค์ ํ๋ฉด ์๋์ฐ๊ฐ ํฌ์ปค์ฑ๋ ๋๋ง๋ค ํญ์ refetch๋ฅผ ์คํํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
Polling(ํด๋ง)์ด๋?
์ค์๊ฐ ์น์ ์ํ ๊ธฐ๋ฒ์ผ๋ก "์ผ์ ํ ์ฃผ๊ธฐ(ํน์ ํ ์๊ฐ)"๋ฅผ ๊ฐ์ง๊ณ ์๋ฒ์ ์๋ต์ ์ฃผ๊ณ ๋ฐ๋ ๋ฐฉ์์ด ํด๋ง ๋ฐฉ์์ด๋ค.
react-query์์๋ "refetchInterval", "refetchIntervalInBackground"์ ์ด์ฉํด์ ๊ตฌํํ ์ ์๋ค.
number | false | ((data: TData | undefined, query: Query) => number | false)์๊ฐ(ms)์ ๊ฐ์ผ๋ก ๋ฃ์ด์ฃผ๋ฉด ์ผ์ ์๊ฐ๋ง๋ค ์๋์ผ๋ก refetch๋ฅผ ์์ผ์ค๋ค.booleanrefetchInterval๊ณผ ํจ๊ป ์ฌ์ฉํ๋ ์ต์
์ด๋ค.const {
data,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
enabled: false,
});
const handleClickRefetch = useCallback(() => {
refetch();
}, [refetch]);
return (
<div>
{data?.data.map((hero: Data) => (
<div key={hero.id}>{hero.name}</div>
))}
<button onClick={handleClickRefetch}>Fetch Heroes</button>
</div>
);
boolean์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ์คํ๋์ง ์๋๋ก ํ ๋ ์ค์ ํ ์ ์๋ค.false๋ฅผ ์ฃผ๋ฉด ์ฟผ๋ฆฌ๊ฐ ์๋ ์คํ๋์ง ์๋๋ค.
pending ์ํ๋ก ์์ํ๋ค.์๋์ผ๋ก ๋ค์ ์์ฒญํ๋ ๊ธฐ๋ฅ์ด๋ค. ์ฟผ๋ฆฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ์ค๋ฅ๋ง ๊ธฐ๋ก๋๋ค.
throwOnError ์์ฑ์ true๋ก ํด์ ์ ๋ฌํด์ผ ํ๋ค.๋ฒํผ ํด๋ฆญ์ด๋ ํน์ ์ด๋ฒคํธ๋ฅผ ํตํด ์์ฒญ์ ์๋ํ ๋ ๊ฐ์ด ์ฌ์ฉํ๋ค.enabled: false๋ฅผ ์คฌ๋ค๋ฉด queryClient๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์ค invalidateQueries์ refetchQueries๋ฅผ ๋ฌด์ํ๋ค.const {
data,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
retry: 10, // ์ค๋ฅ๋ฅผ ํ์ํ๊ธฐ ์ ์ ์คํจํ ์์ฒญ์ 10๋ฒ ์ฌ์๋ํฉ๋๋ค.
});
(boolean | number | (failureCount: number, error: TError) => boolean)์คํจํ๋ฉด useQuery๋ฅผ ํน์ ํ์๋งํผ ์ฌ์์ฒญํ๋ ์ต์
์ด๋ค.false์ธ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ์๋ํ์ง ์๋๋ค. true์ธ ๊ฒฝ์ฐ์๋ ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด ๋ฌดํํ ์ฌ์์ฒญ์ ์๋ํ๋ค.์ซ์๋ฅผ ๋ฃ์ ๊ฒฝ์ฐ, ์คํจ ํ์๊ฐ ํด๋น ์ซ์์ ๋๋ฌํ ๋๊น์ง ์์ฒญ์ ์ฌ์๋ํ๋ค.3, ์๋ฒ ํ๊ฒฝ์์๋ 0์ด๋ค.v4๊น์ง ์๋ onSuccess, onError, onSettled Callback์ useQuery ์ต์
์์ ์ ๊ฑฐ๋๋ค. ๋จ, useMutation์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
return (
<div>
{data.map((heroName, idx) => (
<div key={`${heroName}-${idx}`}>{heroName}</div>
))}
</div>
);
(data: TData) => unknown๋ฐํ๋ ๋ฐ์ดํฐ์ ์ผ๋ถ๋ฅผ ๋ณํํ๊ฑฐ๋ ์ ํํ ์ ์๋ค.const placeholderData = useMemo(() => generateFakeHeroes(), []);
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: placeholderData,
});
TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TDatapending ์ํ์ธ ๋์ ํน์ ์ฟผ๋ฆฌ์ ๋ํ placeholder data๋ก ์ฌ์ฉ๋๋ค.v4๊น์ง ์๋ keepPreviousData๋ ํ์ด์ง๋ค์ด์
๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ๋ง์ด ์ฌ์ฉํ๋ ์ต์
์ด์๋ค. ์บ์ฑ ๋์ง ์์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ๋ ๋ชฉ๋ก์ด ๊น๋นก๊ฑฐ๋ฆฌ๋ ํ์์ ๋ฐฉ์งํ ์ ์๋ค.
ํ์ง๋ง, v5๋ถํฐ keepPreviousData์ isPreviousData ์ต์
์ ์ ๊ฑฐ๋๋ค.
์ด๋ค์ ๊ฐ๊ฐ placeholderData์ isPlaceholderData ํ๋๊ทธ์ ๊ฑฐ์ ์ ์ฌํ๊ฒ ๋์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์๋ ์์ ์ฒ๋ผ placeholderData๋ฅผ ํ์ฉํ๋ฉด์ ์ด์ ๋ฒ์ ์์ keepPreviousData์ ๊ฐ์ "true"๋ก ์คฌ์ ๋์ ๋์ผํ ๊ธฐ๋ฅ์ ์ํํ ์ ์๋ค.
import { useQuery, keepPreviousData } from "@tanstack/react-query";
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: keepPreviousData,
});
import { useQuery } from "@tanstack/react-query";
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: (previousData, previousQuery) => previousData,
});
import { useQuery } from "@tanstack/react-query";
const { data, dataUpdatedAt } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
notifyOnChangeProps: ["data"], // data ๊ฐ ๋ณ๊ฒฝ ์์๋ง ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค
});
string[] | "all" | (() => string[] | "all")notifyOnChangeProps์ ์ค์ ๊ฐ์ ์ฃผ์ง ์์๋ค๋ฉด, data, dataUpdatedAt ์ค ์ด๋ ํ๋๊ฐ ๋ณ๊ฒฝ๋๋ฉด ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค."all"๋ก ์ค์ ํ ๊ฒฝ์ฐ, ์ฟผ๋ฆฌ์ ์ด๋ค ํ๋กํผํฐ๊ฐ ๋ณ๊ฒฝ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋๋ค.const { data: superHeroes } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
const { data: friends } = useQuery({
queryKey: ["friends"],
queryFn: getFriends,
});
๋ณ๋ ฌ๋ก ์์ฒญ๋์ด ์ฒ๋ฆฌ๋๋ค.๋์์ฑ์ ๊ทน๋ํ์ํจ๋ค.const queryResults = useQueries({
queries: [
{
queryKey: ["super-hero", 1],
queryFn: () => getSuperHero(1),
staleTime: Infinity, // ๋ค์๊ณผ ๊ฐ์ด option ์ถ๊ฐ ๊ฐ๋ฅ!
},
{
queryKey: ["super-hero", 2],
queryFn: () => getSuperHero(2),
staleTime: 0,
},
// ...
],
});
useQueries๋ฅผ ์ฌ์ฉํ๋ค.useQueries ํ ์ด ๋ฐํํ ๋ชจ๋ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ ํฌํจ๋ ๋ฐฐ์ด์ ๋จ์ผ ๊ฐ์ผ๋ก ๊ฒฐํฉํ๋ ค๋ฉด combine ์ต์ ์ ์ฌ์ฉํ ์ ์๋ค.
const ids = [1, 2, 3];
const combinedQueries = useQueries({
queries: ids.map((id) => ({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
})),
combine: (results) => {
return {
data: results.map((result) => result.data),
pending: results.some((result) => result.isPending),
};
},
});
data์ pending ํ๋กํผํฐ๋ฅผ ๊ฐ๋๋ค.์ข
์ ์ฟผ๋ฆฌ๋ ๋ค๋ฅธ ์ฟผ๋ฆฌ๊ฐ ๋จผ์ ์๋ฃ๋์ด์ผ๋ง ์คํํ ์ ์๋ ์ฟผ๋ฆฌ๋ฅผ ๋งํ๋ค. ์๋ฅผ ๋ค์ด A ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ธฐ ์ ์ B ์ฟผ๋ฆฌ๊ฐ ์๋ฃ๋์ด์ผ ํ๋ค๋ฉด, B์ ์์กดํ๋ A๊ฐ ์ข
์ ์ฟผ๋ฆฌ์ด๋ค.enabled ์ต์
์ ํตํด ์ข
์ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.// ์ฌ์ ์ ์๋ฃ๋์ด์ผ ํ ์ฟผ๋ฆฌ
const { data: user } = useQuery({
queryKey: ["user", email],
queryFn: () => getUserByEmail(email),
});
const channelId = user?.data.channelId;
// user ์ฟผ๋ฆฌ์ ์ข
์๋ ์ฟผ๋ฆฌ
const { data: courses } = useQuery({
queryKey: ["courses", channelId],
queryFn: () => getCoursesByChannelId(channelId),
enabled: !!channelId,
});
QueryClient ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค.QueryClient๋ ์บ์์ ์ํธ์์ฉํ๋ค.import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์บ์์ ์ ๊ณตํ๋ ๋ฐฉ๋ฒ์ด ์๋ค.const useSuperHeroData = (heroId: string) => {
const queryClient = useQueryClient();
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
initialData: () => {
const queryData = queryClient.getQueryData(["super-heroes"]) as any;
const hero = queryData?.data?.find(
(hero: Hero) => hero.id === parseInt(heroId)
);
if (hero) return { data: hero };
},
});
};
queryClient.getQueryData ๋ฉ์๋๋ ๊ธฐ์กด ์ฟผ๋ฆฌ์ ์บ์ฑ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ธฐ ํจ์์ด๋ค. ์ฟผ๋ฆฌ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด undefined๋ฅผ ๋ฐํํ๋ค.UX์ ์ข์ ์ํฅ์ ์ค ์ ์๋ค.
queryClient.prefetchQuery๋ฅผ ํตํด prefetch ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.const queryClient = useQueryClient();
const prefetchNextPosts = async (nextPage: number) => {
// ํด๋น ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๋ ์ผ๋ฐ ์ฟผ๋ฆฌ๋ค์ฒ๋ผ ์บ์ฑ ๋๋ค.
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
// ...options
});
};
// ๋จ์ ์
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
์ด๋ฏธ ์บ์ฑ ๋์ด ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ค.์ฒซ ๋ฒ์งธ ํ์ด์ง๋ง prefetch ๋๋ฉฐ, ๊ทธ ์ด์์ prefetch ํ๋ ค๋ฉด pages ์ต์
์ ํ์ฉํด์ผ ํ๋ค.
getNextPageParam ํจ์๋ฅผ ๋ฌด์กฐ๊ฑด ์ ๊ณตํด ์ค์ผ ํ๋ค๋ ์ ์ ์ฃผ์ํ์.const prefetchTodos = async () => {
await queryClient.prefetchInfiniteQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
});
};
๋ฌดํ ์คํฌ๋กค์ด๋ load more(๋ ๋ณด๊ธฐ)๊ณผ ๊ฐ์ด ํน์ ์กฐ๊ฑด์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ๋ฉด ์ ์ฉํ๋ค.useInfiniteQuery๋ฅผ ์ ๊ณตํ๋ค.import { useInfiniteQuery } from "@tanstack/react-query";
// useInfiniteQuery์ queryFn์ ๋งค๊ฐ๋ณ์๋ `pageParam`์ด๋ผ๋ ํ๋กํผํฐ๋ฅผ ๊ฐ์ง ์ ์๋ค.
const fetchColors = async ({
pageParam,
}: {
pageParam: number;
}): Promise<AxiosResponse<PaginationColors>> => {
return await axios.get(`http://localhost:4000/colors?page=${pageParam}`);
};
const InfiniteQueries = () => {
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: fetchColors,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 ? allPages.length + 1 : undefined;
},
// ...
});
return (
<div>
{data?.pages.map((group, idx) => ({
/* ... */
}))}
<div>
<button disabled={!hasNextPage} onClick={() => fetchNextPage()}>
LoadMore
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
</div>
);
};
useInfiniteQuery๋ ๊ธฐ๋ณธ์ ์ผ๋ก useQuery์ ์ฌ์ฉ๋ฒ์ ๋น์ทํ์ง๋ง, ์ฐจ์ด์ ์ด ์๋ค.isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage ๋ฑ์ด ์ถ๊ฐ์ ์ผ๋ก ์๋ค.
๋ค์ ํ์ด์ง๋ฅผ fetch ํ ์ ์๋ค.์ด์ ํ์ด์ง๋ฅผ fetch ํ ์ ์๋ค.fetchNextPage ๋ฉ์๋๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค.fetchPreviousPage ๋ฉ์๋๊ฐ ์ด์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค.๋ค์ ํ์ด์ง๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.์ด์ ํ์ด์ง๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.TPageParamํ์๊ฐ์ด๋ค.(lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | nullํ์๊ฐ์ด๋ค.
lastPage๋ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฐ์ ธ์จ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค.allPages๋ ํ์ฌ๊น์ง ๊ฐ์ ธ์จ ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค.lastPageParam์ ๊ฐ์ฅ ์ต๊ทผ์ ์ฌ์ฉ๋ ํ์ด์ง ๋งค๊ฐ๋ณ์์ด๋ค.allPageParams๋ ํ์ฌ๊น์ง ์ฌ์ฉ๋ ๋ชจ๋ ํ์ด์ง ๋งค๊ฐ๋ณ์์ด๋ค.undefined ๋๋ null์ ๋ฐํํ๋ฉด ๋๋ค.getPreviousPageParam๋ ์กด์ฌํ๋ฉฐ, getNextPageParam๊ณผ ๋ฐ๋์ ์์ฑ์ ๊ฐ๊ณ ์๋ค.number | undefined์ต๋ ํ์ด์ง ์์ด๋ค.์ง์ ๋ ๋ฐฉํฅ(next, previous)์ ๋ฐ๋ผ ํ์ด์ง ๋ฐฐ์ด์์ ์ฒซ ๋ฒ์งธ ํ์ด์ง ๋๋ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์ ๊ฑฐ๋๋ค.0 ๋๋ undefined๋ผ๋ฉด ํ์ด์ง ์๋ ๋ฌด์ ํ์ด๋ค.queryFn์ ๋๊ฒจ์ฃผ๋ pageParam๊ฐ ๋จ์ํ ๋ค์ page์ ๊ฐ๋ง์ ๊ด๋ฆฌํ ์ ์๋ ๊ฒ์ ์๋๋ค.getNextPageParam์์ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝ์์ผ ์ค ์ ์๋ค.const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: ({ pageParam }) => fetchColors(pageParam), // pageParam: { page: number; etc: string }
initialPageParam: {
page: 1,
etc: "hi",
},
getNextPageParam: (lastPage, allPages) => {
if (allPages.length >= 4) return undefined;
return {
page: allPages.length + 1,
etc: "bye",
};
},
});
queryFn์ ๋ฃ์ pageParam์์ getNextPageParam์์ ๋ฐํํ ๊ฐ์ฒด๋ฅผ ๋ฐ์์ฌ ์ ์๋ค.const fetchColors = async ({
page,
etc,
}: {
page: number;
etc: string;
}): Promise<AxiosResponse<PaginationColors>> => {
return await axios.get(
`http://localhost:4000/colors?page=${page}&etc=${etc}`
);
};
R(read)๋ useQuery, CUD(Create, Update, Delete)๋ useMutation์ ์ฌ์ฉํ๋ค.const { mutate } = useMutation({
mutationFn: createTodo,
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutate({ title });
};
mutate ํจ์๋ฅผ ์ด์ฉํด์ ์์ฒญ ํจ์๋ฅผ ํธ์ถํ ์ ์๋ค.onSuccess, onError ์ฝ๋ฐฑ์ ํตํด ์ฑ๊ณตํ์ ๋์ ์คํจํ์ ๋์ response ๋ฐ์ดํฐ๋ฅผ ํธ๋ค๋งํ ์ ์๋ค.onMutate๋ mutation ํจ์๊ฐ ์คํ๋๊ธฐ ์ ์ ์คํ๋๊ณ , mutation ํจ์๊ฐ ๋ฐ์ ๋์ผํ ๋ณ์๊ฐ ์ ๋ฌ๋๋ค.onSettled๋ try...catch...finally ๊ตฌ๋ฌธ์ finally์ฒ๋ผ ์์ฒญ์ด ์ฑ๊ณตํ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์๊ด์์ด ๋ง์ง๋ง์ ์คํ๋๋ค.const mutation = useMutation({ mutationFn: addTodo });
try {
const todo = await mutation.mutateAsync(todo);
console.log(todo);
} catch (error) {
console.error(error);
} finally {
console.log("done");
}
mutateAsync๋ฅผ ์ฌ์ฉํด์ ์ป์ด์ฌ ์ ์๋ค.unhandled promise rejection ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ค.useMutation์ Callback -> mutate์ Callback ์์ผ๋ก ์คํ๋๋ค.TkDodo๋ ์์ ๊ฐ์ ์ด์ ๋ก ๋์ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์ ํ๋ค๊ณ ํ๋ค.
์ฟผ๋ฆฌ ์ด๊ธฐํ)์ useMutation์ Callback์ผ๋ก ์คํ์ํจ๋ค.์๋์ผ๋ก ์ทจ์ํ๊ณ ์ถ์ ์๋ ์๋ค.
queryClient.cancelQueries({ queryKey })๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ๋ํ react-query๋ ์ฟผ๋ฆฌ ์ทจ์๋ฟ๋ง ์๋๋ผ queryFn์ Promise๋ ์ทจ์ํ๋ค.const query = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
const queryClient = useQueryClient();
const onCancelQuery = (e) => {
e.preventDefault();
queryClient.cancelQueries({
queryKey: ["super-heroes"],
});
};
return <button onClick={onCancelQuery}>Cancel</button>;
์์ฑ(Post)ํ๊ฑฐ๋ ๊ฒ์๊ธ์ ์ ๊ฑฐ(Delete)ํ์ ๋ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ํ ๋ชฉ๋ก์ ์ค์๊ฐ์ผ๋ก ์ต์ ํํด์ผ ํ ๋๊ฐ ์๋ค.query Key๊ฐ ๋ณํ์ง ์์ผ๋ฏ๋ก ๊ฐ์ ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ๊ณ ์ต์ ํ๋ฅผ ์งํํด์ผ ํ๋๋ฐ, ์ด๋ฐ ๊ฒฝ์ฐ์ invalidateQueries() ๋ฉ์๋๋ฅผ ์ด์ฉํ ์ ์๋ค.refetch๋ฅผ ํ ๋ ์ฌ์ฉํ๋ค!import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onSuccess(data) {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // ์ด key์ ํด๋นํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ!
console.log(data);
},
onError(err) {
console.log(err);
},
});
};
["super-heroes"]์ ๋๊ฒจ์ฃผ๋ฉด queryKey์ "super-heroes"๋ฅผ ํฌํจํ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ๋๋ค.queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
// ์๋ ์ฟผ๋ฆฌ๋ค์ด ๋ชจ๋ ๋ฌดํจํ๋๋ค.
const supermanQuery = useQuery({
queryKey: ["super-heroes", "superman"],
queryFn: fetchSuperHero,
});
const heroQuery = useQuery({
queryKey: ["super-heroes", { id: 1 }],
queryFn: fetchSuperHero,
});
enabled/refetch์์๋ ์ธ๊ธํ์ง๋ง enabled: false ์ต์
์ ์ฃผ๋ฉด queryClient๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์ค invalidateQueries์ refetchQueries๋ฅผ ๋ฌด์ํ๋ค.
queryClient.invalidateQueries๋ฅผ ์ด์ฉํด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ดค๋๋ฐ queryClient.setQueryData๋ฅผ ์ด์ฉํด์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์
๋ฐ์ดํธํ ์ ์๋ค.queryClient.setQueryData๋ ์ฟผ๋ฆฌ์ ์บ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์
๋ฐ์ดํธํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ธฐ ํจ์์ด๋ค.updater ํจ์์ด๋ค. ํด๋น ํจ์์ ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ oldData๋ก ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onSuccess(data) {
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [...oldData.data, data.data],
};
});
},
onError(err) {
console.log(err);
},
});
};
Optimistic Update(๋๊ด์ ์
๋ฐ์ดํธ)๋ ์๋ฒ ์
๋ฐ์ดํธ ์ UI์์๋ ์ด์ฐจํผ ์
๋ฐ์ดํธํ ๊ฒ์ด๋ผ๊ณ (๋๊ด์ ์ธ) ๊ฐ์ ํด์ ๋ฏธ๋ฆฌ UI๋ฅผ ์
๋ฐ์ดํธ ์์ผ์ฃผ๊ณ ์๋ฒ๋ฅผ ํตํด ๊ฒ์ฆ๋ฐ๊ณ ์
๋ฐ์ดํธ ๋๋ ๋กค๋ฐฑํ๋ ๋ฐฉ์์ด๋ค.const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onMutate: async (newHero: any) => {
await queryClient.cancelQueries({ queryKey: ["super-heroes"] });
// ์ด์ ๊ฐ
const previousHeroData = queryClient.getQueryData(["super-heroes"]);
// ์๋ก์ด ๊ฐ์ผ๋ก ๋๊ด์ ์
๋ฐ์ดํธ ์งํ
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [
...oldData.data,
{ ...newHero, id: oldData?.data?.length + 1 },
],
};
});
// ๊ฐ์ด ๋ค์ด์๋ context ๊ฐ์ฒด๋ฅผ ๋ฐํ
return { previousHeroData };
},
// mutation์ด ์คํจํ๋ฉด onMutate์์ ๋ฐํ๋ context๋ฅผ ์ฌ์ฉํ์ฌ ๋กค๋ฐฑ ์งํ
onError(error, hero, context: any) {
queryClient.setQueryData(["super-heroes"], context.previousHeroData);
},
// ์ค๋ฅ ๋๋ ์ฑ๊ณต ํ์๋ ํญ์ refetch
onSettled() {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
},
});
};
cancelQueries๋ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ทจ์์ํฌ ์ ์๋ค. ์ทจ์์ํฌ query์ queryKey๋ฅผ cancelQueries์ ์ธ์๋ก ๋ณด๋ด ์คํ์ํจ๋ค.์ ์ธ์ ์ผ๋ก ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.useQueryErrorResetBoundary๋ ErrorBoundary์ ํจ๊ป ์ฌ์ฉํ๋ค. ErrorBoundary๋ ๋ฆฌ์กํธ ๊ณต์ ๋ฌธ์์์๋ ๊ธฐ๋ณธ ์ฝ๋๋ฅผ ์ ๊ณตํ์ง๋ง, ์ข ๋ ์ฝ๊ฒ ํ์ฉํ ์ ์๋ react-error-boundary ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ค. react-query ๊ณต์ ๋ฌธ์์์๋ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์์๋ก ์ฌ์ฉํ๊ณ ์์ผ๋, ์ฌ๊ธฐ์๋ react-error-boundary๋ฅผ ์ค์นํด์ ์ฌ์ฉํด ๋ณด์.$ npm i react-error-boundary
# or
$ pnpm add react-error-boundary
# or
$ yarn add react-error-boundary
# or
$ bun add react-error-boundary
useQueryErrorResetBoundary ํ
์ ํธ์ถํด reset ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค.fallbackRender prop์ผ๋ก ๋๊ธด ๋ด์ฉ์ด ๋ ๋๋ง ๋๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด children ๋ด์ฉ์ด ๋ ๋๋ง ๋๋ค.resetErrorBoundary๋ฅผ ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ํตํด ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ, ์ด๋ฅผ ํตํด ๋ชจ๋ ์ฟผ๋ฆฌ ์๋ฌ๋ฅผ ์ด๊ธฐํํ ์ ์๋ค. ์๋ ์ฝ๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ button์ ํด๋ฆญํ๋ฉด ์๋ฌ๋ฅผ ์ด๊ธฐํํ๊ฒ๋ ์์ฑํ๋ค.import { useQueryErrorResetBoundary } from "@tanstack/react-query"; // (*)
import { ErrorBoundary } from "react-error-boundary"; // (*)
interface Props {
children: React.ReactNode;
}
const QueryErrorBoundary = ({ children }: Props) => {
const { reset } = useQueryErrorResetBoundary(); // (*)
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
Error!!
<button onClick={() => resetErrorBoundary()}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
};
export default QueryErrorBoundary;
{ throwOnError: true }๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ErrorBoundary ์ปดํฌ๋ํธ๊ฐ ๊ฐ์งํ ์ ์๋ค.import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true, // (*) ์ฌ๊ธฐ์๋ ๊ธ๋ก๋ฒ๋ก ์ธํ
ํ์ง๋ง, ๊ฐ๋ณ ์ฟผ๋ฆฌ๋ก ์ธํ
๊ฐ๋ฅ
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<QueryErrorBoundary>{/* ํ์ ์ปดํฌ๋ํธ๋ค */}</QueryErrorBoundary>
</QueryClientProvider>
);
}
์ ์ธ์ ์ผ๋ก ์์ฑํ ์ ์๊ณ , ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋ Suspense์๋ ๊ฒฐํฉํด์ ์๋ฒ ํต์ ์ํ๊ฐ ๋ก๋ฉ ์ค์ผ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๊ฒ ์ ์ธ์ ์ผ๋ก ์์ฑํ ์ ์๋ค.Component Lazy Loading์ด๋ Data Fetching ๋ฑ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํ ๋, ์๋ต์ ๊ธฐ๋ค๋ฆฌ๋ ๋์ Fallback UI(ex: Loader)๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ํ๋ ์ปดํฌ๋ํธ๋ค.import { Suspense } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// suspense: true, - ๐ก v5๋ถํฐ Deprecated
// useQuery/useInfiniteQuery์ ๊ฐ์ ์ผ๋ฐ ํ
๋์ useSuspenseQuery/useSuspenseInfiniteQuery์ ๊ฐ์ suspense ํ
์ฌ์ฉ
throwOnError: true,
},
},
});
function App() {
return (
<QueryErrorBoundary>
<Suspense fallback={<Loader />}>{/* ํ์ ์ปดํฌ๋ํธ๋ค */}</Suspense>
</QueryErrorBoundary>
);
}
๋ด๋ถ ๋ณต์ก์ฑ์ ์ถ์ํํ๋ ๊ฒ ๋ฐ๋ก ์ ์ธํ ์ปดํฌ๋ํธ์ด๋ค.react-query(useSuspenseQuery)์ ๊ฒฐํฉํ Suspense๋ ์๋์ ๊ฐ์ ๊ณผ์ ์ผ๋ก ๋์ํ๋ค.1. Suspense mount
2. MainComponent mount
3. MainComponent์์ useSuspenseQuery ํ
์ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ๋ฐ์ดํฐ ์์ฒญ
4. MainComponent unmount, fallback UI์ธ Loader mount
5. ๋น๋๊ธฐ ๋ฐ์ดํฐ ์์ฒญ์ด ์๋ฃ๋๋ฉด fallback UI์ธ Loader unmount
6. MainComponent mount
data fetching์ ๋ํ suspense๊ฐ ๋ง์นจ๋ด ์์ ํ๋์์ต๋๋ค.useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 3๊ฐ์ง ํ
์ด ์ถ๊ฐ๋์์ต๋๋ค.suspense ์ต์
์ ์ ๊ฑฐ๋์์ต๋๋ค. ๋ฐ๋ผ์ Suspense๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์ ํ
๋ค์ ํ์ฉํด์ผ ํฉ๋๋ค.data๊ฐ undefined ์ํ๊ฐ ๋์ง ์์ต๋๋ค.import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
const fetchGroups = async (): Promise<{ data: Group[] }> => {
const res = await axios.get("/groups");
return res;
};
// as-is
const { data } = useQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
// to-be
const { data } = useSuspenseQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
queryKey๋ฅผ ์ฌ์ฉํด ๊ฐ์ ธ์์ผ ํ ๋ฐ์ดํฐ๋ฅผ ์๋ณํ๊ณ ์ถ๋ค๋ฉด QueryClient์ queryFn ์ต์
์ ํตํด Default Query Function์ ์ง์ ํด ์ค ์ ์๋ค.// ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์
const getSuperHero = async ({ queryKey }: any) => {
const heroId = queryKey[1];
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: getSuperHero,
// ...queries options
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>{/* ... */}</QueryClientProvider>
);
}
QueryClient์ ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ ์ฟผ๋ฆฌ ํจ์๋ฅผ ์ง์ ํด ์ค๋ค.// ์ฌ์ฉ ์์
const useSuperHeroData = (heroId: string) => {
return useQuery({ queryKey: ["super-hero", heroId] });
};
// ๋ค์ ํํ๋ ๋ถ๊ฐ๋ฅ
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
});
};
queryKey๋ง ๋ฃ์ด์ฃผ๋ฉด queryFn์๋ ์๋์ผ๋ก ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ค์ด๊ฐ๋ค.useQuery๋ฅผ ์ฌ์ฉํ ๋์ ๋ฌ๋ฆฌ queryFn์ ์ง์ ํ์ง ์๊ธฐ์ ์ฟผ๋ฆฌ ํจ์์ ์ง์ ์ธ์๋ฅผ ๋ฃ์ด์ฃผ๋ ํํ์ ์ฌ์ฉ์ ๋ถ๊ฐ๋ฅํ๋ค.queryOptions์ mutationOptions๋ ์ฟผ๋ฆฌ/๋ฎคํ
์ด์
์ต์
์ ํ๊ณณ์ ๋ชจ์ ๋๊ณ , ์ฌ๋ฌ ๊ณณ์์ ํ์
์์ ํ๊ฒ ์ฌ์ฌ์ฉํ๊ธฐ ์ํ ํฌํผ์
๋๋ค.๊ทธ๋๋ก ๋ฐํํ๋ ํญ๋ฑ ํจ์์ด๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ๋ฐํ์ ๋น์ฉ์ด ์์ต๋๋ค. ์ฆ, ์จ์ ํ ํ์
์คํฌ๋ฆฝํธ๋ฅผ ์ํ ํฌํผ์
๋๋ค.queryOptions์ infiniteQueryOptions๋ v5.0.0๋ถํฐ, mutationOptions๋ v5.82.0๋ถํฐ ์ฌ์ฉํ ์ ์์ต๋๋ค.queryKey์ queryFn์ ์ปค์คํ
ํ
์์์ ๊ด๋ฆฌํ๋๋ฐ, ์ต์
์ ๋ณ๋ ํจ์๋ก ๋ถ๋ฆฌํ๋ฉด queryKey๊ฐ ๋ค๊ณ ์๋ ๋ฐ์ดํฐ ํ์
์ ๋ณด๊ฐ ์ฌ๋ผ์ง๋๋ค.// โ ์ผ๋ฐ ๊ฐ์ฒด๋ก ๋ถ๋ฆฌํ ๊ฒฝ์ฐ
const superHeroOptions = (heroId: string) => ({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
});
const hero = queryClient.getQueryData(superHeroOptions("1").queryKey);
// ^? unknown
queryOptions๋ก ๊ฐ์ธ๋ฉด ๋ฐํ๋ queryKey์ queryFn์ ๋ฐํ ํ์
์ด ํจ๊ป ๋ถ๊ธฐ ๋๋ฌธ์, ๋ช
๋ นํ API์์๋ ์ ๋ค๋ฆญ์ ์ง์ ๋ช
์ํ ํ์๊ฐ ์์ต๋๋ค.import { queryOptions } from "@tanstack/react-query";
export const superHeroOptions = (heroId: string) =>
queryOptions({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
staleTime: 1 * 60 * 1000,
// ...options
});
const hero = queryClient.getQueryData(superHeroOptions("1").queryKey);
// ^? AxiosResponse<Hero> | undefined
queryClient ๋ฉ์๋์ ๊ทธ๋๋ก ๋๊ธธ ์ ์์ต๋๋ค.// ํ
useQuery(superHeroOptions("1"));
useSuspenseQuery(superHeroOptions("1"));
useQueries({
queries: [superHeroOptions("1"), superHeroOptions("2")],
});
// queryClient ๋ฉ์๋
queryClient.prefetchQuery(superHeroOptions("1"));
queryClient.ensureQueryData(superHeroOptions("1"));
// queryKey๋ง ํ์ํ ๋ฉ์๋์๋ `.queryKey`๋ฅผ ๊บผ๋ด์ ๋๊ฒจ์ค๋๋ค.
queryClient.setQueryData(superHeroOptions("1").queryKey, newHero);
queryClient.invalidateQueries({ queryKey: superHeroOptions("1").queryKey });
queryClient.setQueryData ๋ฑ์ ์ฌ์ฉํ ๋ ์ด๊ธฐ์ ์ค์ ํด๋ queryKey ํฌ๋งท์ ์ง์ผ์ผ ํ๋ค๊ณ ์ธ๊ธํ์ต๋๋ค. queryOptions๋ฅผ ์ฌ์ฉํ๋ฉด queryKey๋ฅผ ํ๊ณณ์์๋ง ๊ด๋ฆฌํ๋ฏ๋ก ์ด๋ฐ ์ค์๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.select ํจ์๋ฅผ ๋ถ์ด๋ ํจํด์ด ์์ต๋๋ค.const { data } = useQuery({
...superHeroOptions("1"),
select: (data) => data.data.name,
});
/**
* ํ์
์ถ๋ก ์ด ์ ์ง๋๋ฏ๋ก data๋ select์ ๋ฐํ ํ์
์ด ๋ฉ๋๋ค.
* data: string | undefined
*/
infiniteQueryOptions๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ฌ์ฉ๋ฒ์ queryOptions์ ๋์ผํฉ๋๋ค.v5.82.0๋ถํฐ ๋ฎคํ
์ด์
์๋ ๋์ผํ ํฌํผ์ธ mutationOptions๊ฐ ์ถ๊ฐ๋์ต๋๋ค. useMutation์ ๋๊ธธ ์ ์๋ ์ต์
์ ๊ทธ๋๋ก ๋๊ธธ ์ ์์ต๋๋ค.queryOptions์ ๋ฌ๋ฆฌ ์ถ๊ฐ์ ์ธ ํ์
์ ๋ณด๋ฅผ ๋ถ์ด์ง ์๊ณ ๋ฐ์ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํํฉ๋๋ค. ์ฆ, ์ต์
๊ฐ์ฒด์ ๋ํ ํ์
๊ฒ์ฌ์ ์๋ ์์ฑ์ ๋ฐ๊ธฐ ์ํ ํฌํผ์
๋๋ค.import { mutationOptions, useMutation } from "@tanstack/react-query";
export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
});
const { mutate } = useMutation(addSuperHeroOptions);
mutationKey๋ฅผ ์ง์ ํ๋ฉด ๋์ค์ useMutationState๋ useIsMutating์ผ๋ก ํด๋น ๋ฎคํ
์ด์
์ ์ํ๋ฅผ ์กฐํํ ์ ์์ต๋๋ค. ํ์ ์๋ค๋ฉด ์๋ตํด๋ ๋ฉ๋๋ค.import { mutationOptions, useMutationState } from "@tanstack/react-query";
export const addSuperHeroOptions = mutationOptions({
mutationKey: ["super-heroes", "add"],
mutationFn: addSuperHero,
});
// ์ ์ญ "์ ์ฅ ์ค..." ์ธ๋์ผ์ดํฐ
const SavingIndicator = () => {
const isAdding =
useMutationState({
filters: {
mutationKey: addSuperHeroOptions.mutationKey,
status: "pending",
},
}).length > 0;
return isAdding ? <span>Saving...</span> : null;
};
mutationOptions๋ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๊ธฐ ๋๋ฌธ์, ์คํ๋ ๋๋ก ์ฝ๋ฐฑ์ ๋ง๋ถ์ด๋ฉด ์๋ณธ ์ฝ๋ฐฑ์ด ๋ฎ์ด์จ์ง๋๋ค.export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
onSuccess: () => console.log("A"),
});
useMutation({
...addSuperHeroOptions,
onSuccess: () => console.log("B"), // โ "A"๋ ์คํ๋์ง ์์ต๋๋ค.
});
mutationOptions์ ๋๊ณ ์ปดํฌ๋ํธ ์ ์ฉ ๋ก์ง์ mutate์ Callback์ผ๋ก ๋๊ธฐ๋ ๊ฒ์ด ์ข์ต๋๋ค.export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
onSuccess: () => {
// ๊ณตํต ๋ก์ง
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
},
});
const { mutate } = useMutation(addSuperHeroOptions);
mutate(newHero, {
// ์ปดํฌ๋ํธ ์ ์ฉ ๋ก์ง
onSuccess: () => navigate("/super-heroes"),
});
์ ๋ค๋ฆญ(Generics)์ ๋ง์ด ์ฌ์ฉํ๋ค. ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๊ณ API๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ ์ ํ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.ํ์ฌ useQuery๊ฐ ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ์ 4๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ์ ํ์
์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์ select์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์
์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค.queryKey์ ํ์
์ ๋ช
์์ ์ผ๋ก ์ง์ ํด ์ฃผ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.// useQuery์ ํ์
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
import { AxiosError } from "axios";
// useQuery ํ์
์ ์ฉ ์์
const { data } = useQuery<
AxiosResponse<Hero[]>,
AxiosError,
string[],
["super-heroes", number]
>({
queryKey: ["super-heroes", id],
queryFn: getSuperHero,
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
/**
์ฃผ์ ํ์
* data: string[] | undefined
* error: AxiosError<any, any>
* select: (data: AxiosResponse<Hero[]>): string[]
*/
useMutation์ด ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ๋ useQuery์ ๋์ผํ๊ฒ 4๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.mutate ํจ์์ ์ ๋ฌํ ์ธ์๋ฅผ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
onMutate ํจ์์ return๊ฐ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
export function useMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown
>
// useMutation ํ์
์ ์ฉ ์์
const { mutate } = useMutation<Todo, AxiosError, number, number>({
mutationFn: postTodo,
onSuccess: (res, id, nextId) => {},
onError: (err, id, nextId) => {},
onMutate: (id) => id + 1,
onSettled: (res, err, id, nextId) => {},
});
const onClick = () => {
mutate(5);
};
/**
์ฃผ์ ํ์
* data: Todo
* error: AxiosError<any, any>
* onSuccess: (res: Todo, id: number, nextId: number)
* onError: (err: AxiosError, id: number, nextId: number)
* onMutate: (id: number)
* onSettled: (res: Todo, err: AxiosError, id: number, nextId: number),
*/
ํ์ฌ useInfiniteQuery๊ฐ ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ์ 4๊ฐ์ด๋ฉฐ, useQuery์ ์ ์ฌํ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ์ ํ์
์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์ select์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์
์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค.queryKey์ ํ์
์ ๋ช
์์ ์ผ๋ก ์ง์ ํด ์ฃผ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.export function useInfiniteQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey
>
const {
data,
hasNextPage,
fetchNextPage,
//...
} = useInfiniteQuery<
AxiosResponse<PaginationColors>,
AxiosError,
InfiniteData<AxiosResponse<PaginationColors>, number>,
["colors"]
>({
queryKey: ["colors"],
queryFn: fetchColors,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 ? allPages.length + 1 : undefined;
},
// ...options
});
/**
์ฃผ์ ํ์
* data: InfiniteData<AxiosResponse<PaginationColors>, number> | undefined
* error: AxiosError<any, any>
* select: (data: InfiniteData<AxiosResponse<PaginationColors>, number>): InfiniteData<AxiosResponse<PaginationColors>, number>
* getNextPageParam: GetNextPageParamFunction<number, AxiosResponse<PaginationColors>>
*/
queryFn์ ํ์
์ ์ ์ ์ํด์ ํ์
์ถ๋ก ์ด ์ํํ๊ฒ ๋๊ฒ ํ๋ ๊ฒ์ด๋ค.const fetchGroups = async (): Promise<AxiosResponse<Group[]>> => {
return await axios.get("/groups");
};
const { data } = useQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
/**
์ฃผ์ ํ์
* data: Group[] | undefined
* error: Error | null
* select: (data: AxiosResponse<Group[]>): Group[]
*/
์์ฒด ESLint Plugin์ ์ ๊ณตํฉ๋๋ค. ํด๋น ํ๋ฌ๊ทธ์ธ์ ํตํด ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์ ์ฉํ๊ณ , ์ผ๋ฐ์ ์ธ ์ค์๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.$ npm i -D @tanstack/eslint-plugin-query
# or
$ pnpm add -D @tanstack/eslint-plugin-query
# or
$ yarn add -D @tanstack/eslint-plugin-query
# or
$ bun add -D @tanstack/eslint-plugin-query
๊ถ์ฅํ๋ ๋ชจ๋ rule์ ์ ์ฉํ๋ ค๋ฉด ์๋์ ๊ฐ์ด .eslintrc.js ํ์ผ์ extends ๋ฐฐ์ด ์์ plugin:@tanstack/eslint-plugin-query/recommended๋ฅผ ์ถ๊ฐํฉ๋๋ค.module.exports = {
// ...
extends: ["plugin:@tanstack/eslint-plugin-query/recommended"],
rules: {
/* ์์ ํ๊ณ ์ ํ๋ rule ์ถ๊ฐ ๊ฐ๋ฅ... */
},
};
์ฌ์ฉ ๋ฐฉ๋ฒ(2)์ ๊ฐ์ด rule์ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.rule์ ๊ฐ๋ณ์ ์ผ๋ก ์ค์ ํด์ ์ ์ฉํ๋ ค๋ฉด ์๋์ ๊ฐ์ด .eslintrc.js ํ์ผ์ plugins ๋ฐฐ์ด ์์ @tanstack/query๋ฅผ ์ถ๊ฐํ๊ณ , ์ ์ฉํ๊ณ ์ ํ๋ rules์ ๊ท์น์ ์ถ๊ฐํฉ๋๋ค.module.exports = {
// ...
plugins: ["@tanstack/query"],
rules: {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-rest-destructuring": "warn",
"@tanstack/query/stable-query-client": "error",
},
};
TanStack Query v5์ ํ์ํ TypeScript ์ต์ ๋ฒ์ ์ v4.7์
๋๋ค.
TanStack Query v5์ ํ์ํ React ์ต์ ๋ฒ์ ์ v18์
๋๋ค.
useSyncExternalStore ํ
์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค.TanStack Query v5์ ๋ธ๋ผ์ฐ์ ๋ณ ์ง์ ๋ฒ์ ์ ์๋์ ๊ฐ์ต๋๋ค.
Chrome >= 91
Firefox >= 90
Edge >= 91
Safari >= 15
iOS >= 15
Opera >= 77
19 followers ยท starred Jul 2024
28 followers ยท starred Jan 2025
59 followers ยท starred Jun 2026
7 followers ยท starred Apr 2023
๐ @tanstack/react-query ์์ ์์ฃผ ์ฌ์ฉ๋๋ ๊ฐ๋ ์ ๋ฆฌ
1,709
194 commits
updated Sep 20, 2026
Pull Request, Issue๋ก ์์ ๋กญ๊ฒ ๋จ๊ฒจ์ฃผ์ธ์. ๊ฒํ ํ์ ๋ฐ์ํ๊ฒ ์ต๋๋ค.v5๊ฐ 23.10.17์ ๋ฆด๋ฆฌ์ฆ๋์ต๋๋ค. ํด๋น ๋ฌธ์๋ v5 ๊ธฐ์ค์ผ๋ก ์์ฑ๋์ด ์์ต๋๋ค.v4 ๋ฌธ์๋ react query tutorial v4 ๋ฌธ์๋ฅผ ํ์ธํด ์ฃผ์ธ์.v3 -> v4, v4 -> v5 ๋ง์ด๊ทธ๋ ์ด์
์ ๋ฆฌ๋ ์๋ ๋ฌธ์๋ฅผ ํ์ธํด ์ฃผ์ธ์.
์๋ฒ ์ํ ๊ฐ์ ธ์ค๊ธฐ, ์บ์ฑ, ๋๊ธฐํ ๋ฐ ์
๋ฐ์ดํธ๋ฅผ ๋ณด๋ค ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ํด๋ผ์ด์ธํธ ์ํ์ ์๋ฒ ์ํ๋ฅผ ๋ช
ํํ ๊ตฌ๋ถํ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋ค.redux, mobX๊ฐ ํด๋ผ์ด์ธํธ ์ํ ์์
์ ์ ํฉํ์ง๋ง, ๋น๋๊ธฐ ๋๋ ์๋ฒ ์ํ ์์
์๋ ๊ทธ๋ค์ง ์ข์ง ์๋ค๊ณ ์ธ๊ธํ๋ค.import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
// ...
},
},
});
์บ์์ ์ํธ ์์ฉํ ์ ์๋ค.query ๋๋ mutation์ ๊ธฐ๋ณธ ์ต์
์ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ์ข
๋ฅ๊ฐ ์๋นํ๋ฏ๋ก ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ๋ณด์.import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({ /* options */ });
function App() {
return (
<QueryClientProvider client={queryClient}>
<div>๋ธ๋ผ๋ธ๋ผ</div>
</QueryClientProvider>
);
}
QueryClientProvider๋ฅผ ์ต์๋จ์์ ๊ฐ์ธ์ฃผ๊ณ QueryClient ์ธ์คํด์ค๋ฅผ client prop์ผ๋ก ๋ฃ์ด ์ ํ๋ฆฌ์ผ์ด์
์ ์ฐ๊ฒฐํด์ผ ํ๋ค.background ๊ณ์ธต์ด ๋๋ค.
์ ์ฉ devtools๋ฅผ ์ ๊ณตํ๋ฉฐ ๋ณ๋์ ํจํค์ง ์ค์น๊ฐ ํ์ํ๋ค.์๊ฐํํ๋ ๋ฐ ๋์์ด ๋๋ฉฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋๋ฒ๊น
์๊ฐ์ ์ ์ฝํ ์ ์๋ค.process.env.NODE_ENV === "development"์ธ ๊ฒฝ์ฐ์๋ง ์คํ๋๋ค. ์ฆ, ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์๋ํ๋๋ก ์ค์ ๋์ด ์์ผ๋ฏ๋ก ํ๋ก์ ํธ ๋ฐฐํฌ ์์ Devtools ์ฝ์
์ฝ๋๋ฅผ ์ ๊ฑฐํด ์ค ํ์๊ฐ ์๋ค.$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
# or
$ bun add @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
true์ด๋ฉด ๊ฐ๋ฐ ๋๊ตฌ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ ค ์๋๋ก ์ค์ ํ ์ ์๋ค.bottom-rightrelative์ผ ๋ ๋ฒํผ์ devtools๋ฅผ ๋ ๋๋งํ๋ ์์น์ ๋ฐฐ์น๋๋ค.* Query Instances with and without cache data(์บ์ ๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ์๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค)
* Background Refetching(๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฆฌํจ์นญ)
* Inactive Queries(๋นํ์ฑ ์ฟผ๋ฆฌ)
* Garbage Collection(๊ฐ๋น์ง ์ปฌ๋ ์
)
const result = useQuery({
queryKey: ["A"],
queryFn,
staleTime: 0, // (๊ธฐ๋ณธ๊ฐ 0์ด)
gcTime: 1000 * 60 * 5, // (๊ธฐ๋ณธ๊ฐ 5๋ถ)
});
A๋ผ๋ queryKey๋ฅผ ๊ฐ์ง ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ mount๋ฉ๋๋ค.A๋ผ๋ queryKey๋ก ์บ์ฑ๋ฉ๋๋ค.staleTime์ด 0์ด๋ฏ๋ก, ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์๋ง์ ๋ฐ๋ก stale ์ํ๋ก ์ ํ๋ฉ๋๋ค.unmount๋๋ฉด, TanStack Query๋ ํด๋น ์บ์๋ฅผ gcTime ๋์ ์ ์งํฉ๋๋ค.A ์ฟผ๋ฆฌ๊ฐ ๋ค์ mount๋๋ค๋ฉด, ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ๋ฐํํ๊ณ , ๋์์ queryFn์ background์์ ์คํ๋ฉ๋๋ค.
queryFn์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋๋ฉด ์บ์๋ฅผ ์ต์ ๋ฐ์ดํฐ๋ก ์ฑ์ฐ๋ฉฐ, gcTime์ ๋ค์ 5๋ถ์ผ๋ก ์ด๊ธฐํ๋ฉ๋๋ค.mount๋์ง ์์ผ๋ฉด, ํด๋น ์บ์๋ ๊ฐ๋น์ง ์ปฌ๋ ์
์ ์ํด ์ญ์ ๋ฉ๋๋ค.v5๋ถํฐ ์ธ์๋ก ๋จ ํ๋์ ๊ฐ์ฒด๋ง ๋ฐ์ผ๋ฉฐ, ๊ทธ์ค queryKey์ queryFn์ด ํ์ ๊ฐ์ด๋ค.const result = useQuery({
queryKey, // required
queryFn, // required
// ...options ex) gcTime, staleTime, select, ...
});
result.data;
result.isLoading;
result.refetch;
// ...
// ์ค์ ์์
// ๐ก queryFn์ ๋ฐํ ํ์
์ ์ง์ ํด์ฃผ๋ฉด useQuery์ ํ์
์ถ๋ก ์ด ์ํํฉ๋๋ค.
const getAllSuperHero = async (): Promise<AxiosResponse<Hero[]>> => {
return await axios.get("http://localhost:4000/superheroes");
};
const { data, isLoading } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
1. queryKey
// (1) queryKey๋ ๋ฐ์ดํฐ๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ ๋ฟ ์๋๋ผ, ์๋์ ๊ฐ์ด ์ฟผ๋ฆฌ ํจ์์ ํธ๋ฆฌํ๊ฒ ์ ๋ฌํ ์๋ ์๋ค.
const getSuperHero = async ({
queryKey,
}: {
queryKey: ["super-hero", number];
}): Promise<AxiosResponse<Hero>> => {
const heroId = queryKey[1]; // ex) queryKey: ["super-hero", "3"]
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: getSuperHero, // (*)
});
};
๋ฐฐ์ด๋ก ์ง์ ํด ์ค์ผ ํ๋ค.
// An individual todo
useQuery({ queryKey: ["todo", 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ["todo", 5, { preview: true }], ...})
queryKey๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฟผ๋ฆฌ ์บ์ฑ์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ํต์ฌ์ด๋ค.
์์กดํ๋ค๋ฉด ๋ฐฐ์ด์ ์ด์ด์ ์ถ๊ฐํด์ผ ํ๋ค. ex: ["super-hero", heroId, ...]queryClient.setQueryData ๋ฑ๊ณผ ๊ฐ์ด ํน์ ์ฟผ๋ฆฌ์ ์ ๊ทผ์ด ํ์ํ ๋ ์ด๊ธฐ์ ์ค์ ํด๋ ํฌ๋งท์ ์ง์ผ์ค์ผ ์ ๋๋ก ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค.["super-hero", heroId]์ด๋ค. ๊ทธ๋ ๋ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์ฉํ ๋ ๋๊ฐ์ด ["super-hero", heroId] ํฌ๋งท์ ๊ฐ์ ธ์ผ ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ํ๋ ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค.2. queryFn
Promise๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ๋ฃ์ด์ผ ํ๋ค.// (2) ์๋จ์ queryKey ์์ ์ ๋ฐ๋๋ก queryFn ์์ฒด์ ์ผ๋ก ์ธ์๋ฅผ ๋ฐ๋ ํํ
const getSuperHero = async (heroId: string): Promise<AxiosResponse<Hero>> => {
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId), // (*)
});
};
3. options
options์ ๋ง์ด ์ฐ์ด๋ ์ต์
๋ค์ ์ฐจ๊ทผ์ฐจ๊ทผ ์ดํด๋ณผ ์์ ์ด๋ค. ๋ฌธ์ ์ธ์ ๋์ฑ ์์ธํ ์๊ณ ์ถ๋ค๋ฉด ์ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์.const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
gcTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
retry: 1,
// ... options
});
};
const {
data,
error,
status,
isPending,
fetchStatus,
isLoading,
isFetching,
isError,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
Promise์์ resolved๋ ๋ฐ์ดํฐdata, ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ์ ๋ํ ์ํ๋ฅผ ํํํ๋ status๋ ๋ฌธ์์ด ํํ๋ก 3๊ฐ์ง์ ๊ฐ์ด ์กด์ฌํ๋ค.
{ enabled: false } ์ํ๋ก ์ฟผ๋ฆฌ๊ฐ ํธ์ถ๋๋ฉด ์ด ์ํ๋ก ์์๋๋ค.true๋ฅผ ๋ฐํํ๋ค.
queryFn์ ๋ํ ์ ๋ณด๋ฅผ ๋ํ๋
์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์์ ๋ ์ฆ, ์ฒ์ ์คํ๋ ์ฟผ๋ฆฌ์ผ ๋ ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ true/false๋ก ๋ฐํ๋๋ค.
false๋ฅผ ๋ฐํํ๋ค.isFetching && isPending๊ณผ ๋์ผํ๋ค.true๋ฅผ ๋ฐํํ๋ค.
truetruesuccess ์ํ๋ผ๋ฉด ์ฃผ๋ก fetchStatus๋ idle ์ํ์ง๋ง, ๋ฐฑ๊ทธ๋ผ์ด๋์์ re-fetch๊ฐ ๋ฐ์ํ ๋ fetching ์ํ์ผ ์ ์๋ค.pending ์ํ์ผ ๋ fetchStatus๋ ์ฃผ๋ก fetching ์ํ์ง๋ง, ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋์ด ์์ง ์์ ๊ฒฝ์ฐ paused ์ํ๋ฅผ ๊ฐ์ง ์ ์๋ค.data๊ฐ ์๋์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.queryFn ์์ฒญ์ด ์งํ ์ค์ธ์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.useQuery({ queryKey, queryFn, enabled: false });
// isPending: true, isLoading: false
์ค๋๋์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๊ฐ ์๋๋ผ๋ ์๋ฏธ์ด๋ค.์ ์ ํ์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๋ผ๋ ์๋ฏธ์ด๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
gcTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
});
(number | Infinity)
fresh์์ stale ์ํ๋ก ๋ณ๊ฒฝ๋๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ, ๋ง์ฝ staleTime์ด 3000์ด๋ฉด fresh ์ํ์์ 3์ด ๋ค์ stale๋ก ๋ณํfresh ์ํ์ผ ๋๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ์๋กญ๊ฒ mount ๋์ด๋ ๋คํธ์ํฌ ์์ฒญ(fetch)์ด ์ผ์ด๋์ง ์๋๋ค.0์ด๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก fetch ํ์ ๋ฐ๋ก stale์ด ๋๋ค.(number | Infinity)
inactive ์ํ์ผ ๋ ์บ์ฑ ๋ ์ํ๋ก ๋จ์์๋ ์๊ฐ(๋ฐ๋ฆฌ์ด)์ด๋ค.inactive ์ํ๋ก ๋ณ๊ฒฝ๋๋ฉฐ, ์บ์๋ gcTime๋งํผ ์ ์ง๋๋ค.๊ฐ๋น์ง ์ปฌ๋ ํฐ์ ์ํด ์์ง๋๋ค.inactive ๋ ์์ ์ ๊ธฐ์ค์ผ๋ก ์บ์ ๋ฐ์ดํฐ ์ญ์ ๋ฅผ ๊ฒฐ์ ํ๋ค.5๋ถ์ด๋ค. SSR ํ๊ฒฝ์์๋ Infinity์ด๋ค.0์ด์ 5๋ถ์ด๋ผ๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ staleTime์ ์ด๋ ํ ์ค์ ๋ ํ์ง ์์ผ๋ฉด ํด๋น ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ(Observer)๊ฐ mount ๋์ ๋ ๋งค๋ฒ ๋ค์ API๋ฅผ ์์ฒญํ ๊ฒ์ด๋ค.staleTime์ gcTime๋ณด๋ค ์๊ฒ ์ค์ ํ๋ ๊ฒ์ด ์ข๋ค.๋ ์๊ฒฌ์ ๋์ํ์ง ์๋๋ค๊ณ ํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnMount: true,
});
boolean | "always" | ((query: Query) => boolean | "always")stale ์ํ์ผ ๊ฒฝ์ฐ, mount๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์
์ด๋ค.true์ด๋ค.always๋ก ์ค์ ํ๋ฉด ๋ง์ดํธ ์๋ง๋ค ๋งค๋ฒ refetch๋ฅผ ์คํํ๋ค.false๋ก ์ค์ ํ๋ฉด ์ต์ด fetch ์ดํ์๋ refetch ํ์ง ์๋๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnWindowFocus: true,
});
boolean | "always" | ((query: Query) => boolean | "always")stale ์ํ์ผ ๊ฒฝ์ฐ ์๋์ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์
์ด๋ค.true์ด๋ค.always๋ก ์ค์ ํ๋ฉด ์๋์ฐ๊ฐ ํฌ์ปค์ฑ๋ ๋๋ง๋ค ํญ์ refetch๋ฅผ ์คํํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
Polling(ํด๋ง)์ด๋?
์ค์๊ฐ ์น์ ์ํ ๊ธฐ๋ฒ์ผ๋ก "์ผ์ ํ ์ฃผ๊ธฐ(ํน์ ํ ์๊ฐ)"๋ฅผ ๊ฐ์ง๊ณ ์๋ฒ์ ์๋ต์ ์ฃผ๊ณ ๋ฐ๋ ๋ฐฉ์์ด ํด๋ง ๋ฐฉ์์ด๋ค.
react-query์์๋ "refetchInterval", "refetchIntervalInBackground"์ ์ด์ฉํด์ ๊ตฌํํ ์ ์๋ค.
number | false | ((data: TData | undefined, query: Query) => number | false)์๊ฐ(ms)์ ๊ฐ์ผ๋ก ๋ฃ์ด์ฃผ๋ฉด ์ผ์ ์๊ฐ๋ง๋ค ์๋์ผ๋ก refetch๋ฅผ ์์ผ์ค๋ค.booleanrefetchInterval๊ณผ ํจ๊ป ์ฌ์ฉํ๋ ์ต์
์ด๋ค.const {
data,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
enabled: false,
});
const handleClickRefetch = useCallback(() => {
refetch();
}, [refetch]);
return (
<div>
{data?.data.map((hero: Data) => (
<div key={hero.id}>{hero.name}</div>
))}
<button onClick={handleClickRefetch}>Fetch Heroes</button>
</div>
);
boolean์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ์คํ๋์ง ์๋๋ก ํ ๋ ์ค์ ํ ์ ์๋ค.false๋ฅผ ์ฃผ๋ฉด ์ฟผ๋ฆฌ๊ฐ ์๋ ์คํ๋์ง ์๋๋ค.
pending ์ํ๋ก ์์ํ๋ค.์๋์ผ๋ก ๋ค์ ์์ฒญํ๋ ๊ธฐ๋ฅ์ด๋ค. ์ฟผ๋ฆฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ์ค๋ฅ๋ง ๊ธฐ๋ก๋๋ค.
throwOnError ์์ฑ์ true๋ก ํด์ ์ ๋ฌํด์ผ ํ๋ค.๋ฒํผ ํด๋ฆญ์ด๋ ํน์ ์ด๋ฒคํธ๋ฅผ ํตํด ์์ฒญ์ ์๋ํ ๋ ๊ฐ์ด ์ฌ์ฉํ๋ค.enabled: false๋ฅผ ์คฌ๋ค๋ฉด queryClient๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์ค invalidateQueries์ refetchQueries๋ฅผ ๋ฌด์ํ๋ค.const {
data,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
retry: 10, // ์ค๋ฅ๋ฅผ ํ์ํ๊ธฐ ์ ์ ์คํจํ ์์ฒญ์ 10๋ฒ ์ฌ์๋ํฉ๋๋ค.
});
(boolean | number | (failureCount: number, error: TError) => boolean)์คํจํ๋ฉด useQuery๋ฅผ ํน์ ํ์๋งํผ ์ฌ์์ฒญํ๋ ์ต์
์ด๋ค.false์ธ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ์๋ํ์ง ์๋๋ค. true์ธ ๊ฒฝ์ฐ์๋ ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด ๋ฌดํํ ์ฌ์์ฒญ์ ์๋ํ๋ค.์ซ์๋ฅผ ๋ฃ์ ๊ฒฝ์ฐ, ์คํจ ํ์๊ฐ ํด๋น ์ซ์์ ๋๋ฌํ ๋๊น์ง ์์ฒญ์ ์ฌ์๋ํ๋ค.3, ์๋ฒ ํ๊ฒฝ์์๋ 0์ด๋ค.v4๊น์ง ์๋ onSuccess, onError, onSettled Callback์ useQuery ์ต์
์์ ์ ๊ฑฐ๋๋ค. ๋จ, useMutation์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
return (
<div>
{data.map((heroName, idx) => (
<div key={`${heroName}-${idx}`}>{heroName}</div>
))}
</div>
);
(data: TData) => unknown๋ฐํ๋ ๋ฐ์ดํฐ์ ์ผ๋ถ๋ฅผ ๋ณํํ๊ฑฐ๋ ์ ํํ ์ ์๋ค.const placeholderData = useMemo(() => generateFakeHeroes(), []);
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: placeholderData,
});
TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TDatapending ์ํ์ธ ๋์ ํน์ ์ฟผ๋ฆฌ์ ๋ํ placeholder data๋ก ์ฌ์ฉ๋๋ค.v4๊น์ง ์๋ keepPreviousData๋ ํ์ด์ง๋ค์ด์
๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ๋ง์ด ์ฌ์ฉํ๋ ์ต์
์ด์๋ค. ์บ์ฑ ๋์ง ์์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ๋ ๋ชฉ๋ก์ด ๊น๋นก๊ฑฐ๋ฆฌ๋ ํ์์ ๋ฐฉ์งํ ์ ์๋ค.
ํ์ง๋ง, v5๋ถํฐ keepPreviousData์ isPreviousData ์ต์
์ ์ ๊ฑฐ๋๋ค.
์ด๋ค์ ๊ฐ๊ฐ placeholderData์ isPlaceholderData ํ๋๊ทธ์ ๊ฑฐ์ ์ ์ฌํ๊ฒ ๋์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์๋ ์์ ์ฒ๋ผ placeholderData๋ฅผ ํ์ฉํ๋ฉด์ ์ด์ ๋ฒ์ ์์ keepPreviousData์ ๊ฐ์ "true"๋ก ์คฌ์ ๋์ ๋์ผํ ๊ธฐ๋ฅ์ ์ํํ ์ ์๋ค.
import { useQuery, keepPreviousData } from "@tanstack/react-query";
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: keepPreviousData,
});
import { useQuery } from "@tanstack/react-query";
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: (previousData, previousQuery) => previousData,
});
import { useQuery } from "@tanstack/react-query";
const { data, dataUpdatedAt } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
notifyOnChangeProps: ["data"], // data ๊ฐ ๋ณ๊ฒฝ ์์๋ง ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค
});
string[] | "all" | (() => string[] | "all")notifyOnChangeProps์ ์ค์ ๊ฐ์ ์ฃผ์ง ์์๋ค๋ฉด, data, dataUpdatedAt ์ค ์ด๋ ํ๋๊ฐ ๋ณ๊ฒฝ๋๋ฉด ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค."all"๋ก ์ค์ ํ ๊ฒฝ์ฐ, ์ฟผ๋ฆฌ์ ์ด๋ค ํ๋กํผํฐ๊ฐ ๋ณ๊ฒฝ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋๋ค.const { data: superHeroes } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
const { data: friends } = useQuery({
queryKey: ["friends"],
queryFn: getFriends,
});
๋ณ๋ ฌ๋ก ์์ฒญ๋์ด ์ฒ๋ฆฌ๋๋ค.๋์์ฑ์ ๊ทน๋ํ์ํจ๋ค.const queryResults = useQueries({
queries: [
{
queryKey: ["super-hero", 1],
queryFn: () => getSuperHero(1),
staleTime: Infinity, // ๋ค์๊ณผ ๊ฐ์ด option ์ถ๊ฐ ๊ฐ๋ฅ!
},
{
queryKey: ["super-hero", 2],
queryFn: () => getSuperHero(2),
staleTime: 0,
},
// ...
],
});
useQueries๋ฅผ ์ฌ์ฉํ๋ค.useQueries ํ ์ด ๋ฐํํ ๋ชจ๋ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ ํฌํจ๋ ๋ฐฐ์ด์ ๋จ์ผ ๊ฐ์ผ๋ก ๊ฒฐํฉํ๋ ค๋ฉด combine ์ต์ ์ ์ฌ์ฉํ ์ ์๋ค.
const ids = [1, 2, 3];
const combinedQueries = useQueries({
queries: ids.map((id) => ({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
})),
combine: (results) => {
return {
data: results.map((result) => result.data),
pending: results.some((result) => result.isPending),
};
},
});
data์ pending ํ๋กํผํฐ๋ฅผ ๊ฐ๋๋ค.์ข
์ ์ฟผ๋ฆฌ๋ ๋ค๋ฅธ ์ฟผ๋ฆฌ๊ฐ ๋จผ์ ์๋ฃ๋์ด์ผ๋ง ์คํํ ์ ์๋ ์ฟผ๋ฆฌ๋ฅผ ๋งํ๋ค. ์๋ฅผ ๋ค์ด A ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ธฐ ์ ์ B ์ฟผ๋ฆฌ๊ฐ ์๋ฃ๋์ด์ผ ํ๋ค๋ฉด, B์ ์์กดํ๋ A๊ฐ ์ข
์ ์ฟผ๋ฆฌ์ด๋ค.enabled ์ต์
์ ํตํด ์ข
์ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.// ์ฌ์ ์ ์๋ฃ๋์ด์ผ ํ ์ฟผ๋ฆฌ
const { data: user } = useQuery({
queryKey: ["user", email],
queryFn: () => getUserByEmail(email),
});
const channelId = user?.data.channelId;
// user ์ฟผ๋ฆฌ์ ์ข
์๋ ์ฟผ๋ฆฌ
const { data: courses } = useQuery({
queryKey: ["courses", channelId],
queryFn: () => getCoursesByChannelId(channelId),
enabled: !!channelId,
});
QueryClient ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค.QueryClient๋ ์บ์์ ์ํธ์์ฉํ๋ค.import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์บ์์ ์ ๊ณตํ๋ ๋ฐฉ๋ฒ์ด ์๋ค.const useSuperHeroData = (heroId: string) => {
const queryClient = useQueryClient();
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
initialData: () => {
const queryData = queryClient.getQueryData(["super-heroes"]) as any;
const hero = queryData?.data?.find(
(hero: Hero) => hero.id === parseInt(heroId)
);
if (hero) return { data: hero };
},
});
};
queryClient.getQueryData ๋ฉ์๋๋ ๊ธฐ์กด ์ฟผ๋ฆฌ์ ์บ์ฑ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ธฐ ํจ์์ด๋ค. ์ฟผ๋ฆฌ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด undefined๋ฅผ ๋ฐํํ๋ค.UX์ ์ข์ ์ํฅ์ ์ค ์ ์๋ค.
queryClient.prefetchQuery๋ฅผ ํตํด prefetch ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.const queryClient = useQueryClient();
const prefetchNextPosts = async (nextPage: number) => {
// ํด๋น ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๋ ์ผ๋ฐ ์ฟผ๋ฆฌ๋ค์ฒ๋ผ ์บ์ฑ ๋๋ค.
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
// ...options
});
};
// ๋จ์ ์
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
์ด๋ฏธ ์บ์ฑ ๋์ด ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ค.์ฒซ ๋ฒ์งธ ํ์ด์ง๋ง prefetch ๋๋ฉฐ, ๊ทธ ์ด์์ prefetch ํ๋ ค๋ฉด pages ์ต์
์ ํ์ฉํด์ผ ํ๋ค.
getNextPageParam ํจ์๋ฅผ ๋ฌด์กฐ๊ฑด ์ ๊ณตํด ์ค์ผ ํ๋ค๋ ์ ์ ์ฃผ์ํ์.const prefetchTodos = async () => {
await queryClient.prefetchInfiniteQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
});
};
๋ฌดํ ์คํฌ๋กค์ด๋ load more(๋ ๋ณด๊ธฐ)๊ณผ ๊ฐ์ด ํน์ ์กฐ๊ฑด์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ๋ฉด ์ ์ฉํ๋ค.useInfiniteQuery๋ฅผ ์ ๊ณตํ๋ค.import { useInfiniteQuery } from "@tanstack/react-query";
// useInfiniteQuery์ queryFn์ ๋งค๊ฐ๋ณ์๋ `pageParam`์ด๋ผ๋ ํ๋กํผํฐ๋ฅผ ๊ฐ์ง ์ ์๋ค.
const fetchColors = async ({
pageParam,
}: {
pageParam: number;
}): Promise<AxiosResponse<PaginationColors>> => {
return await axios.get(`http://localhost:4000/colors?page=${pageParam}`);
};
const InfiniteQueries = () => {
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: fetchColors,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 ? allPages.length + 1 : undefined;
},
// ...
});
return (
<div>
{data?.pages.map((group, idx) => ({
/* ... */
}))}
<div>
<button disabled={!hasNextPage} onClick={() => fetchNextPage()}>
LoadMore
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
</div>
);
};
useInfiniteQuery๋ ๊ธฐ๋ณธ์ ์ผ๋ก useQuery์ ์ฌ์ฉ๋ฒ์ ๋น์ทํ์ง๋ง, ์ฐจ์ด์ ์ด ์๋ค.isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage ๋ฑ์ด ์ถ๊ฐ์ ์ผ๋ก ์๋ค.
๋ค์ ํ์ด์ง๋ฅผ fetch ํ ์ ์๋ค.์ด์ ํ์ด์ง๋ฅผ fetch ํ ์ ์๋ค.fetchNextPage ๋ฉ์๋๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค.fetchPreviousPage ๋ฉ์๋๊ฐ ์ด์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค.๋ค์ ํ์ด์ง๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.์ด์ ํ์ด์ง๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.TPageParamํ์๊ฐ์ด๋ค.(lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | nullํ์๊ฐ์ด๋ค.
lastPage๋ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฐ์ ธ์จ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค.allPages๋ ํ์ฌ๊น์ง ๊ฐ์ ธ์จ ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค.lastPageParam์ ๊ฐ์ฅ ์ต๊ทผ์ ์ฌ์ฉ๋ ํ์ด์ง ๋งค๊ฐ๋ณ์์ด๋ค.allPageParams๋ ํ์ฌ๊น์ง ์ฌ์ฉ๋ ๋ชจ๋ ํ์ด์ง ๋งค๊ฐ๋ณ์์ด๋ค.undefined ๋๋ null์ ๋ฐํํ๋ฉด ๋๋ค.getPreviousPageParam๋ ์กด์ฌํ๋ฉฐ, getNextPageParam๊ณผ ๋ฐ๋์ ์์ฑ์ ๊ฐ๊ณ ์๋ค.number | undefined์ต๋ ํ์ด์ง ์์ด๋ค.์ง์ ๋ ๋ฐฉํฅ(next, previous)์ ๋ฐ๋ผ ํ์ด์ง ๋ฐฐ์ด์์ ์ฒซ ๋ฒ์งธ ํ์ด์ง ๋๋ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์ ๊ฑฐ๋๋ค.0 ๋๋ undefined๋ผ๋ฉด ํ์ด์ง ์๋ ๋ฌด์ ํ์ด๋ค.queryFn์ ๋๊ฒจ์ฃผ๋ pageParam๊ฐ ๋จ์ํ ๋ค์ page์ ๊ฐ๋ง์ ๊ด๋ฆฌํ ์ ์๋ ๊ฒ์ ์๋๋ค.getNextPageParam์์ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝ์์ผ ์ค ์ ์๋ค.const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: ({ pageParam }) => fetchColors(pageParam), // pageParam: { page: number; etc: string }
initialPageParam: {
page: 1,
etc: "hi",
},
getNextPageParam: (lastPage, allPages) => {
if (allPages.length >= 4) return undefined;
return {
page: allPages.length + 1,
etc: "bye",
};
},
});
queryFn์ ๋ฃ์ pageParam์์ getNextPageParam์์ ๋ฐํํ ๊ฐ์ฒด๋ฅผ ๋ฐ์์ฌ ์ ์๋ค.const fetchColors = async ({
page,
etc,
}: {
page: number;
etc: string;
}): Promise<AxiosResponse<PaginationColors>> => {
return await axios.get(
`http://localhost:4000/colors?page=${page}&etc=${etc}`
);
};
R(read)๋ useQuery, CUD(Create, Update, Delete)๋ useMutation์ ์ฌ์ฉํ๋ค.const { mutate } = useMutation({
mutationFn: createTodo,
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutate({ title });
};
mutate ํจ์๋ฅผ ์ด์ฉํด์ ์์ฒญ ํจ์๋ฅผ ํธ์ถํ ์ ์๋ค.onSuccess, onError ์ฝ๋ฐฑ์ ํตํด ์ฑ๊ณตํ์ ๋์ ์คํจํ์ ๋์ response ๋ฐ์ดํฐ๋ฅผ ํธ๋ค๋งํ ์ ์๋ค.onMutate๋ mutation ํจ์๊ฐ ์คํ๋๊ธฐ ์ ์ ์คํ๋๊ณ , mutation ํจ์๊ฐ ๋ฐ์ ๋์ผํ ๋ณ์๊ฐ ์ ๋ฌ๋๋ค.onSettled๋ try...catch...finally ๊ตฌ๋ฌธ์ finally์ฒ๋ผ ์์ฒญ์ด ์ฑ๊ณตํ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์๊ด์์ด ๋ง์ง๋ง์ ์คํ๋๋ค.const mutation = useMutation({ mutationFn: addTodo });
try {
const todo = await mutation.mutateAsync(todo);
console.log(todo);
} catch (error) {
console.error(error);
} finally {
console.log("done");
}
mutateAsync๋ฅผ ์ฌ์ฉํด์ ์ป์ด์ฌ ์ ์๋ค.unhandled promise rejection ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ค.useMutation์ Callback -> mutate์ Callback ์์ผ๋ก ์คํ๋๋ค.TkDodo๋ ์์ ๊ฐ์ ์ด์ ๋ก ๋์ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์ ํ๋ค๊ณ ํ๋ค.
์ฟผ๋ฆฌ ์ด๊ธฐํ)์ useMutation์ Callback์ผ๋ก ์คํ์ํจ๋ค.์๋์ผ๋ก ์ทจ์ํ๊ณ ์ถ์ ์๋ ์๋ค.
queryClient.cancelQueries({ queryKey })๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ๋ํ react-query๋ ์ฟผ๋ฆฌ ์ทจ์๋ฟ๋ง ์๋๋ผ queryFn์ Promise๋ ์ทจ์ํ๋ค.const query = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
const queryClient = useQueryClient();
const onCancelQuery = (e) => {
e.preventDefault();
queryClient.cancelQueries({
queryKey: ["super-heroes"],
});
};
return <button onClick={onCancelQuery}>Cancel</button>;
์์ฑ(Post)ํ๊ฑฐ๋ ๊ฒ์๊ธ์ ์ ๊ฑฐ(Delete)ํ์ ๋ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ํ ๋ชฉ๋ก์ ์ค์๊ฐ์ผ๋ก ์ต์ ํํด์ผ ํ ๋๊ฐ ์๋ค.query Key๊ฐ ๋ณํ์ง ์์ผ๋ฏ๋ก ๊ฐ์ ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ๊ณ ์ต์ ํ๋ฅผ ์งํํด์ผ ํ๋๋ฐ, ์ด๋ฐ ๊ฒฝ์ฐ์ invalidateQueries() ๋ฉ์๋๋ฅผ ์ด์ฉํ ์ ์๋ค.refetch๋ฅผ ํ ๋ ์ฌ์ฉํ๋ค!import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onSuccess(data) {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // ์ด key์ ํด๋นํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ!
console.log(data);
},
onError(err) {
console.log(err);
},
});
};
["super-heroes"]์ ๋๊ฒจ์ฃผ๋ฉด queryKey์ "super-heroes"๋ฅผ ํฌํจํ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ๋๋ค.queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
// ์๋ ์ฟผ๋ฆฌ๋ค์ด ๋ชจ๋ ๋ฌดํจํ๋๋ค.
const supermanQuery = useQuery({
queryKey: ["super-heroes", "superman"],
queryFn: fetchSuperHero,
});
const heroQuery = useQuery({
queryKey: ["super-heroes", { id: 1 }],
queryFn: fetchSuperHero,
});
enabled/refetch์์๋ ์ธ๊ธํ์ง๋ง enabled: false ์ต์
์ ์ฃผ๋ฉด queryClient๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์ค invalidateQueries์ refetchQueries๋ฅผ ๋ฌด์ํ๋ค.
queryClient.invalidateQueries๋ฅผ ์ด์ฉํด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ดค๋๋ฐ queryClient.setQueryData๋ฅผ ์ด์ฉํด์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์
๋ฐ์ดํธํ ์ ์๋ค.queryClient.setQueryData๋ ์ฟผ๋ฆฌ์ ์บ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์
๋ฐ์ดํธํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ธฐ ํจ์์ด๋ค.updater ํจ์์ด๋ค. ํด๋น ํจ์์ ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ oldData๋ก ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onSuccess(data) {
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [...oldData.data, data.data],
};
});
},
onError(err) {
console.log(err);
},
});
};
Optimistic Update(๋๊ด์ ์
๋ฐ์ดํธ)๋ ์๋ฒ ์
๋ฐ์ดํธ ์ UI์์๋ ์ด์ฐจํผ ์
๋ฐ์ดํธํ ๊ฒ์ด๋ผ๊ณ (๋๊ด์ ์ธ) ๊ฐ์ ํด์ ๋ฏธ๋ฆฌ UI๋ฅผ ์
๋ฐ์ดํธ ์์ผ์ฃผ๊ณ ์๋ฒ๋ฅผ ํตํด ๊ฒ์ฆ๋ฐ๊ณ ์
๋ฐ์ดํธ ๋๋ ๋กค๋ฐฑํ๋ ๋ฐฉ์์ด๋ค.const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onMutate: async (newHero: any) => {
await queryClient.cancelQueries({ queryKey: ["super-heroes"] });
// ์ด์ ๊ฐ
const previousHeroData = queryClient.getQueryData(["super-heroes"]);
// ์๋ก์ด ๊ฐ์ผ๋ก ๋๊ด์ ์
๋ฐ์ดํธ ์งํ
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [
...oldData.data,
{ ...newHero, id: oldData?.data?.length + 1 },
],
};
});
// ๊ฐ์ด ๋ค์ด์๋ context ๊ฐ์ฒด๋ฅผ ๋ฐํ
return { previousHeroData };
},
// mutation์ด ์คํจํ๋ฉด onMutate์์ ๋ฐํ๋ context๋ฅผ ์ฌ์ฉํ์ฌ ๋กค๋ฐฑ ์งํ
onError(error, hero, context: any) {
queryClient.setQueryData(["super-heroes"], context.previousHeroData);
},
// ์ค๋ฅ ๋๋ ์ฑ๊ณต ํ์๋ ํญ์ refetch
onSettled() {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
},
});
};
cancelQueries๋ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ทจ์์ํฌ ์ ์๋ค. ์ทจ์์ํฌ query์ queryKey๋ฅผ cancelQueries์ ์ธ์๋ก ๋ณด๋ด ์คํ์ํจ๋ค.์ ์ธ์ ์ผ๋ก ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.useQueryErrorResetBoundary๋ ErrorBoundary์ ํจ๊ป ์ฌ์ฉํ๋ค. ErrorBoundary๋ ๋ฆฌ์กํธ ๊ณต์ ๋ฌธ์์์๋ ๊ธฐ๋ณธ ์ฝ๋๋ฅผ ์ ๊ณตํ์ง๋ง, ์ข ๋ ์ฝ๊ฒ ํ์ฉํ ์ ์๋ react-error-boundary ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ค. react-query ๊ณต์ ๋ฌธ์์์๋ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์์๋ก ์ฌ์ฉํ๊ณ ์์ผ๋, ์ฌ๊ธฐ์๋ react-error-boundary๋ฅผ ์ค์นํด์ ์ฌ์ฉํด ๋ณด์.$ npm i react-error-boundary
# or
$ pnpm add react-error-boundary
# or
$ yarn add react-error-boundary
# or
$ bun add react-error-boundary
useQueryErrorResetBoundary ํ
์ ํธ์ถํด reset ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค.fallbackRender prop์ผ๋ก ๋๊ธด ๋ด์ฉ์ด ๋ ๋๋ง ๋๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด children ๋ด์ฉ์ด ๋ ๋๋ง ๋๋ค.resetErrorBoundary๋ฅผ ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ํตํด ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ, ์ด๋ฅผ ํตํด ๋ชจ๋ ์ฟผ๋ฆฌ ์๋ฌ๋ฅผ ์ด๊ธฐํํ ์ ์๋ค. ์๋ ์ฝ๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ button์ ํด๋ฆญํ๋ฉด ์๋ฌ๋ฅผ ์ด๊ธฐํํ๊ฒ๋ ์์ฑํ๋ค.import { useQueryErrorResetBoundary } from "@tanstack/react-query"; // (*)
import { ErrorBoundary } from "react-error-boundary"; // (*)
interface Props {
children: React.ReactNode;
}
const QueryErrorBoundary = ({ children }: Props) => {
const { reset } = useQueryErrorResetBoundary(); // (*)
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
Error!!
<button onClick={() => resetErrorBoundary()}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
};
export default QueryErrorBoundary;
{ throwOnError: true }๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ErrorBoundary ์ปดํฌ๋ํธ๊ฐ ๊ฐ์งํ ์ ์๋ค.import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true, // (*) ์ฌ๊ธฐ์๋ ๊ธ๋ก๋ฒ๋ก ์ธํ
ํ์ง๋ง, ๊ฐ๋ณ ์ฟผ๋ฆฌ๋ก ์ธํ
๊ฐ๋ฅ
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<QueryErrorBoundary>{/* ํ์ ์ปดํฌ๋ํธ๋ค */}</QueryErrorBoundary>
</QueryClientProvider>
);
}
์ ์ธ์ ์ผ๋ก ์์ฑํ ์ ์๊ณ , ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋ Suspense์๋ ๊ฒฐํฉํด์ ์๋ฒ ํต์ ์ํ๊ฐ ๋ก๋ฉ ์ค์ผ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๊ฒ ์ ์ธ์ ์ผ๋ก ์์ฑํ ์ ์๋ค.Component Lazy Loading์ด๋ Data Fetching ๋ฑ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํ ๋, ์๋ต์ ๊ธฐ๋ค๋ฆฌ๋ ๋์ Fallback UI(ex: Loader)๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ํ๋ ์ปดํฌ๋ํธ๋ค.import { Suspense } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// suspense: true, - ๐ก v5๋ถํฐ Deprecated
// useQuery/useInfiniteQuery์ ๊ฐ์ ์ผ๋ฐ ํ
๋์ useSuspenseQuery/useSuspenseInfiniteQuery์ ๊ฐ์ suspense ํ
์ฌ์ฉ
throwOnError: true,
},
},
});
function App() {
return (
<QueryErrorBoundary>
<Suspense fallback={<Loader />}>{/* ํ์ ์ปดํฌ๋ํธ๋ค */}</Suspense>
</QueryErrorBoundary>
);
}
๋ด๋ถ ๋ณต์ก์ฑ์ ์ถ์ํํ๋ ๊ฒ ๋ฐ๋ก ์ ์ธํ ์ปดํฌ๋ํธ์ด๋ค.react-query(useSuspenseQuery)์ ๊ฒฐํฉํ Suspense๋ ์๋์ ๊ฐ์ ๊ณผ์ ์ผ๋ก ๋์ํ๋ค.1. Suspense mount
2. MainComponent mount
3. MainComponent์์ useSuspenseQuery ํ
์ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ๋ฐ์ดํฐ ์์ฒญ
4. MainComponent unmount, fallback UI์ธ Loader mount
5. ๋น๋๊ธฐ ๋ฐ์ดํฐ ์์ฒญ์ด ์๋ฃ๋๋ฉด fallback UI์ธ Loader unmount
6. MainComponent mount
data fetching์ ๋ํ suspense๊ฐ ๋ง์นจ๋ด ์์ ํ๋์์ต๋๋ค.useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 3๊ฐ์ง ํ
์ด ์ถ๊ฐ๋์์ต๋๋ค.suspense ์ต์
์ ์ ๊ฑฐ๋์์ต๋๋ค. ๋ฐ๋ผ์ Suspense๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์ ํ
๋ค์ ํ์ฉํด์ผ ํฉ๋๋ค.data๊ฐ undefined ์ํ๊ฐ ๋์ง ์์ต๋๋ค.import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
const fetchGroups = async (): Promise<{ data: Group[] }> => {
const res = await axios.get("/groups");
return res;
};
// as-is
const { data } = useQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
// to-be
const { data } = useSuspenseQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
queryKey๋ฅผ ์ฌ์ฉํด ๊ฐ์ ธ์์ผ ํ ๋ฐ์ดํฐ๋ฅผ ์๋ณํ๊ณ ์ถ๋ค๋ฉด QueryClient์ queryFn ์ต์
์ ํตํด Default Query Function์ ์ง์ ํด ์ค ์ ์๋ค.// ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์
const getSuperHero = async ({ queryKey }: any) => {
const heroId = queryKey[1];
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: getSuperHero,
// ...queries options
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>{/* ... */}</QueryClientProvider>
);
}
QueryClient์ ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ ์ฟผ๋ฆฌ ํจ์๋ฅผ ์ง์ ํด ์ค๋ค.// ์ฌ์ฉ ์์
const useSuperHeroData = (heroId: string) => {
return useQuery({ queryKey: ["super-hero", heroId] });
};
// ๋ค์ ํํ๋ ๋ถ๊ฐ๋ฅ
const useSuperHeroData = (heroId: string) => {
return useQuery({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
});
};
queryKey๋ง ๋ฃ์ด์ฃผ๋ฉด queryFn์๋ ์๋์ผ๋ก ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ค์ด๊ฐ๋ค.useQuery๋ฅผ ์ฌ์ฉํ ๋์ ๋ฌ๋ฆฌ queryFn์ ์ง์ ํ์ง ์๊ธฐ์ ์ฟผ๋ฆฌ ํจ์์ ์ง์ ์ธ์๋ฅผ ๋ฃ์ด์ฃผ๋ ํํ์ ์ฌ์ฉ์ ๋ถ๊ฐ๋ฅํ๋ค.queryOptions์ mutationOptions๋ ์ฟผ๋ฆฌ/๋ฎคํ
์ด์
์ต์
์ ํ๊ณณ์ ๋ชจ์ ๋๊ณ , ์ฌ๋ฌ ๊ณณ์์ ํ์
์์ ํ๊ฒ ์ฌ์ฌ์ฉํ๊ธฐ ์ํ ํฌํผ์
๋๋ค.๊ทธ๋๋ก ๋ฐํํ๋ ํญ๋ฑ ํจ์์ด๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ๋ฐํ์ ๋น์ฉ์ด ์์ต๋๋ค. ์ฆ, ์จ์ ํ ํ์
์คํฌ๋ฆฝํธ๋ฅผ ์ํ ํฌํผ์
๋๋ค.queryOptions์ infiniteQueryOptions๋ v5.0.0๋ถํฐ, mutationOptions๋ v5.82.0๋ถํฐ ์ฌ์ฉํ ์ ์์ต๋๋ค.queryKey์ queryFn์ ์ปค์คํ
ํ
์์์ ๊ด๋ฆฌํ๋๋ฐ, ์ต์
์ ๋ณ๋ ํจ์๋ก ๋ถ๋ฆฌํ๋ฉด queryKey๊ฐ ๋ค๊ณ ์๋ ๋ฐ์ดํฐ ํ์
์ ๋ณด๊ฐ ์ฌ๋ผ์ง๋๋ค.// โ ์ผ๋ฐ ๊ฐ์ฒด๋ก ๋ถ๋ฆฌํ ๊ฒฝ์ฐ
const superHeroOptions = (heroId: string) => ({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
});
const hero = queryClient.getQueryData(superHeroOptions("1").queryKey);
// ^? unknown
queryOptions๋ก ๊ฐ์ธ๋ฉด ๋ฐํ๋ queryKey์ queryFn์ ๋ฐํ ํ์
์ด ํจ๊ป ๋ถ๊ธฐ ๋๋ฌธ์, ๋ช
๋ นํ API์์๋ ์ ๋ค๋ฆญ์ ์ง์ ๋ช
์ํ ํ์๊ฐ ์์ต๋๋ค.import { queryOptions } from "@tanstack/react-query";
export const superHeroOptions = (heroId: string) =>
queryOptions({
queryKey: ["super-hero", heroId],
queryFn: () => getSuperHero(heroId),
staleTime: 1 * 60 * 1000,
// ...options
});
const hero = queryClient.getQueryData(superHeroOptions("1").queryKey);
// ^? AxiosResponse<Hero> | undefined
queryClient ๋ฉ์๋์ ๊ทธ๋๋ก ๋๊ธธ ์ ์์ต๋๋ค.// ํ
useQuery(superHeroOptions("1"));
useSuspenseQuery(superHeroOptions("1"));
useQueries({
queries: [superHeroOptions("1"), superHeroOptions("2")],
});
// queryClient ๋ฉ์๋
queryClient.prefetchQuery(superHeroOptions("1"));
queryClient.ensureQueryData(superHeroOptions("1"));
// queryKey๋ง ํ์ํ ๋ฉ์๋์๋ `.queryKey`๋ฅผ ๊บผ๋ด์ ๋๊ฒจ์ค๋๋ค.
queryClient.setQueryData(superHeroOptions("1").queryKey, newHero);
queryClient.invalidateQueries({ queryKey: superHeroOptions("1").queryKey });
queryClient.setQueryData ๋ฑ์ ์ฌ์ฉํ ๋ ์ด๊ธฐ์ ์ค์ ํด๋ queryKey ํฌ๋งท์ ์ง์ผ์ผ ํ๋ค๊ณ ์ธ๊ธํ์ต๋๋ค. queryOptions๋ฅผ ์ฌ์ฉํ๋ฉด queryKey๋ฅผ ํ๊ณณ์์๋ง ๊ด๋ฆฌํ๋ฏ๋ก ์ด๋ฐ ์ค์๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.select ํจ์๋ฅผ ๋ถ์ด๋ ํจํด์ด ์์ต๋๋ค.const { data } = useQuery({
...superHeroOptions("1"),
select: (data) => data.data.name,
});
/**
* ํ์
์ถ๋ก ์ด ์ ์ง๋๋ฏ๋ก data๋ select์ ๋ฐํ ํ์
์ด ๋ฉ๋๋ค.
* data: string | undefined
*/
infiniteQueryOptions๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ฌ์ฉ๋ฒ์ queryOptions์ ๋์ผํฉ๋๋ค.v5.82.0๋ถํฐ ๋ฎคํ
์ด์
์๋ ๋์ผํ ํฌํผ์ธ mutationOptions๊ฐ ์ถ๊ฐ๋์ต๋๋ค. useMutation์ ๋๊ธธ ์ ์๋ ์ต์
์ ๊ทธ๋๋ก ๋๊ธธ ์ ์์ต๋๋ค.queryOptions์ ๋ฌ๋ฆฌ ์ถ๊ฐ์ ์ธ ํ์
์ ๋ณด๋ฅผ ๋ถ์ด์ง ์๊ณ ๋ฐ์ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํํฉ๋๋ค. ์ฆ, ์ต์
๊ฐ์ฒด์ ๋ํ ํ์
๊ฒ์ฌ์ ์๋ ์์ฑ์ ๋ฐ๊ธฐ ์ํ ํฌํผ์
๋๋ค.import { mutationOptions, useMutation } from "@tanstack/react-query";
export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
});
const { mutate } = useMutation(addSuperHeroOptions);
mutationKey๋ฅผ ์ง์ ํ๋ฉด ๋์ค์ useMutationState๋ useIsMutating์ผ๋ก ํด๋น ๋ฎคํ
์ด์
์ ์ํ๋ฅผ ์กฐํํ ์ ์์ต๋๋ค. ํ์ ์๋ค๋ฉด ์๋ตํด๋ ๋ฉ๋๋ค.import { mutationOptions, useMutationState } from "@tanstack/react-query";
export const addSuperHeroOptions = mutationOptions({
mutationKey: ["super-heroes", "add"],
mutationFn: addSuperHero,
});
// ์ ์ญ "์ ์ฅ ์ค..." ์ธ๋์ผ์ดํฐ
const SavingIndicator = () => {
const isAdding =
useMutationState({
filters: {
mutationKey: addSuperHeroOptions.mutationKey,
status: "pending",
},
}).length > 0;
return isAdding ? <span>Saving...</span> : null;
};
mutationOptions๋ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๊ธฐ ๋๋ฌธ์, ์คํ๋ ๋๋ก ์ฝ๋ฐฑ์ ๋ง๋ถ์ด๋ฉด ์๋ณธ ์ฝ๋ฐฑ์ด ๋ฎ์ด์จ์ง๋๋ค.export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
onSuccess: () => console.log("A"),
});
useMutation({
...addSuperHeroOptions,
onSuccess: () => console.log("B"), // โ "A"๋ ์คํ๋์ง ์์ต๋๋ค.
});
mutationOptions์ ๋๊ณ ์ปดํฌ๋ํธ ์ ์ฉ ๋ก์ง์ mutate์ Callback์ผ๋ก ๋๊ธฐ๋ ๊ฒ์ด ์ข์ต๋๋ค.export const addSuperHeroOptions = mutationOptions({
mutationFn: addSuperHero,
onSuccess: () => {
// ๊ณตํต ๋ก์ง
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
},
});
const { mutate } = useMutation(addSuperHeroOptions);
mutate(newHero, {
// ์ปดํฌ๋ํธ ์ ์ฉ ๋ก์ง
onSuccess: () => navigate("/super-heroes"),
});
์ ๋ค๋ฆญ(Generics)์ ๋ง์ด ์ฌ์ฉํ๋ค. ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๊ณ API๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ ์ ํ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.ํ์ฌ useQuery๊ฐ ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ์ 4๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ์ ํ์
์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์ select์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์
์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค.queryKey์ ํ์
์ ๋ช
์์ ์ผ๋ก ์ง์ ํด ์ฃผ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.// useQuery์ ํ์
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
import { AxiosError } from "axios";
// useQuery ํ์
์ ์ฉ ์์
const { data } = useQuery<
AxiosResponse<Hero[]>,
AxiosError,
string[],
["super-heroes", number]
>({
queryKey: ["super-heroes", id],
queryFn: getSuperHero,
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
/**
์ฃผ์ ํ์
* data: string[] | undefined
* error: AxiosError<any, any>
* select: (data: AxiosResponse<Hero[]>): string[]
*/
useMutation์ด ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ๋ useQuery์ ๋์ผํ๊ฒ 4๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.mutate ํจ์์ ์ ๋ฌํ ์ธ์๋ฅผ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
onMutate ํจ์์ return๊ฐ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.
export function useMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown
>
// useMutation ํ์
์ ์ฉ ์์
const { mutate } = useMutation<Todo, AxiosError, number, number>({
mutationFn: postTodo,
onSuccess: (res, id, nextId) => {},
onError: (err, id, nextId) => {},
onMutate: (id) => id + 1,
onSettled: (res, err, id, nextId) => {},
});
const onClick = () => {
mutate(5);
};
/**
์ฃผ์ ํ์
* data: Todo
* error: AxiosError<any, any>
* onSuccess: (res: Todo, id: number, nextId: number)
* onError: (err: AxiosError, id: number, nextId: number)
* onMutate: (id: number)
* onSettled: (res: Todo, err: AxiosError, id: number, nextId: number),
*/
ํ์ฌ useInfiniteQuery๊ฐ ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ์ 4๊ฐ์ด๋ฉฐ, useQuery์ ์ ์ฌํ๋ค.
์คํ ๊ฒฐ๊ณผ์ ํ์
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.error ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ์ ํ์
์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์ select์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์
์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค.queryKey์ ํ์
์ ๋ช
์์ ์ผ๋ก ์ง์ ํด ์ฃผ๋ ์ ๋ค๋ฆญ ํ์
์ด๋ค.export function useInfiniteQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey
>
const {
data,
hasNextPage,
fetchNextPage,
//...
} = useInfiniteQuery<
AxiosResponse<PaginationColors>,
AxiosError,
InfiniteData<AxiosResponse<PaginationColors>, number>,
["colors"]
>({
queryKey: ["colors"],
queryFn: fetchColors,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 ? allPages.length + 1 : undefined;
},
// ...options
});
/**
์ฃผ์ ํ์
* data: InfiniteData<AxiosResponse<PaginationColors>, number> | undefined
* error: AxiosError<any, any>
* select: (data: InfiniteData<AxiosResponse<PaginationColors>, number>): InfiniteData<AxiosResponse<PaginationColors>, number>
* getNextPageParam: GetNextPageParamFunction<number, AxiosResponse<PaginationColors>>
*/
queryFn์ ํ์
์ ์ ์ ์ํด์ ํ์
์ถ๋ก ์ด ์ํํ๊ฒ ๋๊ฒ ํ๋ ๊ฒ์ด๋ค.const fetchGroups = async (): Promise<AxiosResponse<Group[]>> => {
return await axios.get("/groups");
};
const { data } = useQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
/**
์ฃผ์ ํ์
* data: Group[] | undefined
* error: Error | null
* select: (data: AxiosResponse<Group[]>): Group[]
*/
์์ฒด ESLint Plugin์ ์ ๊ณตํฉ๋๋ค. ํด๋น ํ๋ฌ๊ทธ์ธ์ ํตํด ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์ ์ฉํ๊ณ , ์ผ๋ฐ์ ์ธ ์ค์๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.$ npm i -D @tanstack/eslint-plugin-query
# or
$ pnpm add -D @tanstack/eslint-plugin-query
# or
$ yarn add -D @tanstack/eslint-plugin-query
# or
$ bun add -D @tanstack/eslint-plugin-query
๊ถ์ฅํ๋ ๋ชจ๋ rule์ ์ ์ฉํ๋ ค๋ฉด ์๋์ ๊ฐ์ด .eslintrc.js ํ์ผ์ extends ๋ฐฐ์ด ์์ plugin:@tanstack/eslint-plugin-query/recommended๋ฅผ ์ถ๊ฐํฉ๋๋ค.module.exports = {
// ...
extends: ["plugin:@tanstack/eslint-plugin-query/recommended"],
rules: {
/* ์์ ํ๊ณ ์ ํ๋ rule ์ถ๊ฐ ๊ฐ๋ฅ... */
},
};
์ฌ์ฉ ๋ฐฉ๋ฒ(2)์ ๊ฐ์ด rule์ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.rule์ ๊ฐ๋ณ์ ์ผ๋ก ์ค์ ํด์ ์ ์ฉํ๋ ค๋ฉด ์๋์ ๊ฐ์ด .eslintrc.js ํ์ผ์ plugins ๋ฐฐ์ด ์์ @tanstack/query๋ฅผ ์ถ๊ฐํ๊ณ , ์ ์ฉํ๊ณ ์ ํ๋ rules์ ๊ท์น์ ์ถ๊ฐํฉ๋๋ค.module.exports = {
// ...
plugins: ["@tanstack/query"],
rules: {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-rest-destructuring": "warn",
"@tanstack/query/stable-query-client": "error",
},
};
TanStack Query v5์ ํ์ํ TypeScript ์ต์ ๋ฒ์ ์ v4.7์
๋๋ค.
TanStack Query v5์ ํ์ํ React ์ต์ ๋ฒ์ ์ v18์
๋๋ค.
useSyncExternalStore ํ
์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค.TanStack Query v5์ ๋ธ๋ผ์ฐ์ ๋ณ ์ง์ ๋ฒ์ ์ ์๋์ ๊ฐ์ต๋๋ค.
Chrome >= 91
Firefox >= 90
Edge >= 91
Safari >= 15
iOS >= 15
Opera >= 77
19 followers ยท starred Jul 2024
28 followers ยท starred Jan 2025
59 followers ยท starred Jun 2026
7 followers ยท starred Apr 2023