- ํด๋น ์ ์ฅ์๋ TanStack Query(React)์์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ ๋ค์ ์ ๋ฆฌํ ์ ์ฅ์์ ๋๋ค. TanStack Query(React)์ ๋ชจ๋ ํ์ฉ ๋ฐฉ๋ฒ์ด ์์ฑ๋ ์ํ๋ ์๋๋ฉฐ, ํ์ํ ๋ด์ฉ์ ์ถ๊ฐ, ๋ณด์ํ ์์ ์ ๋๋ค.
- ๊ธฐ์ฌํด์ฃผ์ ๋ชจ๋ ๋ถ๊ป ๊ฐ์ฌ๋๋ฆฝ๋๋ค.
- ์คํ์, ๊ฐ๋
์ฑ์ด ์ข์ง ์์ ๋ถ๋ถ ๋๋ ์ถ๊ฐ ๋ด์ฉ์
Pull Request
,Issue
๋ฑ์ ์์ ๋กญ๊ฒ ๋จ๊ฒจ์ฃผ์๋ฉด ๊ฒํ ํ์ ๋ฐ์ํ๊ฒ ์ต๋๋ค.
- โญ๏ธ TanStack Query(React)
v5
๊ฐ 23.10.17์ ๋ฆด๋ฆฌ์ฆ๋์ต๋๋ค. ํด๋น ๋ฌธ์๋v5
๊ธฐ์ค์ผ๋ก ์์ฑ๋์ด ์์ต๋๋ค. - โญ๏ธ ๊ธฐ์กด
v4
๋ฌธ์๋ react query tutorial v4 ๋ฌธ์๋ฅผ ํ์ธํด ์ฃผ์ธ์.
-
v3 -> v4
,v4 -> v5
Migrating ์ ๋ฆฌ ๋ฌธ์๋ ์๋ ๋ฌธ์๋ค์ ํ์ธํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
- React Query ๊ฐ์ ๋ฐ ๊ธฐ๋ฅ
- ๊ธฐ๋ณธ ์ค์ (QueryClientProvider, QueryClient)
- React Query Devtools
- React Query ์บ์ฑ ๋ผ์ดํ ์ฌ์ดํด
- useQuery
- useQuery ์ฃผ์ ๋ฆฌํด ๋ฐ์ดํฐ
- staleTime๊ณผ gcTime
- ๋ง์ดํธ ๋ ๋๋ง๋ค ์ฌ์์ฒญํ๋ refetchOnMount
- ์๋์ฐ๊ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค ์ฌ์์ฒญํ๋ refetchOnWindowFocus
- Polling ๋ฐฉ์์ ๊ตฌํํ๊ธฐ ์ํ refetchInterval์ refetchIntervalInBackground)
- ์๋ ์คํ์ enabled์ ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ์์ฒญํ๋ refetch
- ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด ์ฌ์์ฒญํ๋ retry
- onSuccess, onError, onSettled - ๐ก v5 @Deprecated
- select๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ๋ณํ
- ์ฟผ๋ฆฌ๊ฐ pending ์ํ์ธ ๋์ ๋ณด์ฌ ์ค ์ ์๋ placeholderData
- Paginated ๊ตฌํ์ ์ ์ฉํ keepPreviousData - ๐ก v5 @Deprecated
- ํน์ ์ฟผ๋ฆฌ ํ๋กํผํฐ ๋ณ๊ฒฝ ์์๋ง ๋ฆฌ๋ ๋๋ง์ ํธ๋ฆฌ๊ฑฐ ํ ์ ์๋ notifyOnChangeProps
- ์ฟผ๋ฆฌ๋ฅผ ๋ณ๋ ฌ(Parallel) ์์ฒญํ ์ ์๋ useQueries
- ์ข ์ ์ฟผ๋ฆฌ(Dependent Queries)
- QueryClient ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ useQueryClient
- ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ ์ ์๋ initialData
- ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ถ๋ฌ์ค๋ PreFetching
- Infinite Queries(๋ฌดํ ์ฟผ๋ฆฌ) + useInfiniteQuery
- ์๋ฒ์ HTTP CUD๊ด๋ จ ์์ ์ ์ํ useMutation
- ์ฟผ๋ฆฌ ์๋ ์ทจ์ cancelQueries
- ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ ์ ์๋ queryClient.invalidateQueries
- ์บ์ ๋ฐ์ดํฐ ์ฆ์ ์ ๋ฐ์ดํธ๋ฅผ ์ํ queryClient.setQueryData
- ์ฌ์ฉ์ ๊ฒฝํ(UX)์ ์ฌ๋ ค์ฃผ๋ Optimistic Updates(๋๊ด์ ์ ๋ฐ์ดํธ)
- ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ Fallback UI๋ฅผ ์ ์ธ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํ ErrorBoundary + useQueryErrorResetBoundary
- ์๋ฒ ๋ก๋ฉ ์ค์ผ ๋ Fallback UI๋ฅผ ์ ์ธ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํ Suspense
- ์ฑ ์ ์ฒด์ ๋์ผํ ์ฟผ๋ฆฌ ํจ์๋ฅผ ๊ณต์ ํ๋ Default Query Function
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์ ํ์ ์คํฌ๋ฆฝํธ ์ ์ฉ
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ ESLint ์ ์ฉ
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ ์ง์ ๋ฒ์
- QueryClient ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ ๋ฌธ์
- ๊ธฐ๋ณธ์ ์ธ React Query ์ํคํ ์ฒ ์ดํด๋ณด๊ธฐ: inside React Query
- react-query๋ ๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์
์์
์๋ฒ ์ํ ๊ฐ์ ธ์ค๊ธฐ
,์บ์ฑ
,๋๊ธฐํ ๋ฐ ์ ๋ฐ์ดํธ
๋ฅผ ๋ณด๋ค ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ํด๋ผ์ด์ธํธ ์ํ์ ์๋ฒ ์ํ๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋ค. - react-query์์๋ ๊ธฐ์กด ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ
redux
,mobX
๊ฐํด๋ผ์ด์ธํธ ์ํ ์์
์ ์ ํฉํ์ง๋ง,๋น๋๊ธฐ ๋๋ ์๋ฒ ์ํ ์์
์๋ ๊ทธ๋ค์ง ์ข์ง ์๋ค๊ณ ์ธ๊ธํ๋ค. - ํด๋ผ์ด์ธํธ ์ํ(Client State)์ ์๋ฒ ์ํ(Server State)๋ ์์ ํ ๋ค๋ฅธ ๊ฐ๋ ์ด๋ฉฐ, ํด๋ผ์ด์ธํธ ์ํ๋ ๊ฐ๊ฐ์ input ๊ฐ์ผ๋ก ์๋ฅผ ๋ค ์ ์๊ณ , ์๋ฒ ์ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋์ด ์๋ ๋ฐ์ดํฐ๋ก ์๋ฅผ ๋ค ์ ์๋ค.
- ์บ์ฑ
- ๋์ผํ ๋ฐ์ดํฐ์ ๋ํ ์ค๋ณต ์์ฒญ์ ๋จ์ผ ์์ฒญ์ผ๋ก ํตํฉ
- ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค๋๋ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ
- ๋ฐ์ดํฐ๊ฐ ์ผ๋ง๋ ์ค๋๋์๋์ง ์ ์ ์๋ค.
- ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๋ฅผ ๊ฐ๋ฅํ ๋น ๋ฅด๊ฒ ๋ฐ์
- ํ์ด์ง๋ค์ด์ ๋ฐ ๋ฐ์ดํฐ ์ง์ฐ ๋ก๋์ ๊ฐ์ ์ฑ๋ฅ ์ต์ ํ
- ์๋ฒ ์ํ์ ๋ฉ๋ชจ๋ฆฌ ๋ฐ ๊ฐ๋น์ง ์์ง ๊ด๋ฆฌ
- ๊ตฌ์กฐ ๊ณต์ ๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจํ
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
// ...
},
},
});
- QueryClient๋ฅผ ์ฌ์ฉํ์ฌ
์บ์
์ ์ํธ ์์ฉํ ์ ์๋ค. - QueryClient์์ ๋ชจ๋
query
๋๋mutation
์ ๊ธฐ๋ณธ ์ต์ ์ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ์ข ๋ฅ๊ฐ ์๋นํ๋ฏ๋ก ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ๋ณด์.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({ /* options */});
function App() {
return (
<QueryClientProvider client={queryClient}>
<div>๋ธ๋ผ๋ธ๋ผ</div>
</QueryClientProvider>;
);
}
- react-query๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋
QueryClientProvider
๋ฅผ ์ต์๋จ์์ ๊ฐ์ธ์ฃผ๊ณQueryClient
์ธ์คํด์ค๋ฅผ client props๋ก ๋ฃ์ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฐ๊ฒฐํด์ผ ํ๋ค. - ์ ์์์์ App.js์ QueryClientProvider๋ก ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๊ณ , client props์๋ค queryClient๋ฅผ ์ฐ๊ฒฐํจ์ผ๋ก์จ, ์ด context๋ ์ฑ์์ ๋น๋๊ธฐ ์์ฒญ์ ์์์ ์ฒ๋ฆฌํ๋
background
๊ณ์ธต์ด ๋๋ค.
- React Query Devtools ๊ณต์ ๋ฌธ์
- react-query๋
์ ์ฉ devtools
๋ฅผ ์ ๊ณตํ๋ฉฐ ๋ณ๋์ ํจํค์ง ์ค์น๊ฐ ํ์ํ๋ค. - devtools๋ฅผ ์ฌ์ฉํ๋ฉด React Query์ ๋ชจ๋ ๋ด๋ถ ๋์์
์๊ฐํ
ํ๋ ๋ฐ ๋์์ด ๋๋ฉฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด๋๋ฒ๊น ์๊ฐ์ ์ ์ฝ
ํ ์ ์๋ค. - devtools๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก
process.env.NODE_ENV === "development"
์ธ ๊ฒฝ์ฐ์๋ง ์คํ๋๋ค, ์ฆ ์ผ๋ฐ์ ์ผ๋ก ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์๋ํ๋๋ก ์ค์ ๋์ด ์์ผ๋ฏ๋ก, ํ๋ก์ ํธ ๋ฐฐํฌ ์์ Devtools ์ฝ์ ์ฝ๋๋ฅผ ์ ๊ฑฐํด ์ค ํ์๊ฐ ์๋ค. - Next 13+์ App Dir์์ dev dependency๋ก ์ค์นํด์ผ ๋์ํ๋ค.
$ 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>
);
}
- initialIsOpen (Boolean)
true
์ด๋ฉด ๊ฐ๋ฐ ๋๊ตฌ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ ค ์๋๋ก ์ค์ ํ ์ ์๋ค.
- buttonPosition?: ("top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative")
- ๊ธฐ๋ณธ๊ฐ:
bottom-right
- devtools ํจ๋์ ์ฌ๋ซ๊ธฐ ์ํ ๋ก๊ณ ์์น
relative
์ผ ๋ ๋ฒํผ์ devtools๋ฅผ ๋ ๋๋งํ๋ ์์น์ ๋ฐฐ์น๋๋ค.
- ๊ธฐ๋ณธ๊ฐ:
- ์ผ๋ฐ์ ์ผ๋ก initialIsOpen, buttonPosition์ ์์ฃผ ์ฌ์ฉํ๋ฉฐ ๊ทธ ์ธ์ position, client์ ๊ฐ์ ์ต์ ๋ค๋ ์กด์ฌํ๋ค.
* Query Instances with and without cache data(์บ์ ๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ์๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค)
* Background Refetching(๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฆฌํจ์นญ)
* Inactive Queries(๋นํ์ฑ ์ฟผ๋ฆฌ)
* Garbage Collection(๊ฐ๋น์ง ์ปฌ๋ ์
)
gcTime
์ ๊ธฐ๋ณธ๊ฐ 5๋ถ,staleTime
๊ธฐ๋ณธ๊ฐ 0์ด๋ฅผ ๊ฐ์
A
๋ผ๋ queryKey๋ฅผ ๊ฐ์ง A ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐmount
๋จ- ๋คํธ์ํฌ์์ ๋ฐ์ดํฐ fetchํ๊ณ , ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ A๋ผ๋ queryKey๋ก
์บ์ฑ
ํจ - ์ด ๋ฐ์ดํฐ๋
fresh
์ํ์์staleTime(๊ธฐ๋ณธ๊ฐ 0)
์ดํstale
์ํ๋ก ๋ณ๊ฒฝ๋จ - A ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ
unmount
๋จ - ์บ์๋
gcTime(๊ธฐ๋ณธ๊ฐ 5min)
๋งํผ ์ ์ง๋๋ค๊ฐ๊ฐ๋น์ง ์ฝ๋ ํฐ(GC)
๋ก ์์ง๋จ - ๋ง์ผ, gcTime ์ง๋๊ธฐ ์ ์ด๊ณ , A ์ฟผ๋ฆฌ ์ธ์คํด์ค freshํ ์ํ๋ผ๋ฉด ์๋กญ๊ฒ mount๋๋ฉด ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ค๋ค.
- useQuery ๊ณต์ ๋ฌธ์
- useQuery๋
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, // (*)
});
};
- useQuery์ queryKey๋
๋ฐฐ์ด
๋ก ์ง์ ํด ์ค์ผ ํ๋ค.- ์ด๋ ๋จ์ผ ๋ฌธ์์ด๋ง ํฌํจ๋ ๋ฐฐ์ด์ด ๋ ์๋ ์๊ณ , ์ฌ๋ฌ ๋ฌธ์์ด๊ณผ ์ค์ฒฉ๋ ๊ฐ์ฒด๋ก ๊ตฌ์ฑ๋ ๋ณต์กํ ํํ์ผ ์๋ ์๋ค.
// An individual todo
useQuery({ queryKey: ["todo", 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ["todo", 5, { preview: true }], ...})
- useQuery๋
queryKey
๋ฅผ ๊ธฐ๋ฐ์ผ๋ก์ฟผ๋ฆฌ ์บ์ฑ
์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ํต์ฌ์ด๋ค.- ๋ง์ฝ, ์ฟผ๋ฆฌ๊ฐ ํน์ ๋ณ์์
์์กด
ํ๋ค๋ฉด ๋ฐฐ์ด์๋ค ์ด์ด์ ์ค์ผ ํ๋ค.ex: ["super-hero", heroId, ...]
- ์ด๋ ์ฌ์ค ๊ต์ฅํ ์ค์ํ๋ค. ์๋ฅผ ๋ค์ด,
queryClient.setQueryData
๋ฑ๊ณผ ๊ฐ์ด ํน์ ์ฟผ๋ฆฌ์ ์ ๊ทผ์ด ํ์ ํ ๋์ด๊ธฐ์ ์ค์ ํด๋ ํฌ๋งท
์ ์ง์ผ์ค์ผ ์ ๋๋ก ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค. - ์๋ options ์์ ๋ฅผ ์ดํด๋ณด๋ฉด useSuperHeroData์ queryKey๋
["super-hero", heroId]
์ด๋ค. ๊ทธ๋ ๋ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์ฉํ ๋ ๋๊ฐ์ด["super-hero", heroId]
ํฌ๋งท์ ๊ฐ์ ธ์ผ ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ํ๋ ์ฟผ๋ฆฌ์ ์ ๊ทผํ ์ ์๋ค.
- ๋ง์ฝ, ์ฟผ๋ฆฌ๊ฐ ํน์ ๋ณ์์
2. queryFn
- useQuery์ 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
- useQuery ๊ณต์ ๋ฌธ์
- useQuery์
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,
fetchStatus,
isLoading,
isFetching,
isError,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
- data: ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ฆฌํดํ
Promise
์์resolved
๋ ๋ฐ์ดํฐ - error: ์ฟผ๋ฆฌ ํจ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, ์ฟผ๋ฆฌ์ ๋ํ ์ค๋ฅ ๊ฐ์ฒด
- status:
data
, ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ์ ๋ํ ์ํ๋ฅผ ํํํ๋ status๋ ๋ฌธ์์ด ํํ๋ก3๊ฐ์ง
์ ๊ฐ์ด ์กด์ฌํ๋ค.- pending: ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๊ฐ ์๊ณ , ์ฟผ๋ฆฌ ์๋๊ฐ ์์ง ์๋ฃ๋์ง ์์ ์ํ.
{ enabled: false }
์ํ๋ก ์ฟผ๋ฆฌ๊ฐ ํธ์ถ๋๋ฉด ์ด ์ํ๋ก ์์๋๋ค.- Dependent Queries ๊ณต์ ๋ฌธ์
- error: ์๋ฌ ๋ฐ์ํ์ ๋ ์ํ
- success: ์ฟผ๋ฆฌ ํจ์๊ฐ ์ค๋ฅ ์์ด ์์ฒญ ์ฑ๊ณตํ๊ณ ๋ฐ์ดํฐ๋ฅผ ํ์ํ ์ค๋น๊ฐ ๋ ์ํ.
- pending: ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๊ฐ ์๊ณ , ์ฟผ๋ฆฌ ์๋๊ฐ ์์ง ์๋ฃ๋์ง ์์ ์ํ.
- fetchStatus:
queryFn
์ ๋ํ ์ ๋ณด๋ฅผ ๋ํ๋- fetching: ์ฟผ๋ฆฌ๊ฐ ํ์ฌ ์คํ ์ค์ธ ์ํ
- paused: ์ฟผ๋ฆฌ๋ฅผ ์์ฒญํ์ง๋ง, ์ ์ ์ค๋จ๋ ์ํ (network mode์ ์ฐ๊ด)
- idle: ์ฟผ๋ฆฌ๊ฐ ํ์ฌ ์๋ฌด ์์ ๋ ์ํํ์ง ์๋ ์ํ
- isLoading:
์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์์ ๋
์ฆ, ์ฒ์ ์คํ๋ ์ฟผ๋ฆฌ์ผ ๋ ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผtrue/false
๋ก ๋ฐํ๋๋ค.- ์ด๋ ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ๋ก๋ฉ ์ฌ๋ถ์ ์๊ด์์ด
false
๋ฅผ ๋ฐํํ๋ค. isFetching && isPending
์ ๋์ผํ๋ค.
- ์ด๋ ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ๋ก๋ฉ ์ฌ๋ถ์ ์๊ด์์ด
- isFetching: ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ๋ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ฉด ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ
true/false
๋ก ๋ฐํ๋๋ค. - isSuccess: ์ฟผ๋ฆฌ ์์ฒญ์ด ์ฑ๊ณตํ๋ฉด
true
- isError: ์ฟผ๋ฆฌ ์์ฒญ ์ค์ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
true
- refetch: ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ๋ค์ ๊ฐ์ ธ์ค๋ ํจ์.
- ๊ทธ ์ธ ๋ฐํ ๋ฐ์ดํฐ๋ค์ ์์ธํ ์๊ณ ์ถ์ผ๋ฉด useQuery ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ
- Why Two Different States ๊ณต์ ๋ฌธ์
- fetchStatus๋ HTTP ๋คํธ์ํฌ ์ฐ๊ฒฐ ์ํ์ ์ข ๋ ๊ด๋ จ๋ ์ํ ๋ฐ์ดํฐ์ด๋ค.
- ์๋ฅผ ๋ค์ด, status๊ฐ
success
์ํ๋ผ๋ฉด ์ฃผ๋ก fetchStatus๋idle
์ํ์ง๋ง, ๋ฐฑ๊ทธ๋ผ์ด๋์์ re-fetch๊ฐ ๋ฐ์ํ ๋fetching
์ํ์ผ ์ ์๋ค. - status๊ฐ ๋ณดํต
loading
์ํ์ผ ๋ fetchStatus๋ ์ฃผ๋กfetching
๋ฅผ ๊ฐ์ง๋ง, ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋์ด ์์ง ์์ ๊ฒฝ์ฐpaused
์ํ๋ฅผ ๊ฐ์ง ์ ์๋ค.
- ์๋ฅผ ๋ค์ด, status๊ฐ
- ์ ๋ฆฌํ์๋ฉด ์๋์ ๊ฐ๋ค.
- status๋
data
๊ฐ ์๋์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค. - fetchStatus๋ ์ฟผ๋ฆฌ ์ฆ,
queryFn ์์ฒญ
์ด ์งํ ์ค์ธ์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.
- status๋
- stale์ ์ฉ์ด ๋ป๋๋ก
์ฉ์
์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๊ฐ ์๋๋ผ๋ ์๋ฏธ์ด๋ค. - fresh๋ ๋ป ๊ทธ๋๋ก
์ ์ ํ
์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๋ผ๋ ์๋ฏธ์ด๋ค.
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
gcTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
});
- staleTime:
(number | Infinity)
- staleTime์ ๋ฐ์ดํฐ๊ฐ
fresh์์ stale
์ํ๋ก ๋ณ๊ฒฝ๋๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ, ๋ง์ฝ staleTime์ด3000
์ด๋ฉด fresh ์ํ์์3์ด
๋ค์ stale๋ก ๋ณํ fresh
์ํ์ผ ๋๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ์๋กญ๊ฒ mount ๋์ด๋ ๋คํธ์ํฌ ์์ฒญ(fetch)์ด ์ผ์ด๋์ง ์๋๋ค.- ์ฐธ๊ณ ๋ก, staleTime์ ๊ธฐ๋ณธ๊ฐ์
0
์ด๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก fetch ํ์ ๋ฐ๋ก stale์ด ๋๋ค.
- staleTime์ ๋ฐ์ดํฐ๊ฐ
- gcTime:
(number | Infinity)
- ๋ฐ์ดํฐ๊ฐ ์ฌ์ฉํ์ง ์๊ฑฐ๋,
inactive
์ํ์ผ ๋์บ์ฑ ๋ ์ํ๋ก
๋จ์์๋ ์๊ฐ(๋ฐ๋ฆฌ์ด)์ด๋ค. - ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ unmount ๋๋ฉด ๋ฐ์ดํฐ๋
inactive ์ํ๋ก ๋ณ๊ฒฝ
๋๋ฉฐ, ์บ์๋gcTime
๋งํผ ์ ์ง๋๋ค. - gcTime์ด ์ง๋๋ฉด
๊ฐ๋น์ง ์ฝ๋ ํฐ
๋ก ์์ง๋๋ค. - gcTime์ด ์ง๋๊ธฐ ์ ์ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ๋ค์ mount ๋๋ฉด, ๋ฐ์ดํฐ๋ฅผ fetch ํ๋ ๋์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ค๋ค.
- gcTime์ staleTime๊ณผ ๊ด๊ณ์์ด, ๋ฌด์กฐ๊ฑด
inactive
๋ ์์ ์ ๊ธฐ์ค์ผ๋ก ์บ์ ๋ฐ์ดํฐ ์ญ์ ๋ฅผ ๊ฒฐ์ ํ๋ค. - gcTime์ ๊ธฐ๋ณธ๊ฐ์
5๋ถ
์ด๋ค. SSR ํ๊ฒฝ์์๋Infinity
์ด๋ค.
- ๋ฐ์ดํฐ๊ฐ ์ฌ์ฉํ์ง ์๊ฑฐ๋,
- ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ staleTime๊ณผ gcTime์ ๊ธฐ๋ณธ๊ฐ์ ๊ฐ๊ฐ
0๋ถ
๊ณผ5๋ถ
์ด๋ค. ๋ฐ๋ผ์ staleTime์ ์ด๋ ํ ์ค์ ๋ ํ์ง ์์ผ๋ฉด ํด๋น ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ(Observer)๊ฐ mount ๋์ ๋ ๋งค๋ฒ ๋ค์ API๋ฅผ ์์ฒญํ ๊ฒ์ด๋ค. - staleTime์ gcTime๋ณด๋ค ๊ธธ๊ฒ ์ค์ ํ๋ค๊ณ ๊ฐ์ ํ๋ฉด, staleTime๋งํผ์ ์บ์ฑ์ ๊ธฐ๋ํ์ ๋ ์ํ๋ ๊ฒฐ๊ณผ๋ฅผ ์ป์ง ๋ชปํ ๊ฒ์ด๋ค. ์ฆ, ๋ ๊ฐ์ ์ต์
์ ์ ์ ํ๊ฒ ์ค์ ํด ์ค์ผ ํ๋ค.
- ์ฐธ๊ณ ๋ก, TkDodo์ reply์ ๋ฐ๋ฅด๋ฉด TkDodo๋
staleTime์ gcTime๋ณด๋ค ์๊ฒ ์ค์ ํ๋ ๊ฒ์ด ์ข๋ค.
๋ ์๊ฒฌ์ ๋์ํ์ง ์๋๋ค๊ณ ํ๋ค. - ์์ปจ๋, staleTime์ด 60๋ถ์ผ์ง๋ผ๋ ์ ์ ๊ฐ ์์ฃผ ์ฌ์ฉํ์ง ์๋ ๋ฐ์ดํฐ๋ผ๋ฉด ๊ตณ์ด gcTime์ 60๋ถ ์ด์์ผ๋ก ์ค์ ํ์ฌ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ญ๋นํ ํ์๊ฐ ์๋ค.
- ์ฐธ๊ณ ๋ก, TkDodo์ reply์ ๋ฐ๋ฅด๋ฉด TkDodo๋
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnMount: true,
});
- refetchOnMount:
boolean | "always" | ((query: Query) => boolean | "always")
- refetchOnMount๋ ๋ฐ์ดํฐ๊ฐ
stale
์ํ์ผ ๊ฒฝ์ฐ, mount๋ง๋คrefetch
๋ฅผ ์คํํ๋ ์ต์ ์ด๋ค. - refetchOnMount์ ๊ธฐ๋ณธ๊ฐ์
true
์ด๋ค. always
๋ก ์ค์ ํ๋ฉด ๋ง์ดํธ ์๋ง๋ค ๋งค๋ฒ refetch๋ฅผ ์คํํ๋ค.false
๋ก ์ค์ ํ๋ฉด ์ต์ด fetch ์ดํ์๋ refetch ํ์ง ์๋๋ค.
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchOnWindowFocus: true,
});
- refetchOnWindowFocus:
boolean | "always" | ((query: Query) => boolean | "always")
- refetchOnWindowFocus๋ ๋ฐ์ดํฐ๊ฐ
stale
์ํ์ผ ๊ฒฝ์ฐ์๋์ฐ ํฌ์ปค์ฑ
๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์ ์ด๋ค. - refetchOnWindowFocus์ ๊ธฐ๋ณธ๊ฐ์
true
์ด๋ค. - ์๋ฅผ ๋ค์ด, ํฌ๋กฌ์์ ๋ค๋ฅธ ํญ์ ๋๋ ๋ค๊ฐ ๋ค์ ์๋ ๋ณด๋ ์ค์ธ ํญ์ ๋๋ ์ ๋๋ ์ด ๊ฒฝ์ฐ์ ํด๋นํ๋ค. ์ฌ์ง์ด F12๋ก ๊ฐ๋ฐ์ ๋๊ตฌ ์ฐฝ์ ์ผ์ ๋คํธ์ํฌ ํญ์ด๋ , ์ฝ์ ํญ์ด๋ ๊ฐ๋ฐ์ ๋๊ตฌ ์ฐฝ์์ ๋๋ค๊ฐ ํ์ด์ง ๋ด๋ถ๋ฅผ ๋ค์ ํด๋ฆญํ์ ๋๋ ์ด ๊ฒฝ์ฐ์ ํด๋นํ๋ค.
always
๋ก ์ค์ ํ๋ฉด ํญ์ ์๋์ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ค๋ ์๋ฏธ์ด๋ค.
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
Polling(ํด๋ง)์ด๋?
์ค์๊ฐ ์น์ ์ํ ๊ธฐ๋ฒ์ผ๋ก "์ผ์ ํ ์ฃผ๊ธฐ(ํน์ ํ ์๊ฐ)"๋ฅผ ๊ฐ์ง๊ณ ์๋ฒ์ ์๋ต์ ์ฃผ๊ณ ๋ฐ๋ ๋ฐฉ์์ด ํด๋ง ๋ฐฉ์์ด๋ค.
react-query์์๋ "refetchInterval", "refetchIntervalInBackground"์ ์ด์ฉํด์ ๊ตฌํํ ์ ์๋ค.
- refetchInterval:
number | false | ((data: TData | undefined, query: Query) => number | false)
- refetchInterval์
์๊ฐ(ms)
๋ฅผ ๊ฐ์ผ๋ก ๋ฃ์ด์ฃผ๋ฉด ์ผ์ ์๊ฐ๋ง๋ค ์๋์ผ๋ก refetch๋ฅผ ์์ผ์ค๋ค.
- refetchIntervalInBackground:
boolean
- refetchIntervalInBackground๋
refetchInterval
๊ณผ ํจ๊ป ์ฌ์ฉํ๋ ์ต์ ์ด๋ค. - ํญ/์ฐฝ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์๋ ๋์ refetch ์์ผ์ค๋ค. ์ฆ, ๋ธ๋ผ์ฐ์ ์ focus ๋์ด ์์ง ์์๋ refetch๋ฅผ ์์ผ์ฃผ๋ ๊ฒ์ ์๋ฏธํ๋ค.
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>
);
- enabled:
boolean
- enabled๋
์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ์คํ๋์ง ์๋๋ก ํ ๋ ์ค์
ํ ์ ์๋ค. - enabled๋ฅผ
false
๋ฅผ ์ฃผ๋ฉด ์ฟผ๋ฆฌ๊ฐ ์๋ ์คํ๋์ง ์๋๋ค.- useQuery ๋ฐํ ๊ฐ ์ค status๊ฐ
pending
์ํ๋ก ์์ํ๋ค.
- useQuery ๋ฐํ ๊ฐ ์ค status๊ฐ
- refetch๋ ์ฟผ๋ฆฌ๋ฅผ
์๋
์ผ๋ก ๋ค์ ์์ฒญํ๋ ๊ธฐ๋ฅ์ด๋ค. ์ฟผ๋ฆฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ์ค๋ฅ๋ง ๊ธฐ๋ก๋๋ค.- ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๋ ค๋ฉด
throwOnError
์์ฑ์true
๋ก ํด์ ์ ๋ฌํด์ผ ํ๋ค.
- ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๋ ค๋ฉด
- ๋ณดํต ์๋์ผ๋ก ์ฟผ๋ฆฌ ์์ฒญ์ ํ์ง ์๊ณ
๋ฒํผ ํด๋ฆญ
์ด๋ ํน์ ์ด๋ฒคํธ๋ฅผ ํตํด ์์ฒญ์ ์๋ํ ๋ ๊ฐ์ด ์ฌ์ฉํ๋ค. - ๐ก ์ฃผ์ํ ์ ์,
enabled: false
๋ฅผ ์คฌ๋ค๋ฉดqueryClient
๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์คinvalidateQueries
์refetchQueries
๋ฅผ ๋ฌด์ํ๋ค.
const {
data,
refetch,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
retry: 10, // ์ค๋ฅ๋ฅผ ํ์ํ๊ธฐ ์ ์ ์คํจํ ์์ฒญ์ 10๋ฒ ์ฌ์๋ํฉ๋๋ค.
});
- retry:
(boolean | number | (failureCount: number, error: TError) => boolean)
- retry๋ ์ฟผ๋ฆฌ๊ฐ
์คํจ
ํ๋ฉด useQuery๋ฅผํน์ ํ์
๋งํผ ์ฌ์์ฒญํ๋ ์ต์ ์ด๋ค. - retry๊ฐ
false
์ธ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์ ์๋ํ์ง ์๋๋ค.true
์ธ ๊ฒฝ์ฐ์๋ ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด์ ๋ฌดํ ์ฌ์์ฒญ์ ์๋ํ๋ค. - ๊ฐ์ผ๋ก
์ซ์
๋ฅผ ๋ฃ์ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๊ฐ ํด๋น ์ซ์๋ฅผ ์ถฉ์กฑํ ๋๊น์ง ์์ฒญ์ ์ฌ์๋ํ๋ค. - ๊ธฐ๋ณธ๊ฐ์ ํด๋ผ์ด์ธํธ ํ๊ฒฝ์์๋
3
, ์๋ฒ ํ๊ฒฝ์์๋0
์ด๋ค.
- NOTE:
v4
๊น์ง ์๋ onSuccess, onError, onSettled Callback์useQuery
์ต์ ์์ @Deprecated ๋๋ค. ๋จ,useMutation
์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค. - Breaking React Query's API on purpose ์ฐธ๊ณ
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>
);
- select:
(data: TData) => unknown
- select ์ต์
์ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ํจ์์์
๋ฐํ๋ ๋ฐ์ดํฐ์ ์ผ๋ถ๋ฅผ ๋ณํํ๊ฑฐ๋ ์ ํํ ์ ์๋ค.
- ์ฐธ๊ณ ๋ก, ๋ฐํ๋ ๋ฐ์ดํฐ ๊ฐ์๋ ์ํฅ์ ์ฃผ์ง๋ง ์ฟผ๋ฆฌ ์บ์์ ์ ์ฅ๋๋ ๋ด์ฉ์๋ ์ํฅ์ ์ฃผ์ง ์๋๋ค.
const placeholderData = useMemo(() => generateFakeHeroes(), []);
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
placeholderData: placeholderData,
});
- placeholderData:
TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TData
- placeholderData๋ฅผ ์ค์ ํ๋ฉด ์ฟผ๋ฆฌ๊ฐ
pending
์ํ์ธ ๋์ ํน์ ์ฟผ๋ฆฌ์ ๋ํ placeholder data๋ก ์ฌ์ฉ๋๋ค. - placeholderData๋ ์บ์์ ์ ์ง๋์ง ์์ผ๋ฉฐ, ์๋ฒ ๋ฐ์ดํฐ์ ๊ด๊ณ์๋ ๋ณด์ฌ์ฃผ๊ธฐ์ฉ ๊ฐ์ง ๋ฐ์ดํฐ๋ค.
- placeholderData์ ํจ์๋ฅผ ์ ๊ณตํ๋ ๊ฒฝ์ฐ ์ฒซ ๋ฒ์งธ ์ธ์๋ก ์ด์ ์ ๊ด์ฐฐ๋ ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ , ๋ ๋ฒ์งธ ์ธ์๋ ์ด์ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ๋๋ค.
-
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,
});
- ์๋ ์์์ฒ๋ผ ์์ฑํด์ ์์
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 ๊ฐ ๋ณ๊ฒฝ์์๋ง ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค
});
- notifyOnChangeProps:
string[] | "all" | (() => string[] | "all")
- ์ฟผ๋ฆฌ์ ํน์ ํ๋กํผํฐ๋ค์ด ๋ณ๊ฒฝ๋์์ ๋๋ง ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋๋ก ์ค์ ํ ์ ์๋ค.
- ๋ณ๋๋ก ์ค์ ํ์ง ์์ผ๋ฉด, ์ปดํฌ๋ํธ์์ ์ ๊ทผํ ๊ฐ์ด ๋ณ๊ฒฝ๋์์ ๋ ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค (๊ธฐ๋ณธ ๋์). ์ฆ, ์ ์์์์
notifyOnChangeProps
์ ์ค์ ๊ฐ์ ์ฃผ์ง ์์๋ค๋ฉด,data
,dataUpdatedAt
์ค ์ด๋ ํ๋๊ฐ ๋ณ๊ฒฝ๋๋ฉด ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค. "all"
๋ก ์ค์ ํ ๊ฒฝ์ฐ, ์ฟผ๋ฆฌ์ ์ด๋ค ํ๋กํผํฐ๊ฐ ๋ณ๊ฒฝ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋๋ค.- ์ฐธ๊ณ : ๊ธฐ๋ณธ ๋์์ Object.defineProperty()๋ฅผ ํ์ฉํ๋ค.
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,
},
// ...
],
});
- ํ์ง๋ง, ์ฟผ๋ฆฌ ์ฌ๋ฌ ๊ฐ๋ฅผ ๋์์ ์ํํด์ผ ํ๋๋ฐ, ๋ ๋๋ง์ด ๊ฑฐ๋ญ๋๋ ์ฌ์ด์ฌ์ด์ ๊ณ์ ์ฟผ๋ฆฌ๊ฐ ์ํ๋์ด์ผ ํ๋ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ํํ๋ ๋ก์ง์ด hook ๊ท์น์ ์ด๊ธ๋ ์๋ ์๋ค. ์ด๋ด ๋๋
useQueries
๋ฅผ ์ฌ์ฉํ๋ค. - 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),
})
}
})
- combinedQueries๋
data
์pending
ํ๋กํผํฐ๋ฅผ ๊ฐ๋๋ค. - Note: ์ฐธ๊ณ ๋ก ๊ฒฐํฉํ๋ฉด ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์ ๋๋จธ์ง ๋ค๋ฅธ ํ๋กํผํฐ๋ค์ ์์ค๋๋ค.
- Dependent Queries ๊ณต์ ๋ฌธ์
์ข ์ ์ฟผ๋ฆฌ
๋ ์ด๋ค A๋ผ๋ ์ฟผ๋ฆฌ๊ฐ ์๋๋ฐ ์ด A ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ธฐ ์ ์ ์ฌ์ ์ ์๋ฃ๋์ด์ผ ํ๋ B ์ฟผ๋ฆฌ๊ฐ ์๋๋ฐ, ์ด๋ฌํ B ์ฟผ๋ฆฌ์ ์์กดํ๋ A ์ฟผ๋ฆฌ๋ฅผ ์ข ์ ์ฟผ๋ฆฌ๋ผ๊ณ ํ๋ค.- react-query์์๋
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,
});
- useQueryClient๋
QueryClient
์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค. QueryClient
๋ ์บ์์ ์ํธ์์ฉํ๋ค.- QueryClient๋ ๋ค์ ๋ฌธ์์์ ์์ธํ๊ฒ ๋ค๋ฃฌ๋ค.
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
- Initial Query Data ๊ณต์ ๋ฌธ์
- ์ฟผ๋ฆฌ์ ๋ํ
์ด๊ธฐ ๋ฐ์ดํฐ
๊ฐ ํ์ํ๊ธฐ ์ ์ ์บ์์ ์ ๊ณตํ๋ ๋ฐฉ๋ฒ์ด ์๋ค. - initialData ์ต์ ์ ํตํด์ ์ฟผ๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ์ฑ์ฐ๋ ๋ฐ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ์ด๊ธฐ ๋ก๋ ์ํ๋ ๊ฑด๋๋ธ ์๋ ์๋ค.
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
๋ฅผ ๋ฐํํ๋ค.
- prefetching ๊ณต์ ๋ฌธ์
- prefetch๋ ๋ง ๊ทธ๋๋ก ๋ฏธ๋ฆฌ fetchํด์ค๊ฒ ๋ค๋ ์๋ฏธ์ด๋ค.
- ๋น๋๊ธฐ ์์ฒญ์ ๋ฐ์ดํฐ์์ด ํด์๋ก ๋ฐ์์ค๋ ์๋๊ฐ ๋๋ฆฌ๊ณ , ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค. ์ฌ์ฉ์ ๊ฒฝํ์ ์ํด ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ์์์ ์บ์ฑํด ๋์ผ๋ฉด? ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ธฐ ์ ์ ์ฌ์ฉ์๊ฐ ์บ์ฑ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณผ ์ ์์ด
UX์ ์ข์ ์ํฅ
์ ์ค ์ ์๋ค.- ์๋ฅผ ๋ค์ด ํ์ด์ง๋ค์ด์ ์ ๊ตฌํํ๋ค๊ณ ๊ฐ์ ํ๋ฉด, ํ์ด์ง1์์ ํ์ด์ง2๋ก ์ด๋ํ์ ๋ ํ์ด์ง3์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ์๋๋ ๊ฒ์ด๋ค!
- react query์์๋
queryClient.prefetchQuery
์ ํตํด์ prefetch ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
const prefetchNextPosts = async (nextPage: number) => {
const queryClient = useQueryClient();
// ํด๋น ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๋ ์ผ๋ฐ ์ฟผ๋ฆฌ๋ค์ฒ๋ผ ์บ์ฑ ๋๋ค.
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
// ...options
});
};
// ๋จ์ ์
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
- ์ฐธ๊ณ ๋ก prefetchQuery๋ฅผ ํตํด ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ์ ๋ํ ๋ฐ์ดํฐ๊ฐ
์ด๋ฏธ ์บ์ฑ ๋์ด ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ค.
- infinite ์ฟผ๋ฆฌ๋ ๋ฐ๋ก ์๋์ ๋์ค๊ฒ ์ง๋ง ์ผ๋ฐ ์ฟผ๋ฆฌ๋ค์ฒ๋ผ infinite ์ฟผ๋ฆฌ๋ prefetch ํ ์ ์๋ค.
- ๊ธฐ๋ณธ์ ์ผ๋ก ์ฟผ๋ฆฌ์
์ฒซ ๋ฒ์งธ ํ์ด์ง๋ง 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
});
};
- Infinite Queries ๊ณต์ ๋ฌธ์
- useInfiniteQuery ๊ณต์ ๋ฌธ์
- Infinite Queries(๋ฌดํ ์ฟผ๋ฆฌ)๋
๋ฌดํ ์คํฌ๋กค
์ด๋load more(๋ ๋ณด๊ธฐ)
๊ณผ ๊ฐ์ด ํน์ ์กฐ๊ฑด์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ๋ฉด ์ ์ฉํ๋ค. - react-query๋ ์ด๋ฌํ ๋ฌดํ ์ฟผ๋ฆฌ๋ฅผ ์ง์ํ๊ธฐ ์ํด useQuery์ ์ ์ฉํ ๋ฒ์ ์ธ
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;
},
// ...
});
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์ ์ฌ์ฉ๋ฒ์ ๋น์ทํ์ง๋ง, ์ฐจ์ด์ ์ด ์๋ค.- useInfiniteQuery๋ ๋ฐํ ๊ฐ์ผ๋ก
isFetchingNextPage
,isFetchingPreviousPage
,fetchNextPage
,fetchPreviousPage
,hasNextPage
๋ฑ์ด ์ถ๊ฐ์ ์ผ๋ก ์๋ค.- data.pages: ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ๋ฐฐ์ด์ด๋ค.
- data.pageParams: ๋ชจ๋ ํ์ด์ง ๋งค๊ฐ๋ณ์๋ฅผ ํฌํจํ๋ ๋ฐฐ์ด์ด๋ค.
- fetchNextPage:
๋ค์ ํ์ด์ง
๋ฅผ fetch ํ ์ ์๋ค. - fetchPreviousPage:
์ด์ ํ์ด์ง
๋ฅผ fetch ํ ์ ์๋ค. - isFetchingNextPage:
fetchNextPage
๋ฉ์๋๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค. - isFetchingPreviousPage:
fetchPreviousPage
๋ฉ์๋๊ฐ ์ด์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค. - hasNextPage: ๊ฐ์ ธ์ฌ ์ ์๋
๋ค์ ํ์ด์ง
๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค. - hasPreviousPage: ๊ฐ์ ธ์ฌ ์ ์๋
์ด์ ํ์ด์ง
๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.
- initialPageParam:
TPageParam
- initialPageParam์ ์ด์ฉํด์ ์ฒซ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉํ ๊ธฐ๋ณธ ํ์ด์ง ๋งค๊ฐ๋ณ์์ด๋ค.
ํ์๊ฐ
์ด๋ค.
- getNextPageParam:
(lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null
- getNextPageParam ์ ์ด์ฉํด์ ํ์ด์ง๋ฅผ ์ฆ๊ฐ์ํฌ ์ ์๋ค.
ํ์๊ฐ
์ด๋ค.- getNextPageParam์ ์ฒซ ๋ฒ์งธ ์ธ์
lastPage
๋ fetch ํด์จ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฐ์ ธ์จ ํ์ด์ง ๋ชฉ๋ก์ด๋ค. - ๋ ๋ฒ์งธ ์ธ์
allPages
๋ ํ์ฌ๊น์ง ๊ฐ์ ธ์จ ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค. - ์ธ ๋ฒ์งธ ์ธ์
firstPageParam
๋ ์ฒซ ๋ฒ์งธ ํ์ด์ง์ ๋งค๊ฐ๋ณ์์ด๋ค. - ๋ค ๋ฒ์งธ ์ธ์
allPageParams
๋ ๋ชจ๋ ํ์ด์ง์ ๋งค๊ฐ๋ณ์์ด๋ค.
- getNextPageParam์ ์ฒซ ๋ฒ์งธ ์ธ์
- ์ฌ์ฉ ๊ฐ๋ฅํ ๋ค์ ํ์ด์ง๊ฐ ์์์ ํ์ํ๋ ค๋ฉด
undefined
๋๋null
์ ๋ฐํํ๋ฉด ๋๋ค. getPreviousPageParam
๋ ์กด์ฌํ๋ฉฐ,getNextPageParam
์ ๋ฐ๋์ ์์ฑ์ ๊ฐ๊ณ ์๋ค.
- maxPages:
number | undefined
- infinite ์ฟผ๋ฆฌ์ ์ ์ฅํ
์ต๋ ํ์ด์ง ์
์ด๋ค. - ์ต๋ ํ์ด์ง ์์ ๋๋ฌํ๋๋ฐ ์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ฉด
์ง์ ๋ ๋ฐฉํฅ(next, previous)
์ ๋ฐ๋ผ ํ์ด์ง ๋ฐฐ์ด์์ ์ฒซ ๋ฒ์งธ ํ์ด์ง ๋๋ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์ ๊ฑฐ๋๋ค. 0
๋๋undefined
๋ผ๋ฉด ํ์ด์ง ์๋ ๋ฌด์ ํ์ด๋ค.
queryFn
์ ๋๊ฒจ์ฃผ๋ pageParam๊ฐ ๋จ์ํ ๋ค์ page์ ๊ฐ๋ง์ ๊ด๋ฆฌํ ์ ์๋ ๊ฒ์ ์๋๋ค.- pageParam ๊ฐ์
getNextPageParam
์์ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝ์์ผ ์ค ์ ์๋ค. - ๋ฌด์จ ๋ง์ธ์ง ์์๋ฅผ ๋ณด๋ฉด ์ดํด๊ฐ ์ฝ๋ค. ๐ ์๋์ ๊ฐ์ด getNextPageParam์์ ๋ฐํ ๋ฐ์ดํฐ๊ฐ ๋จ์ํ ๋ค์ ํ์ด์ง๊ฐ์ด ์๋ ๊ฐ์ฒด๋ก ๋ฐํํ๋ค๊ณ ํด๋ณด์.
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: ({ pageParam }) => fetchColors(pageParam), // pageParam: { page: number; etc: string }
initialPageParam: {
page: number,
etc: "hi",
},
getNextPageParam: (lastPage, allPages) => {
return (
allPages.length < 4 && {
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}`
);
};
- ์ฆ, getNextPageParam์ ๋ฐํ ๊ฐ์ด pageParams๋ก ๋ค์ด๊ฐ๊ธฐ ๋๋ฌธ์ pageParams๋ฅผ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด getNextPageParam์ ๋ฐํ ๊ฐ์ ์ค์ ํ๋ฉด ๋๋ค.
- useMutation ๊ณต์ ๋ฌธ์
- react-query์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ Get ํ ๋๋ useQuery๋ฅผ ์ฌ์ฉํ๋ค.
- ๋ง์ฝ ์๋ฒ์ data๋ฅผ post, patch, put, delete์ ๊ฐ์ด ์์ ํ๊ณ ์ ํ๋ค๋ฉด ์ด๋๋ useMutation์ ์ด์ฉํ๋ค.
- ์์ฝํ์๋ฉด
R(read)๋ useQuery
,CUD(Create, Update, Delete)๋ useMutation
์ ์ฌ์ฉํ๋ค.
const mutation = useMutation({
mutationFn: createTodo,
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutate({ title });
};
- useMutation์ ๋ฐํ ๊ฐ์ธ mutation ๊ฐ์ฒด์
mutate
๋ฉ์๋๋ฅผ ์ด์ฉํด์ ์์ฒญ ํจ์๋ฅผ ํธ์ถํ ์ ์๋ค. - mutate๋
onSuccess
,onError
๋ฉ์๋๋ฅผ ํตํด ์ฑ๊ณตํ์ ์, ์คํจํ์ ์ response ๋ฐ์ดํฐ๋ฅผ ํธ๋ค๋งํ ์ ์๋ค. onMutate
๋ mutation ํจ์๊ฐ ์คํ๋๊ธฐ ์ ์ ์คํ๋๊ณ , mutation ํจ์๊ฐ ๋ฐ์ ๋์ผํ ๋ณ์๊ฐ ์ ๋ฌ๋๋ค.onSettled
๋ try...catch...finally ๊ตฌ๋ฌธ์finally
์ฒ๋ผ ์์ฒญ์ด ์ฑ๊ณตํ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์๊ด์์ด ๋ง์ง๋ง์ ์คํ๋๋ค.
const mutation = useMutation(addTodo);
try {
const todo = await mutation.mutateAsync(todo);
console.log(todo);
} catch (error) {
console.error(error);
} finally {
console.log("done");
}
- ๋ง์ฝ, useMutation์ ์ฌ์ฉํ ๋ promise ํํ์ response๊ฐ ํ์ํ ๊ฒฝ์ฐ๋ผ๋ฉด
mutateAsync
๋ฅผ ์ฌ์ฉํด์ ์ป์ด์ฌ ์ ์๋ค.
- ๋๋ถ๋ถ์ ๊ฒฝ์ฐ์ ์ฐ๋ฆฌ๋ mutate๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ๋ฆฌํ๋ค. ์๋ํ๋ฉด mutate๋ ์ฝ๋ฐฑ(onSuccess, onError)๋ฅผ ํตํด data์ error์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฐ๋ฆฌ๊ฐ ํน๋ณํ ํธ๋ค๋งํด ์ค ํ์๊ฐ ์๋ค.
- ํ์ง๋ง mutateAsync๋ Promise๋ฅผ ์ง์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ์ด๋ฐ ์๋ฌ ํธ๋ค๋ง ๊ฐ์ ๋ถ๋ถ์ ์ง์ ๋ค๋ค์ผ ํ๋ค.
- ๋ง์ฝ ์ด๋ฅผ ๋ค๋ฃจ์ง ์์ผ๋ฉด
unhandled promise rejection
์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ค.
- ๋ง์ฝ ์ด๋ฅผ ๋ค๋ฃจ์ง ์์ผ๋ฉด
- TkDodo Blog: Mutate or MutateAsync
- useMutation์ onSuccess, onError, onSettled์ ๊ฐ์ Callback ํจ์๋ค์ ๊ฐ์ง ์ ์๋ค.
- ๊ทธ๋ฟ๋ง ์๋๋ผ, mutate ์ญ์ ์์ ๊ฐ์ Callback ํจ์๋ค์ ๊ฐ์ง ์ ์๋ค.
- ๋์ ๋์์ ๊ฐ๋ค๊ณ ์๊ฐํ ์ ์์ง๋ง ์ฝ๊ฐ์ ์ฐจ์ด๊ฐ ์๋ค. ๋ค์๊ณผ ๊ฐ๋ค.
- useMutation์ Callback ํจ์์ mutate์ Callback ํจ์๋ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋๋ค.
- ์์๋
useMutation์ Callback -> mutate์ Callback
์์ผ๋ก ์คํ๋๋ค. - mutation์ด ์๋ฃ๋๊ธฐ ์ ์ ์ปดํฌ๋ํธ๊ฐ unmount๋๋ค๋ฉด mutate์ Callback์ ์คํ๋์ง ์์ ์ ์๋ค.
TkDodo
๋ ์์ ๊ฐ์ ์ด์ ๋ก ๋์ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์ ํ๋ค๊ณ ํ๋ค.- ๊ผญ ํ์ํ ๋ก์ง(ex.
์ฟผ๋ฆฌ ์ด๊ธฐํ
)์ useMutation์ Callback์ผ๋ก ์คํ์ํจ๋ค. - ๋ฆฌ๋ค์ด๋ ์ ๋ฐ UI ๊ด๋ จ ์์ ์ mutate Callback์์ ์คํ์ํจ๋ค.
- ๊ผญ ํ์ํ ๋ก์ง(ex.
- TkDodo Blog: Some callbacks might not fire
- Query Cancellation ๊ณต์ ๋ฌธ์
- ์ฟผ๋ฆฌ๋ฅผ
์๋์ผ๋ก ์ทจ์
ํ๊ณ ์ถ์ ์๋ ์๋ค.- ์๋ฅผ ๋ค์ด ์์ฒญ์ ์๋ฃํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ ์ฌ์ฉ์๊ฐ ์ทจ์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ฒญ์ ์ค์งํ๋๋ก ํ์ฉํ ์ ์๋ค.
- ๋๋, ์์ง HTTP ์์ฒญ์ด ๋๋์ง ์์์ ๋, ํ์ด์ง๋ฅผ ๋ฒ์ด๋ ๋๋ ์ค๊ฐ์ ์ทจ์ํด์ ๋ถํ์ํ ๋คํธ์ํฌ ๋ฆฌ์์ค๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
- ์ด๋ ๊ฒ ํ๋ ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ทจ์ํ๊ณ ์ด์ ์ํ๋ก ๋๋๋ฆฌ๊ธฐ ์ํด
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>;
- invalidateQueries์ ํ๋ฉด์ ์ต์ ์ํ๋ก ์ ์งํ๋ ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ด๋ค.
- ์๋ฅผ ๋ค๋ฉด, ๊ฒ์ํ ๋ชฉ๋ก์์ ์ด๋ค ๊ฒ์๊ธ์
์์ฑ(Post)
ํ๊ฑฐ๋ ๊ฒ์๊ธ์์ ๊ฑฐ(Delete)
ํ์ ๋ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ํ ๋ชฉ๋ก์ ์ค์๊ฐ์ผ๋ก ์ต์ ํํด์ผ ํ ๋๊ฐ ์๋ค. - ํ์ง๋ง ์ด๋,
query Key
๊ฐ ๋ณํ์ง ์์ผ๋ฏ๋ก ๊ฐ์ ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ๊ณ ์ต์ ํ๋ฅผ ์งํํด์ผ ํ๋๋ฐ, ์ด๋ฐ ๊ฒฝ์ฐ์invalidateQueries()
๋ฉ์๋๋ฅผ ์ด์ฉํ ์ ์๋ค. - ์ฆ, query๊ฐ ์ค๋๋์๋ค๋ ๊ฒ์ ํ๋จํ๊ณ ๋ค์
refetch
๋ฅผ ํ ๋ ์ฌ์ฉํ๋ค!
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation(addSuperHero, {
onSuccess(data) {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // ์ด key์ ํด๋นํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ!
console.log(data);
},
onError(err) {
console.log(err);
},
});
};
- ์ฐธ๊ณ ๋ก, queryKey์
["super-heroes"]
์ ๋๊ฒจ์ฃผ๋ฉด queryKey์ "super-heroes"๋ฅผ ํฌํจํ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ๋๋ค.
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });
// ์๋ query๋ค ๋ชจ๋ ๋ฌดํจํ ๋๋ค.
const query = useQuery({
queryKey: ["super-heroes", "superman"],
queryFn: fetchSuperHero,
});
const query = useQuery({
queryKey: ["super-heroes", { id: 1 }],
queryFn: fetchSuperHero,
});
- ์์
enabled/refetch
์์๋ ์ธ๊ธํ์ง๋งenabled: false
์ต์ ์ ์ฃผ๋ฉดqueryClient
๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์คinvalidateQueries
์refetchQueries
๋ฅผ ๋ฌด์ํ๋ค. - ์์ธํ ๋ด์ฉ์ queryClient.invalidateQueries ์ ๋ฆฌ๋ฅผ ์ฐธ๊ณ ํ์.
- queryClient.setQueryData ๊ณต์ ๋ฌธ์
- ๋ฐ๋ก ์์์
queryClient.invalidateQueries
๋ฅผ ์ด์ฉํด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ดค๋๋ฐqueryClient.setQueryData
๋ฅผ ์ด์ฉํด์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์ ๋ฐ์ดํธํ ์ ์๋ค. queryClient.setQueryData
๋ ์ฟผ๋ฆฌ์ ์บ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์ ๋ฐ์ดํธํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋๋๊ธฐ ํจ์
์ด๋ค.- 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๋ฅผ ์ ๋ฐ์ดํธ
์์ผ์ฃผ๊ณ ์๋ฒ๋ฅผ ํตํด ๊ฒ์ฆ๋ฐ๊ณ ์ ๋ฐ์ดํธ ๋๋ ๋กค๋ฐฑํ๋ ๋ฐฉ์์ด๋ค.- ์๋ฅผ ๋ค์ด facebook์ ์ข์์ ๋ฒํผ์ด ์๋๋ฐ ์ด๊ฒ์ ์ ์ ๊ฐ ๋๋ฅธ๋ค๋ฉด, ์ผ๋จ client ์ชฝ state๋ฅผ ๋จผ์ ์ ๋ฐ์ดํธํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ง์ฝ์ ์คํจํ๋ค๋ฉด, ์์ state๋ก ๋์๊ฐ๊ณ ์ฑ๊ณตํ๋ฉด ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ค์ fetch ํด์ ์๋ฒ ๋ฐ์ดํฐ์ ํ์คํ ์ฐ๋์ ์งํํ๋ค.
- Optimistic Update๊ฐ ์ ๋ง ์ ์ฉํ ๋๋ ์ธํฐ๋ท ์๋๊ฐ ๋๋ฆฌ๊ฑฐ๋ ์๋ฒ๊ฐ ๋๋ฆด ๋์ด๋ค. ์ ์ ๊ฐ ํํ ์ก์ ์ ๊ธฐ๋ค๋ฆด ํ์ ์์ด ๋ฐ๋ก ์ ๋ฐ์ดํธ๋๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ์ ๊ฒฝํ(UX) ์ธก๋ฉด์์ ์ข๋ค.
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutateFn: addSuperHero,
onMutate: async (newHero: any) => {
await queryClient.cancelQueries(["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(["super-heroes"]);
},
});
};
- ์ฐธ๊ณ ๋ก ์ ์์ ์์
cancelQueries
๋ ์ฟผ๋ฆฌ๋ฅผ์๋์ผ๋ก ์ทจ์
์ํฌ ์ ์๋ค. ์ทจ์์ํฌ query์ queryKey๋ฅผ cancelQueries์ ์ธ์๋ก ๋ณด๋ด ์คํ์ํจ๋ค. - ์๋ฅผ ๋ค์ด, ์์ฒญ์ ์๋ฃํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ, ์ฌ์ฉ์๊ฐ ์ทจ์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ฒญ์ ์ค์งํ๋ ๊ฒฝ์ฐ ์ด์ฉํ ์ ์๋ค.
- useQueryErrorResetBoundary ๊ณต์ ๋ฌธ์
- react-query์์ ErrorBoundary์ useQueryErrorResetBoundary๋ฅผ ๊ฒฐํฉํด
์ ์ธ์
์ผ๋ก ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค. - ErrorBoundary์ ๋ํ ์ค๋ช ์ ํด๋น ๋ฌธ์ ์ฐธ๊ณ ErrorBoundary
useQueryErrorResetBoundary
๋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
- ์ค์น ํ์ ์๋์ ๊ฐ์ QueryErrorBoundary๋ผ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๊ณ , ๊ทธ ๋ด๋ถ์
useQueryErrorResetBoundary
ํ ์ ํธ์ถํดreset
ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค. - ์๋ ์ฝ๋ ๋ด์ฉ์ ๋จ์ํ๋ค.
- Error๊ฐ ๋ฐ์ํ๋ฉด ErrorBoundary์
fallbackRender
prop์ผ๋ก ๋๊ธด ๋ด์ฉ์ด ๋ ๋๋ง ๋๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด children ๋ด์ฉ์ด ๋ ๋๋ง ๋๋ค. - ๋ํ, fallbackRender์ ๋ฃ์ด์ฃผ๋ ์ฝ๋ฐฑ ํจ์ ๋งค๊ฐ ๋ณ์๋ก
resetErrorBoundary
๋ฅผ ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ํตํด ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ, ์ด๋ฅผ ํตํด ๋ชจ๋ ์ฟผ๋ฆฌ ์๋ฌ๋ฅผ์ด๊ธฐํ
ํ ์ ์๋ค. ์๋ ์ฝ๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ button์ ํด๋ฆญํ๋ฉด ์๋ฌ๋ฅผ ์ด๊ธฐํํ๊ฒ๋ ์์ฑํ๋ค.
- Error๊ฐ ๋ฐ์ํ๋ฉด ErrorBoundary์
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;
- ๊ทธ๋ฆฌ๊ณ App.js์๋ค QueryErrorBoundary ์ปดํฌ๋ํธ๋ฅผ ์ถ๊ฐํ๋ฉด ๋๋ค. ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ queryClient ์ต์
์๋ค
{ 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>
);
}
- ErrorBoundary๋ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ ๋ณด์ฌ์ฃผ๋ Fallback UI๋ฅผ
์ ์ธ์
์ผ๋ก ์์ฑํ ์ ์๊ณ , ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋Suspense
์๋ ๊ฒฐํฉํด์์๋ฒ ํต์ ์ํ๊ฐ ๋ก๋ฉ ์ค
์ผ ๋ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๊ฒ ์ ์ธ์ ์ผ๋ก ์์ฑํ ์ ์๋ค. - ์ฐธ๊ณ ๋ก, Suspense ์ปดํฌ๋ํธ๋ ๋ฆฌ์กํธ v16๋ถํฐ ์ ๊ณต๋๋
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>;
);
}
- ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ฐ๋ฆฌ๋ ์๋ฒ ์ํ๊ฐ ๋ก๋ฉ์ผ ๋ Loader ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๊ฒ ๋ค!๋ผ๊ณ ์ดํดํ ์ ์๋ค.
- Suspense์ปดํฌ๋ํธ ๋ด๋ถ์์ ์ด๋ค ๋ก์ง์ด ๋์ํ๋์ง ์ฐ๋ฆฌ๋ ์ ๊ฒฝ ์ฐ์ง ์์๋๋๋ค. ์ด์ฒ๋ผ
๋ด๋ถ ๋ณต์ก์ฑ์ ์ถ์ํ
ํ๋ ๊ฒ ๋ฐ๋ก์ ์ธํ ์ปดํฌ๋ํธ
์ด๋ค. - ์์ ๊ฐ์ด
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
- new hooks for suspense
- v5์์๋
data fetching
์ ๋ํsuspense
๊ฐ ๋ง์นจ๋ด ์์ ํ๋์์ต๋๋ค. useSuspenseQuery
,useSuspenseInfiniteQuery
,useSuspenseQueries
3๊ฐ์ง ํ ์ด ์ถ๊ฐ๋์์ต๋๋ค.- ๊ธฐ์กด์
suspense ์ต์
์ ์ ๊ฑฐ๋์์ต๋๋ค. ๋ฐ๋ผ์ Suspense๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์ ํ ๋ค์ ํ์ฉํด์ผ ํฉ๋๋ค. - ์ 3๊ฐ์ง ํ
์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ํ์
๋ ๋ฒจ์์
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,
});
-
TanStack Query(React) ๊ณต์ ๋ฌธ์์
Community Resources
์์๋ Suspense๋ฅผ ๋ํ์ ์ธ์ดํ
ํ๊ฒ ์ ์ฌ์ฉํ๊ธฐ ์ํด useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery๋ฅผ ์ ๊ณตํ๋ @suspensive/react-query๋ฅผ ์๊ฐํ๊ณ ์๋ค. -
suspensive/react-query์ ํ (useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery)์ @tanstack/react-query v5 ๋ฒ์ ์ ์ถ๊ฐ(๊ด๋ จ Pull Request)๋๊ณ ๊ณต์ API๋ก ์ด ํ์ด์ง์์ ํ์ธํ ์ ์์ต๋๋ค.
- Default Query Function ๊ณต์ ๋ฌธ์
- ์ฑ ์ ์ฒด์์ ๋์ผํ ์ฟผ๋ฆฌ ํจ์๋ฅผ ๊ณต์ ํ๊ณ ,
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),
});
};
- useQuery์ ์ฒซ ๋ฒ์งธ ์ธ์๋ก
queryKey
๋ง ๋ฃ์ด์ฃผ๋ฉด ๋ ๋ฒ์งธ ์ธ์์ ๋ค์ด๊ฐqueryFn
์ ์๋์ผ๋ก ์ค์ ๋ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ค์ด๊ฐ๋ค. - ์ผ๋ฐ์ ์ผ๋ก
useQuery
๋ฅผ ์ฌ์ฉํ ๋์ ๋ฌ๋ฆฌqueryFn
์ ์ง์ ํ์ง ์๊ธฐ์ ์ฟผ๋ฆฌ ํจ์์ ์ง์ ์ธ์๋ฅผ ๋ฃ์ด์ฃผ๋ ํํ์ ์ฌ์ฉ์ ๋ถ๊ฐ๋ฅํ๋ค.
- React Query๋ TypeScript์
์ ๋ค๋ฆญ(Generics)
์ ๋ง์ด ์ฌ์ฉํ๋ค. ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๊ณ API๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ ์ ํ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
ํ์ฌ useQuery๊ฐ ๊ฐ๊ณ ์๋ ์ ๋ค๋ฆญ์ 4๊ฐ
์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
- TQueryFnData: useQuery๋ก ์คํํ๋ query function์
์คํ ๊ฒฐ๊ณผ
์ ํ์ ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค. - TError: query function์
error
ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค. - TData: useQuery์
data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ
์ ํ์ ์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์select
์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์ ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค. - TQueryKey: useQuery์ ์ฒซ ๋ฒ์งธ ์ธ์
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๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
- TData: useMutation์ ๋๊ฒจ์ค mutation function์
์คํ ๊ฒฐ๊ณผ
์ ํ์ ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค.- data์ ํ์ ๊ณผ onSuccess(1๋ฒ์งธ ์ธ์) ์ธ์์ ํ์ ์ผ๋ก ํ์ฉ๋๋ค.
- TError: useMutation์ ๋๊ฒจ์ค mutation function์
error
ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค. - TVariables:
mutate ํจ์
์ ์ ๋ฌ ํ ์ธ์๋ฅผ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค.- onSuccess(2๋ฒ์งธ ์ธ์), onError(2๋ฒ์งธ ์ธ์), onMutate(1๋ฒ์งธ ์ธ์), onSettled(3๋ฒ์งธ ์ธ์) ์ธ์์ ํ์ ์ผ๋ก ํ์ฉ๋๋ค.
- TContext: mutation function์ ์คํํ๊ธฐ ์ ์ ์ํํ๋
onMutate ํจ์์ return๊ฐ
์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค.- onMutate์ ๊ฒฐ๊ณผ๊ฐ์ ํ์ ์ onSuccess(3๋ฒ์งธ ์ธ์), onError(3๋ฒ์งธ ์ธ์), onSettled(4๋ฒ์งธ ์ธ์)์์ ํ์ฉํ๋ ค๋ฉด ํด๋น ํ์ ์ ์ง์ ํด์ผ ํ๋ค.
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์ ์ ์ฌํ๋ค.
- TQueryFnData: useInfiniteQuery๋ก ์คํํ๋ query function์
์คํ ๊ฒฐ๊ณผ
์ ํ์ ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค. - TError: query function์
error
ํ์์ ์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค. - TData: useInfiniteQuery์
data์ ๋ด๊ธฐ๋ ์ค์ง์ ์ธ ๋ฐ์ดํฐ
์ ํ์ ์ ๋งํ๋ค. ์ฒซ ๋ฒ์งธ ์ ๋ค๋ฆญ๊ณผ์ ์ฐจ์ด์ ์select
์ ๊ฐ์ด query function์ ๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํธ๋ค๋ง์ ํตํด ๋ฐํํ๋ ๊ฒฝ์ฐ์ ๋์ํ ์ ์๋ ํ์ ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ข๋ค. - TQueryKey: useInfiniteQuery์ ์ฒซ ๋ฒ์งธ ์ธ์
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;
},
// ...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>>
*/
- TypeScript ๊ณต์ ๋ฌธ์
- ์์ ์ ๋ค๋ฆญ์ ๋ชจ๋ ์ฌ์ฉํ๋ ๊ฑด ์ฝ๋์ ๋ณต์ก๋๊ฐ ๋์ด๋๋ค. ํ์ง๋ง react query๋ ํ์ ์ ์ ์ ๋ฌํ๋ฏ๋ก ๊ตณ์ด ์ ๋ค๋ฆญ์ ๋ชจ๋ ์ง์ ์ ๊ณตํ ํ์๊ฐ ์๋ค.
- ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์
queryFn
์ ํ์ ์ ์ ์ ์ํด์ํ์ ์ถ๋ก
์ด ์ํํ๊ฒ ๋๊ฒ ํ๋ ๊ฒ์ด๋ค.
const fetchGroups = async (): Promise<AxiosResponse<Group[]> => {
return await axios.get("/groups");
};
const { data } = useQuery({
queryKey: ["groups"],
queryFn: fetchGroups,
select: (data) => data.data,
});
/**
์ฃผ์ ํ์
* data: AxiosResponse<Group[]> | undefined
* error: Error | null
* select: (data: AxiosResponse<Group[]>): Group[]
*/
- Tanstack Query๋
์์ฒด 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 ์ถ๊ฐ ๊ฐ๋ฅ... */
},
};
- ๋ฌผ๋ก , rule์ ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด rules์ ์๋
์ฌ์ฉ๋ฐฉ๋ฒ(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
์ ๋๋ค.- React v18 ์ด์์์ ์ง์ํ๋
useSyncExternalStore
ํ ์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
- React v18 ์ด์์์ ์ง์ํ๋
-
Tanstack Query v5์ ๋ธ๋ผ์ฐ์ ๋ณ ์ง์ ๋ฒ์ ์ ์๋์ ๊ฐ์ต๋๋ค.
Chrome >= 91
Firefox >= 90
Edge >= 91
Safari >= 15
iOS >= 15
opera >= 77