- ํด๋น ์ ์ฅ์๋ TanStack Query(React Query v4)์์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ ๋ค์ ์ ๋ฆฌํ ์ ์ฅ์์ ๋๋ค. TanStack Query(React Query v4)์ ๋ชจ๋ ํ์ฉ ๋ฐฉ๋ฒ์ด ์์ฑ๋ ์ํ๋ ์๋๋ฉฐ, ํ์ํ ๋ด์ฉ์ ์ถ๊ฐ, ๋ณด์ํ ์์ ์ ๋๋ค.
- ์คํ์, ๊ฐ๋
์ฑ ์์ข์ ๋ถ๋ถ ๋๋ ์ถ๊ฐ ๋ด์ฉ์
Pull Request
,Issue
๋ฑ ์์ ๋กญ๊ฒ ๋จ๊ฒจ์ฃผ์๋ฉด ๊ฒํ ํ์ ๋ฐ์ํ๊ฒ ์ต๋๋ค.
- TanStack Query(React Query v4)๋
React Query v3์ ๋๋ถ๋ถ์ ๊ธฐ๋ฅ์ ํธํ
ํฉ๋๋ค. ์ฃผ์ ์ฐจ์ด์ ์ ์๋ ๋ฌธ์์ ๊ฐ๋ตํ๊ฒ ์ ๋ฆฌํ์ต๋๋ค. ์ฐธ๊ณ ํด์ฃผ์๋ฉด ๊ฐ์ฌ๋๋ฆฝ๋๋ค ๐โโ๏ธ - TanStack Query v3 vs v4 ๋น๊ต ๋ฌธ์
- React Query ๊ฐ์ ๋ฐ ๊ธฐ๋ฅ
- ๊ธฐ๋ณธ ์ค์ (QueryClientProvider, QueryClient)
- React Query Devtools
- React Query ์บ์ฑ ๋ผ์ดํ ์ฌ์ดํด
- useQuery
- useQuery ์ฃผ์ ๋ฆฌํด ๋ฐ์ดํฐ
- staleTime๊ณผ cacheTime
- ๋ง์ดํธ ๋ ๋๋ง๋ค ์ฌ์์ฒญํ๋ refetchOnMount
- ์๋์ฐ๊ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค ์ฌ์์ฒญํ๋ refetchOnWindowFocus
- Polling ๋ฐฉ์์ ๊ตฌํํ๊ธฐ ์ํ refetchInterval์ refetchIntervalInBackground)
- ์๋ ์คํ์ enabled์ ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ์์ฒญํ๋ refetch
- ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด ์ฌ์์ฒญํ๋ retry
- onSuccess, onError, onSettled
- select๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ๋ณํ
- Paginated ๊ตฌํ์ ์ ์ฉํ keepPreviousData
- ์ฟผ๋ฆฌ๋ฅผ ๋ณ๋ ฌ(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
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์ ํ์ ์คํฌ๋ฆฝํธ ์ ์ฉ
- react-query๋ ๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์
์์
์๋ฒ ์ํ ๊ฐ์ ธ์ค๊ธฐ
,์บ์ฑ
,๋๊ธฐํ ๋ฐ ์ ๋ฐ์ดํธ
๋ฅผ ๋ณด๋ค ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ํด๋ผ์ด์ธํธ ์ํ์ ์๋ฒ ์ํ๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋ค. - react-query์์๋ ๊ธฐ์กด ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ
redux
,mobX
๊ฐํด๋ผ์ด์ธํธ ์ํ ์์
์ ์ ํฉํ์ง๋ง,๋น๋๊ธฐ ๋๋ ์๋ฒ ์ํ ์์
์๋ ๊ทธ๋ค์ง ์ข์ง ์๋ค๊ณ ์ธ๊ธํ๋ค. - ํด๋ผ์ด์ธํธ ์ํ(Client State)์ ์๋ฒ ์ํ(Server State)๋ ์์ ํ ๋ค๋ฅธ ๊ฐ๋ ์ด๋ฉฐ, ํด๋ผ์ด์ธํธ ์ํ๋ ๊ฐ๊ฐ์ input ๊ฐ์ผ๋ก ์๋ฅผ ๋ค ์ ์๊ณ , ์๋ฒ ์ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋์ด ์๋ ๋ฐ์ดํฐ๋ก ์๋ฅผ ๋ค ์ ์๋ค.
- ์บ์ฑ
- ๋์ผํ ๋ฐ์ดํฐ์ ๋ํ ์ค๋ณต ์์ฒญ์ ๋จ์ผ ์์ฒญ์ผ๋ก ํตํฉ
- ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค๋๋ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ
- ๋ฐ์ดํฐ๊ฐ ์ผ๋ง๋ ์ค๋๋์๋์ง ์ ์ ์๋ค.
- ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๋ฅผ ๊ฐ๋ฅํ ๋น ๋ฅด๊ฒ ๋ฐ์
- ํ์ด์ง๋ค์ด์ ๋ฐ ๋ฐ์ดํฐ ์ง์ฐ ๋ก๋์ ๊ฐ์ ์ฑ๋ฅ ์ต์ ํ
- ์๋ฒ ์ํ์ ๋ฉ๋ชจ๋ฆฌ ๋ฐ ๊ฐ๋น์ง ์์ง ๊ด๋ฆฌ
- ๊ตฌ์กฐ ๊ณต์ ๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจํ
// QueryClient ์์
// v3
import { QueryClient } from "react-query";
// v4
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
// ...
},
},
});
- QueryClient๋ฅผ ์ฌ์ฉํ์ฌ
์บ์
์ ์ํธ ์์ฉํ ์ ์๋ค. - QueryClient์์ ๋ชจ๋
query
๋๋mutation
์ ๊ธฐ๋ณธ ์ต์ ์ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ์ข ๋ฅ๊ฐ ์๋นํ๊ธฐ ๋๋ฌธ์ ๊ณต์ ์ฌ์ดํธ๋ฅผ ์ฐธ๊ณ ํด๋ณด์.
// QueryClientProvider + QueryClient
// v3
import { QueryClient, QueryClientProvider } from "react-query";
// v4
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 prosp์๋ค queryClient๋ฅผ ์ฐ๊ฒฐํจ์ผ๋ก์จ, ์ด context๋ ์ฑ์์ ๋น๋๊ธฐ ์์ฒญ์ ์์์ ์ฒ๋ฆฌํ๋
background
๊ณ์ธต์ด ๋๋ค.
- react-query๋
์ ์ฉ devtools
๋ฅผ ์ ๊ณตํ๋ค. - devtools๋ฅผ ์ฌ์ฉํ๋ฉด React Query์ ๋ชจ๋ ๋ด๋ถ ๋์์
์๊ฐํ
ํ๋ ๋ฐ ๋์์ด ๋๋ฉฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด๋๋ฒ๊น ์๊ฐ์ ์ ์ฝ
ํ ์ ์๋ค. - devtools๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก
process.env.NODE_ENV === 'development'
์ธ ๊ฒฝ์ฐ์๋ง ์คํ๋๋ค, ์ฆ ์ผ๋ฐ์ ์ผ๋ก ๊ฐ๋ฐํ๊ฒฝ์์๋ง ์๋ํ๋ฏ๋ก ์ค์ ๋์ด์์ผ๋ฏ๋ก, ํ๋ก์ ํธ ๋ฐฐํฌ ์์ Devtools ์ฝ์ ์ฝ๋๋ฅผ ์ ๊ฑฐํด์ค ํ์๊ฐ ์๋ค.
// v3
import { ReactQueryDevtools } from "react-query/devtools";
<AppContext.Provider value={user}>
<QueryClientProvider client={queryClient}>
// ...
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</AppContext.Provider>;
- initialIsOpen (Boolean)
true
์ด๋ฉด ๊ฐ๋ฐ ๋๊ตฌ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ ค ์๋๋ก ์ค์ ํ ์ ์๋ค.
- position?: ("top-left" | "top-right" | "bottom-left" | "bottom-right")
- ๊ธฐ๋ณธ๊ฐ:
bottom-left
- devtools ํจ๋์ ์ด๊ณ ๋ซ๊ธฐ ์ํ ๋ก๊ณ ์์น
- ๊ธฐ๋ณธ๊ฐ:
- ์ผ๋ฐ์ ์ผ๋ก initialIsOpen, position์ ์์ฃผ ์ฌ์ฉํ์ง๋ง, panelProps, closeButtonProps, toggleButtonProps์ ๊ฐ์ ์ต์ ๋ค๋ ์กด์ฌํ๋ค.
- v4๋ถํฐ๋ devtools๋ฅผ ์ํ ๋ณ๋์ ํจํค์ง ์ค์น๊ฐ ํ์ํ๋ค.
$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn 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>
);
}
- React-Query ์บ์ ๋ผ์ดํ ์ฌ์ดํด
* Query Instances with and without cache data(์บ์ ๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ์๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค)
* Background Refetching(๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฆฌํจ์นญ)
* Inactive Queries(๋นํ์ฑ ์ฟผ๋ฆฌ)
* Garbage Collection(๊ฐ๋น์ง ์ปฌ๋ ์
)
cacheTime
์ ๊ธฐ๋ณธ๊ฐ 5๋ถ,staleTime
๊ธฐ๋ณธ๊ฐ 0์ด๋ฅผ ๊ฐ์
A
๋ผ๋ queryKey๋ฅผ ๊ฐ์ง A ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐmount
๋จ- ๋คํธ์ํฌ์์ ๋ฐ์ดํฐ fetchํ๊ณ , ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ A๋ผ๋ queryKey๋ก
์บ์ฑ
ํจ - ์ด ๋ฐ์ดํฐ๋
fresh
์ํ์์staleTime(๊ธฐ๋ณธ๊ฐ 0)
์ดํstale
์ํ๋ก ๋ณ๊ฒฝ๋จ - A ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ
unmount
๋จ - ์บ์๋
cacheTime(๊ธฐ๋ณธ๊ฐ 5min)
๋งํผ ์ ์ง๋๋ค๊ฐ๊ฐ๋น์ง ์ฝ๋ ํฐ(GC)
๋ก ์์ง๋จ - ๋ง์ผ, cacheTime์ด ์ง๋๊ธฐ ์ ์ด๊ณ , A ์ฟผ๋ฆฌ ์ธ์คํด์ค freshํ ์ํ๋ผ๋ฉด ์๋กญ๊ฒ mount๋๋ฉด ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ค๋ค.
// ์ฌ์ฉ๋ฒ(1)
const { data, isLoading, ... } = useQuery(queryKey, queryFn, {
// ...options ex) enabled, staleTime, ...
});
// ์ฌ์ฉ๋ฒ(2)
const result = useQuery({
queryKey,
queryFn,
// ...options ex) enabled, staleTime, ...
});
result.data
result.isLoading
// ...
// ์ค์ ์์
const getAllSuperHero = async () => {
return await axios.get("http://localhost:4000/superheroes");
};
const { data, isLoading } = useQuery(["super-heroes"], getAllSuperHero);
- useQuery๋ ๊ธฐ๋ณธ์ ์ผ๋ก 3๊ฐ์ ์ธ์๋ฅผ ๋ฐ๋๋ค. ์ฒซ ๋ฒ์งธ ์ธ์๊ฐ
queryKey(ํ์)
, ๋ ๋ฒ์งธ ์ธ์๊ฐqueryFn(ํ์)
, ์ธ ๋ฒ์งธ ์ธ์๊ฐoptions(optional)
์ด๋ค.
1. queryKey
// (1)
const getSuperHero = async ({ queryKey }: any) => {
const heroId = queryKey[1]; // queryKey: ['super-hero', '3']
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
// ํด๋น ์ฟผ๋ฆฌ๋ heroId์ ์์กด
return useQuery(["super-hero", heroId], getSuperHero);
};
-
v3๊น์ง๋ queryKey๋ก ๋ฌธ์์ด ๋๋ ๋ฐฐ์ด ๋ชจ๋ ์ง์ ํ ์ ์๋๋ฐ,
v4
๋ถํฐ๋ ๋ฌด์กฐ๊ฑด๋ฐฐ์ด
๋ก ์ง์ ํด์ผ ํ๋ค. -
useQuery๋ ์ฒซ ๋ฒ์งธ ์ธ์์ธ
queryKey
๋ฅผ ๊ธฐ๋ฐ์ผ๋ก๋ฐ์ดํฐ ์บ์ฑ
์ ๊ด๋ฆฌํ๋ค.- ๋ง์ฝ, ์ฟผ๋ฆฌ๊ฐ ํน์ ๋ณ์์
์์กด
ํ๋ค๋ฉด ๋ฐฐ์ด์๋ค ์ด์ด์ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.ex: ["super-hero", heroId, ...]
- ์ด๋ ์ฌ์ค ๊ต์ฅํ ์ค์ํ๋ค. ์๋ฅผ ๋ค์ด,
queryClient.setQueryData
๋ฑ๊ณผ ๊ฐ์ด ํน์ ์ฟผ๋ฆฌ์ ์ ๊ทผ์ด ํ์ ํ ๋์ด๊ธฐ์ ์ค์ ํด๋ ํฌ๋งท
์ ์ง์ผ์ค์ผ ์ ๋๋ก ์ฟผ๋ฆฌ์ ์ ๊ทผ ํ ์ ์๋ค. - ์๋ options ์์ ๋ฅผ ์ดํด๋ณด๋ฉด useSuperHeroData์ queryKey๋
["super-hero", heroId]
์ด๋ค. ๊ทธ๋ ๋ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์ฉํ ๋ ๋๊ฐ์ด["super-hero", heroId]
ํฌ๋งท์ ๊ฐ์ ธ์ผ ํ๋ค. ์๊ทธ๋ฌ๋ฉด ์ ๋๋ก ์ํ๋ ์ฟผ๋ฆฌ ์ ๊ทผ์ด ์๋๋ค.
- ๋ง์ฝ, ์ฟผ๋ฆฌ๊ฐ ํน์ ๋ณ์์
2. queryFn
// (2)
const getSuperHero = async (heroId: string) => {
return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
const useSuperHeroData = (heroId: string) => {
return useQuery(["super-hero", heroId], () => getSuperHero(heroId));
};
- useQuery์ ๋ ๋ฒ์งธ ์ธ์์ธ queryFn๋
Promise
๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ๋ฃ์ด์ผํ๋ค. - ์ฐธ๊ณ ๋ก, queryKey์ ์์ ์ queryFn ์์ ๊ฐ
์ฝ๊ฐ ์ฐจ์ด์
์ด ์๋ค.- queryKey ์์ ๋ 2๋ฒ์งธ queryFn์ getSuperHero ํจ์๋ฅผ ๋ฐ๋ก ๋๊ฒจ์ฃผ๊ณ , getSuperHero์์ ๋งค๊ฐ ๋ณ์๋ก ๊ฐ์ฒด๋ฅผ ๋ฐ์์ ํด๋น ๊ฐ์ฒด์ queryKey๋ฅผ ํ์ฉํ๊ณ ์๋ค.
- queryFn ์์ ๋ ๊ทธ๋ฅ 2๋ฒ์งธ queryFn์ ํ์ดํ ํจ์๋ฅผ ์ฌ์ฉํ๊ณ , getSuperHero์ ์ธ์๋ก heroId๋ฅผ ๋๊ฒจ์ฃผ๊ณ ์๋ค.
- ํด๋น ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ชจ๋ ์์์ผ๋๊ณ , ๊ฒฐ๊ณผ๋ ๋์ผํ๋ค.
3. options
- useQuery์ ์ธ ๋ฒ์งธ ์ธ์์ธ
options
์ ๋ง์ด ์ฐ์ด๋ ์ต์ ๋ค์ ์๋ ๋ด์ฉ์์ ์ค๋ช ํ ์์ ์ด๋ค. ๋ฌธ์ ์ธ์ ๋ ๋ง์ ์ต์ ๋ค์ ์๊ณ ์ถ๋ค๋ฉด useQuery ๊ณต์ ๋ฌธ์๋ฅผ ํตํด ํ์ธํด๋ณด์.
// ์
const useSuperHeroData = (heroId: string) => {
return useQuery(["super-hero", heroId], () => getSuperHero(heroId), {
cacheTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
retry: 1,
// ...options
});
};
const { status, isLoading, isError, error, data, isFetching, ... } = useQuery(
["colors", pageNum],
() => fetchColors(pageNum)
);
- status: ์ฟผ๋ฆฌ ์์ฒญ ํจ์์ ์ํ๋ฅผ ํํํ๋ status๋ 4๊ฐ์ง์ ๊ฐ์ด ์กด์ฌํ๋ค.(๋ฌธ์์ด ํํ)
- idle: ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๊ฐ ์๊ณ ๋น์์ ๋,
{ enabled: false }
์ํ๋ก ์ฟผ๋ฆฌ๊ฐ ํธ์ถ๋๋ฉด ์ด ์ํ๋ก ์์๋๋ค. - loading: ๋ง ๊ทธ๋๋ก ์์ง ์บ์๋ ๋ฐ์ดํฐ๊ฐ ์๊ณ ๋ก๋ฉ์ค์ผ ๋ ์ํ
- error: ์์ฒญ ์๋ฌ ๋ฐ์ํ์ ๋ ์ํ
- success: ์์ฒญ ์ฑ๊ณตํ์ ๋ ์ํ
- idle: ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๊ฐ ์๊ณ ๋น์์ ๋,
- data: ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ฆฌํดํ Promise์์
resolved
๋ ๋ฐ์ดํฐ - isLoading:
์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์์ ๋
์ฆ, ์ฒ์ ์คํ๋ ์ฟผ๋ฆฌ ์ผ ๋ ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ true/false๋ก ๋ฐํ๋๋ค.- ์ด๋ ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ๋ก๋ฉ ์ฌ๋ถ์ ์๊ด์์ด false๋ฅผ ๋ฐํํ๋ค.
- isFetching: ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ๋ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ฉด ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ true/false๋ก ๋ฐํ๋๋ค.
- ์ด๋ ์บ์ฑ ๋ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ๋ ์ฟผ๋ฆฌ ๋ก๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ true/false๋ฅผ ๋ฐํํ๋ค.
- error: ์ฟผ๋ฆฌ ํจ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, ์ฟผ๋ฆฌ์ ๋ํ ์ค๋ฅ ๊ฐ์ฒด
- isError: ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
true
- ๊ทธ ์ธ ๋ฐํ ๋ฐ์ดํฐ๋ค์ ์์ธํ ์๊ณ ์ถ์ผ๋ฉด useQuery ๊ณต์ ์ฌ์ดํธ ๋ฌธ์ ์ฐธ๊ณ
- TanStack Query(v4) ๋ถํฐ๋ status์
idle์ด ์ ๊ฑฐ
๋๊ณ , ์๋ก์ด ์ํ๊ฐ์ธfetchStatus
๊ฐ ์ถ๊ฐ๋๋ค. - fetchStatus
- fetching: ์ฟผ๋ฆฌ๊ฐ ํ์ฌ ์คํ์ค์ด๋ค.
- paused: ์ฟผ๋ฆฌ๋ฅผ ์์ฒญํ์ง๋ง, ์ ์ ์ค๋จ๋ ์ํ์ด๋ค.
- idle: ์ฟผ๋ฆฌ๊ฐ ํ์ฌ ์๋ฌด ์์ ๋ ์ํํ์ง ์๋ ์ํ์ด๋ค.
-
fetchStatus๋ HTTP ๋คํธ์ํฌ ์ฐ๊ฒฐ ์ํ์ ์ข ๋ ๊ด๋ จ๋ ์ํ ๋ฐ์ดํฐ์ด๋ค.
- ์๋ฅผ ๋ค์ด, status๊ฐ
success
์ํ๋ผ๋ฉด ์ฃผ๋ก fetchStatus๋idle
์ํ์ง๋ง, ๋ฐฑ๊ทธ๋ผ์ด๋์์ re-fetch๊ฐ ๋ฐ์ํ ๋fetching
์ํ์ผ ์ ์๋ค. - status๊ฐ ๋ณดํต
loading
์ํ์ผ ๋ fetchStatus๋ ์ฃผ๋กfetching
๋ฅผ ๊ฐ์ง๋ง, ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋์ด ์์ง ์์ ๊ฒฝ์ฐpaused
์ํ๋ฅผ ๊ฐ์ง ์ ์๋ค.
- ์๋ฅผ ๋ค์ด, status๊ฐ
-
์ ๋ฆฌํ์๋ฉด ์๋์ ๊ฐ๋ค.
- status๋
data
๊ฐ ์๋์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค. - fetchStatus๋ ์ฟผ๋ฆฌ ์ฆ,
queryFn ์์ฒญ
์ด ์งํ์ค์ธ์ง ์๋์ง์ ๋ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.
- status๋
- ์ถ๊ฐ์ ์ธ ์ต์ ๋ค์ useQuery v4 ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ
- stale์ ์ฉ์ด ๋ป๋๋ก
์ฉ์
์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๊ฐ ์๋๋ผ๋ ์๋ฏธ์ด๋ค. - fresh๋ ๋ป ๊ทธ๋๋ก
์ ์ ํ
์ด๋ผ๋ ์๋ฏธ์ด๋ค. ์ฆ, ์ต์ ์ํ๋ผ๋ ์๋ฏธ์ด๋ค.
const { isLoading, isFetching, data, isError, error } = useQuery(
["super-hero"],
getSuperHero,
{
cacheTime: 5 * 60 * 1000, // 5๋ถ
staleTime: 1 * 60 * 1000, // 1๋ถ
}
);
- staleTime:
(number | Infinity)
- staleTime์ ๋ฐ์ดํฐ๊ฐ
fresh์์ stale
์ํ๋ก ๋ณ๊ฒฝ๋๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ, ๋ง์ฝ staleTime์ด 3000์ด๋ฉด fresh์ํ์์ 3์ด ๋ค์ stale๋ก ๋ณํ fresh
์ํ์ผ ๋๋ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ์๋กญ๊ฒ mount ๋์ด๋ ๋คํธ์ํฌ ์์ฒญ(fetch)์ด ์ผ์ด๋์ง ์๋๋ค.- ๋ฐ์ดํฐ๊ฐ ํ๋ฒ fetch ๋๊ณ ๋์
staleTime
์ด ์ง๋์ง ์์๋ค๋ฉด(fresh์ํ) unmount ํ ๋ค์ mount ๋์ด๋ fetch๊ฐ ์ผ์ด๋์ง ์๋๋ค. - staleTime์ ๊ธฐ๋ณธ๊ฐ์
0
์ด๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก fetch ํ์ ๋ฐ๋ก stale์ด ๋๋ค.
- staleTime์ ๋ฐ์ดํฐ๊ฐ
- cacheTime:
(number | Infinity)
- ๋ฐ์ดํฐ๊ฐ
inactive
์ํ์ผ ๋์บ์ฑ ๋ ์ํ๋ก
๋จ์์๋ ์๊ฐ - ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ unmount ๋๋ฉด ๋ฐ์ดํฐ๋
inactive ์ํ๋ก ๋ณ๊ฒฝ
๋๋ฉฐ, ์บ์๋cacheTime
๋งํผ ์ ์ง๋๋ค. - cacheTime์ด ์ง๋๋ฉด
๊ฐ๋น์ง ์ฝ๋ ํฐ
๋ก ์์ง๋๋ค. - cacheTime์ด ์ง๋๊ธฐ ์ ์ ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ๋ค์ mount ๋๋ฉด, ๋ฐ์ดํฐ๋ฅผ fetchํ๋ ๋์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ค๋ค.
- cacheTime์ staleTime๊ณผ ๊ด๊ณ์์ด, ๋ฌด์กฐ๊ฑด inactive ๋ ์์ ์ ๊ธฐ์ค์ผ๋ก ์บ์ ๋ฐ์ดํฐ ์ญ์ ๋ฅผ ๊ฒฐ์ ํ๋ค.
- cacheTime์ ๊ธฐ๋ณธ๊ฐ์
5๋ถ
์ด๋ค.
- ๋ฐ์ดํฐ๊ฐ
- ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ staleTime๊ณผ cacheTime์ ๊ธฐ๋ณธ๊ฐ์ ๊ฐ๊ฐ
0๋ถ
๊ณผ5๋ถ
์ด๋ค. ๋ฐ๋ผ์ staleTime์ ์ด๋ ํ ์ค์ ๋ ํ์ง ์์ผ๋ฉด ์บ์ฑ์ด ์ ํ ๋์ง ์๋๋ค. ์๋ํ๋ฉด, ํญ์ ์บ์ฑ ๋์ด ์๋ ๋ฐ์ดํฐ๊ฐstale
ํ๋ค๊ณ ์ฌ๊ธฐ๊ธฐ ๋๋ฌธ์ด๋ค. - staleTime์ ๊ธธ๊ฒ ์ค์ ํ๋๋ผ๋ cacheTime์ด ์งง๋ค๋ฉด ์ด ๋ํ ์บ์ฑ์ด ์ํํ๊ฒ ์งํ๋์ง ์์ ๊ฒ์ด๋ค. ๊ฒฐ๊ตญ์๋ ๋ ๊ฐ์ ์ต์ ์ ์ ์ ํ๊ฒ ์ค์ ํด์ค์ผ ํ๋ค.
const { isLoading, isFetching, data, isError, error } = useQuery(
["super-hero"],
getSuperHero,
{
refetchOnMount: true,
}
);
- refetchOnMount (boolean | "always")
- refetchOnMount๋ ๋ฐ์ดํฐ๊ฐ
stale
์ํ์ผ ๊ฒฝ์ฐ, mount๋ง๋คrefetch
๋ฅผ ์คํํ๋ ์ต์ ์ด๋ค. ๊ธฐ๋ณธ๊ฐ์true
์ด๋ค. always
๋ก ์ค์ ํ๋ฉด ๋ง์ดํธ ์๋ง๋ค ๋งค๋ฒ refetch๋ฅผ ์คํํ๋ค.false
๋ก ์ค์ ํ๋ฉด ์ต์ด fetch ์ดํ์๋ refetchํ์ง ์๋๋ค.
const { isLoading, isFetching, data, isError, error } = useQuery(
["super-hero"],
getSuperHero,
{
refetchOnWindowFocus: true,
}
);
- refetchOnWindowFocus๋ ๋ฐ์ดํฐ๊ฐ
stale
์ํ์ผ ๊ฒฝ์ฐ์๋์ฐ ํฌ์ปค์ฑ
๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ ์ต์ ์ด๋ค. ๊ธฐ๋ณธ๊ฐ์true
์ด๋ค. - ์๋ฅผ ๋ค์ด, ํฌ๋กฌ์์ ๋ค๋ฅธ ํญ์ ๋๋ ๋ค๊ฐ ๋ค์ ์๋ ๋ณด๋ ์ค์ธ ํญ์ ๋๋ ์ ๋๋ ์ด ๊ฒฝ์ฐ์ ํด๋นํ๋ค. ์ฌ์ง์ด F12๋ก ๊ฐ๋ฐ์ ๋๊ตฌ ์ฐฝ์ ์ผ์ ๋คํธ์ํฌ ํญ์ด๋ , ์ฝ์ ํญ์ด๋ ๊ฐ๋ฐ์ ๋๊ตฌ ์ฐฝ์์ ๋๋ค๊ฐ ํ์ด์ง ๋ด๋ถ๋ฅผ ๋ค์ ํด๋ฆญํ์ ๋๋ ์ด ๊ฒฝ์ฐ์ ํด๋นํ๋ค.
always
๋ก ์ค์ ํ๋ฉด ํญ์ ์๋์ฐ ํฌ์ปค์ฑ ๋ ๋๋ง๋ค refetch๋ฅผ ์คํํ๋ค๋ ์๋ฏธ์ด๋ค.
const { isLoading, isFetching, data, isError, error } = useQuery(
["super-hero"],
getSuperHero,
{
refetchInterval: 2000,
refetchIntervalInBackground: true,
}
);
- Polling(ํด๋ง)์ด๋? ๋ฆฌ์ผํ์ ์น์ ์ํ ๊ธฐ๋ฒ์ผ๋ก
์ผ์ ํ ์ฃผ๊ธฐ(ํน์ ํ ์๊ฐ)
๋ฅผ ๊ฐ์ง๊ณ ์๋ฒ์ ์๋ต์ ์ฃผ๊ณ ๋ฐ๋ ๋ฐฉ์์ด ํด๋ง ๋ฐฉ์์ด๋ค. - react-query์์๋
refetchInterval
,refetchIntervalInBackground
์ ์ด์ฉํด์ ๊ตฌํํ ์ ์๋ค. refetchInterval
์ ์๊ฐ(ms)๋ฅผ ๊ฐ์ผ๋ก ๋ฃ์ด์ฃผ๋ฉด ์ผ์ ์๊ฐ๋ง๋ค ์๋์ผ๋ก refetch๋ฅผ ์์ผ์ค๋ค.refetchIntervalInBackground
๋refetchInterval
๊ณผ ํจ๊ป ์ฌ์ฉํ๋ ์ต์ ์ด๋ค. ํญ/์ฐฝ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์๋ ๋์ refetch ์์ผ์ค๋ค. ์ฆ, ๋ธ๋ผ์ฐ์ ์ focus๋์ด ์์ง ์์๋ refetch๋ฅผ ์์ผ์ฃผ๋ ๊ฒ์ ์๋ฏธํ๋ค.
const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
["super-hero"],
getSuperHero,
{
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
๋ ์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ์คํ๋์ง ์๋๋ก ํ ๋ ์ค์ ํ ์ ์๋ค.false
๋ฅผ ์ฃผ๋ฉด ์๋ ์คํ๋์ง ์๋๋ค. ๋ํ, useQuery ๋ฆฌํด ๋ฐ์ดํฐ ์ค status๊ฐ idle ์ํ๋ก ์์ํ๋ค.refetch
๋ ์ฟผ๋ฆฌ๋ฅผ์๋
์ผ๋ก ๋ค์ ์์ฒญํ๋ ๊ธฐ๋ฅ์ด๋ค. ์ฟผ๋ฆฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ์ค๋ฅ๋ง ๊ธฐ๋ก๋๋ค. ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๋ ค๋ฉดthrowOnError
์์ฑ์true
๋ก ํด์ ์ ๋ฌํด์ผ ํ๋ค.- ๋ณดํต ์๋์ผ๋ก ์ฟผ๋ฆฌ ์์ฒญ์ ํ์ง ์๊ณ ๋ฒํผ ํด๋ฆญ์ด๋ ํน์ ์ด๋ฒคํธ๋ฅผ ํตํด ์์ฒญ์ ์๋ํ ๋ ๊ฐ์ด ์ฌ์ฉํ๋ค.
- ๋ง์ฝ
enabled: false
๋ฅผ ์คฌ๋ค๋ฉดqueryClient
๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์คinvalidateQueries
์refetchQueries
๋ฅผ ๋ฌด์ํ๋ค.
const result = useQuery(["todos", 1], fetchTodoListPage, {
retry: 10, // ์ค๋ฅ๋ฅผ ํ์ํ๊ธฐ ์ ์ ์คํจํ ์์ฒญ์ 10๋ฒ ์ฌ์๋ํฉ๋๋ค.
});
- retry (boolean | number | (failureCount: number, error: TError) => boolean)
- retry๋ ์ฟผ๋ฆฌ๊ฐ
์คํจ
ํ๋ฉด useQuery๋ฅผํน์ ํ์(๊ธฐ๋ณธ๊ฐ 3)
๋งํผ ์ฌ์์ฒญํ๋ ์ต์ ์ด๋ค. - retry๊ฐ
false
์ธ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์ ์๋ํ์ง ์๋๋ค. true
์ธ ๊ฒฝ์ฐ์๋ ์คํจํ ์ฟผ๋ฆฌ์ ๋ํด์ ๋ฌดํ ์ฌ์์ฒญ์ ์๋ํ๋ค.- ๊ฐ์ผ๋ก
์ซ์
๋ฅผ ๋ฃ์ ๊ฒฝ์ฐ, ์คํจํ ์ฟผ๋ฆฌ๊ฐ ํด๋น ์ซ์๋ฅผ ์ถฉ์กฑํ ๋๊น์ง ์์ฒญ์ ์ฌ์๋ํ๋ค.
- NOTE: ์ onSuccess, onError, onSettled Callback์
useQuery
์ต์ ์์@Deprecated
๋์ด ์ญ์ ๋ ์์ (v5์ ๋ฐ์)์ด๋ค. ๋จ,useMutation
์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.- Breaking React Query's API on purpose TkDodo ๋ฌธ์ ๋ฒ์ญ ๋ฌธ์ ์ฐธ๊ณ
const onSuccess = useCallback((data) => {
console.log("Success", data);
}, []);
const onError = useCallback((err) => {
console.log("Error", err);
}, []);
const onSettled = useCallback(() => {
console.log("Settled");
}, []);
const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
["super-hero"],
getSuperHero,
{
onSuccess,
onError,
onSettled,
}
);
onSuccess
ํจ์๋ ์ฟผ๋ฆฌ ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์งํ๋ผ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋ ์บ์๊ฐ ์ ๋ฐ์ดํธ๋ ๋๋ง๋ค ์คํ๋๋ค.onError
ํจ์๋ ์ฟผ๋ฆฌ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ณ ์ค๋ฅ๊ฐ ์ ๋ฌ๋๋ฉด ์คํ๋๋ค.onSettled
ํจ์๋ ์ฟผ๋ฆฌ ์์ฒญ์ด ์ฑ๊ณต, ์คํจ ๋ชจ๋ ์คํ๋๋ค.
const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
["super-hero"],
getSuperHero,
{
onSuccess,
onError,
select(data) {
const superHeroNames = data.data.map((hero: Data) => hero.name);
return superHeroNames;
},
}
);
return (
<div>
<button onClick={handleClickRefetch}>Fetch Heroes</button>
{data.map((heroName: string, idx: number) => (
<div key={idx}>{heroName}</div>
))}
</div>
);
select
์ต์ ์ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ํจ์์์ ๋ฐํ๋ ๋ฐ์ดํฐ์ ์ผ๋ถ๋ฅผ ๋ณํํ๊ฑฐ๋ ์ ํํ ์ ์๋ค.
const fetchColors = async (pageNum: number) => {
return await axios.get(
`http://localhost:4000/colors?_limit=2&_page=${pageNum}`
);
};
const { isLoading, isError, error, data, isFetching, isPreviousData } =
useQuery(["colors", pageNum], () => fetchColors(pageNum), {
keepPreviousData: true,
});
- keepPreviousData๋ฅผ
true
๋ก ์ค์ ํ๋ฉด ์ฟผ๋ฆฌ ํค๊ฐ ๋ณ๊ฒฝ๋์ด์ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ๋์์๋๋ง์ง๋ง data ๊ฐ์ ์ ์งํ๋ค.
- keepPreviousData์
ํ์ด์ง๋ค์ด์
๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ํธ๋ฆฌํ๋ค. ์บ์ฑ ๋์ง ์์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ๋ ๋ชฉ๋ก์ด๊น๋นก๊ฑฐ๋ฆฌ๋ ํ์์ ๋ฐฉ์ง
ํ ์ ์๋ค. - ๋ํ,
isPreviousData
๊ฐ์ผ๋ก ํ์ฌ์ ์ฟผ๋ฆฌ ํค์ ํด๋นํ๋ ๊ฐ์ธ์ง ํ์ธํ ์ ์๋ค.ํ์ด์ง๋ค์ด์
์ ์๋ก ๋ค๋ฉด, ์์ง ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์บ์ฑ ๋์ง ์์๋ค๋ฉด, ์ด์ ๋ฐ์ดํฐ์ด๋ฏ๋ก true๋ฅผ ๋ฐํํ๊ณ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ผ๋ก ๋ฐ์์ ธ ์๋ค๋ฉด ์ด์ ๋ฐ์ดํฐ๊ฐ ์๋๋ฏ๋ก false๋ฅผ ๋ฐํํ๋ค.
function Todos() {
const placeholderData = useMemo(() => generateFakeTodos(), []);
const result = useQuery(["todos"], () => fetch("/todos"), {
placeholderData,
});
}
- placeholderData๋ฅผ ์ฌ์ฉํ๋ฉด
mock ๋ฐ์ดํฐ
์ค์ ๋ ๊ฐ๋ฅํ๋ค. ๋์ ์บ์ฑ์ด ์๋๋ค๋ ๋จ์ ์ด ์๋ค.
const { data: superHeroes } = useQuery(["super-hero"], getSuperHero);
const { data: friends } = useQuery(["friends"], fetchFriends);
- ๋ช ๊ฐ์ง ์ํฉ์ ์ ์ธํ๋ฉด ์ฟผ๋ฆฌ ์ฌ๋ฌ ๊ฐ๊ฐ ์ ์ธ๋ ์ผ๋ฐ์ ์ธ ์ํฉ์ผ ๋, ์ฟผ๋ฆฌ ํจ์๋ค์
๊ทธ๋ฅ ๋ณ๋ ฌ๋ก ์์ฒญ๋ผ์ ์ฒ๋ฆฌ
๋๋ค. - ์ด๋ฌํ ํน์ง์ ์ฟผ๋ฆฌ ์ฒ๋ฆฌ์
๋์์ฑ
์ ๊ทน๋ํ ์ํจ๋ค.
// v3
const queryResults = useQueries(
heroIds.map((id) => ({
queryKey: ["super-hero", id],
queryFn: () => getSuperHero(id),
}))
);
/*
const queryResults = useQueries(
{
queryKey: ['super-hero', 1],
queryFn: () => fetchSuperHero(1)
},
{
queryKey: ['super-hero', 2],
queryFn: () => fetchSuperHero(2)
},
// ...
);
*/
- ํ์ง๋ง, ์ฟผ๋ฆฌ ์ฌ๋ฌ ๊ฐ๋ฅผ ๋์์ ์ํํด์ผ ํ๋๋ฐ, ๋ ๋๋ง์ด ๊ฑฐ๋ญ๋๋ ์ฌ์ด์ฌ์ด์ ๊ณ์ ์ฟผ๋ฆฌ๊ฐ ์ํ๋์ด์ผ ํ๋ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ํํ๋ ๋ก์ง์ด hook ๊ท์น์ ์ด๊ธ๋ ์๋ ์๋ค. ์ด๋ด ๋๋
useQueries
๋ฅผ ์ฌ์ฉํ๋ค.
- useQueries๊ฐ v4๋ถํฐ ์ฟผ๋ฆฌ๋ฅผ ๋๊ธฐ๋ ๋ฐฉ์์ด ๋ณ๊ฒฝ๋๋ค. ์ฐจ์ด์ ์ผ๋ก๋ queriesํ๋กํผํฐ๋ฅผ ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ๋๊ฒจ์ค์ผ ํ๋ค.
// v4
const queryResults = useQueries({
queries: [
{
queryKey: ["super-hero", 1],
queryFn: () => fetchSuperHero(1),
staleTime: Infinity, // ๋ค์๊ณผ ๊ฐ์ด option ์ถ๊ฐ ๊ฐ๋ฅ
},
{
queryKey: ["super-hero", 2],
queryFn: () => fetchSuperHero(2),
staleTime: 0,
},
// ...
],
});
์ข ์ ์ฟผ๋ฆฌ
๋ ์ด๋ค A๋ผ๋ ์ฟผ๋ฆฌ๊ฐ ์๋๋ฐ ์ด A์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ธฐ ์ ์ ์ฌ์ ์ ์๋ฃ๋์ด์ผ ํ๋ B ์ฟผ๋ฆฌ๊ฐ ์๋๋ฐ, ์ด๋ฌํ B์ฟผ๋ฆฌ์ ์์กดํ๋ A์ฟผ๋ฆฌ๋ฅผ ์ข ์ ์ฟผ๋ฆฌ๋ผ๊ณ ํ๋ค.- react-query์์๋ ์ฟผ๋ฆฌ๋ฅผ ์คํํ ์ค๋น๊ฐ ๋์๋ค๋ ๊ฒ์ ์๋ ค์ฃผ๋
enabled
์ต์ ์ ํตํด ์ข ์ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.
const DependantQueriesPage = ({ email }: Props) => {
// ์ฌ์ ์ ์๋ฃ๋์ด์ผํ ์ฟผ๋ฆฌ
const { data: user } = useQuery(['user', email], () =>
fetchUserByEmail(email)
);
const channelId = user?.data.channelId;
// user ์ฟผ๋ฆฌ์ ์ข
์ ์ฟผ๋ฆฌ
const { data } = useQuery(
['courses', channelId],
() => fetchCoursesByChannelId(channelId),
{ enabled: !!channelId }
);
- useQueryClient๋
QueryClient
์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค. QueryClient
๋ ์บ์์ ์ํธ์์ฉํ๋ค.- QueryClient๋ ๋ค์ ๋ฌธ์์์ ์์ธํ๊ฒ ๋ค๋ฃฌ๋ค
import { useQueryClient } from "react-query";
const queryClient = useQueryClient();
- ์ฟผ๋ฆฌ์ ๋ํ
์ด๊ธฐ ๋ฐ์ดํฐ
๊ฐ ํ์ํ๊ธฐ ์ ์ ์บ์์ ์ ๊ณตํ๋ ๋ฐฉ๋ฒ์ด ์๋ค. - initialData ์ต์ ์ ํตํด์ ์ฟผ๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ์ฑ์ฐ๋ ๋ฐ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ์ด๊ธฐ ๋ก๋ ์ํ๋ ๊ฑด๋๋ธ ์๋ ์๋ค.
const useSuperHeroData = (heroId: string) => {
const queryClient = useQueryClient();
return useQuery(['super-hero', heroId], fetchSuperHero, {
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 };
return undefined;
},
});
};
- ์ฐธ๊ณ ๋ก ์ ์์ ์์
queryClient.getQueryData
๋ฉ์๋๋ ๊ธฐ์กด ์ฟผ๋ฆฌ์์บ์ฑ ๋ ๋ฐ์ดํฐ
๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ธฐ ํจ์์ด๋ค. ์ฟผ๋ฆฌ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉดundefined
๋ฅผ ๋ฐํํ๋ค.
- prefetch๋ ๋ง ๊ทธ๋๋ก ๋ฏธ๋ฆฌ fetchํด์ค๊ฒ ๋ค๋ ์๋ฏธ์ด๋ค.
- ๋น๋๊ธฐ ์์ฒญ์ ๋ฐ์ดํฐ ์์ด ํด ์๋ก ๋ฐ์์ค๋ ์๋๊ฐ ๋๋ฆฌ๊ณ , ์๊ฐ์ด ์ค๋๊ฑธ๋ฆฐ๋ค. ์ฌ์ฉ์ ๊ฒฝํ์ ์ํด ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ์์์ ์บ์ฑํด๋์ผ๋ฉด? ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ธฐ์ ์ ์ฌ์ฉ์๊ฐ ์บ์ฑ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณผ ์ ์์ด
UX์ ์ข์ ์ํฅ
์ ์ค ์ ์๋ค.- ์๋ฅผ ๋ค์ด ํ์ด์ง๋ค์ด์ ์ ๊ตฌํํ๋ค๊ณ ๊ฐ์ ํ๋ฉด, ํ์ด์ง1์์ ํ์ด์ง2๋ก ์ด๋ํ์ ๋ ํ์ด์ง3์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ์๋๋ ๊ฒ์ด๋ค!
- react query์์๋
queryClient.prefetchQuery
์ ํตํด์ prefetch ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
const prefetchNextPosts = async (nextPage: number) => {
const queryClient = useQueryClient();
// ํด๋น ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๋ ์ผ๋ฐ ์ฟผ๋ฆฌ๋ค์ฒ๋ผ ์บ์ฑ๋๋ค.
await queryClient.prefetchQuery(
["posts", nextPage],
() => fetchPosts(nextPage),
{ ...options }
);
};
// ๋จ์ ์
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
- ์ฐธ๊ณ ๋ก prefetchQuery๋ฅผ ํตํด ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ์ ๋ํ ๋ฐ์ดํฐ๊ฐ
์ด๋ฏธ ์บ์ฑ๋์ด ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ค.
- Infinite Queries(๋ฌดํ ์ฟผ๋ฆฌ)๋
๋ฌดํ ์คํฌ๋กค
์ด๋load more(๋ ๋ณด๊ธฐ)
๊ณผ ๊ฐ์ด ํน์ ์กฐ๊ฑด์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ๋ฉด ์ ์ฉํ๋ค. - react-query๋ ์ด๋ฌํ ๋ฌดํ ์ฟผ๋ฆฌ๋ฅผ ์ง์ํ๊ธฐ ์ํด useQuery์ ์ ์ฉํ ๋ฒ์ ์ธ
useInfiniteQuery
์ ์ง์ํ๋ค.
import { useInfiniteQuery } from "react-query";
// import { useInfiniteQuery } from '@tanstack/react-query' v4
const fetchColors = async ({ pageParam = 1 }) => {
return await axios.get(
`http://localhost:4000/colors?_limit=2&_page=${pageParam}`
);
};
const InfiniteQueries = () => {
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery(["colors"], fetchColors, {
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
๋ฑ์ด ์ถ๊ฐ์ ์ผ๋ก ์๋ค.- fetchNextPage:
๋ค์ ํ์ด์ง
๋ฅผ fetch ํ ์ ์๋ค. - fetchPreviousPage:
์ด์ ํ์ด์ง
๋ฅผ fetch ํ ์ ์๋ค. - isFetchingNextPage:
fetchNextPage
๋ฉ์๋๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค. - isFetchingPreviousPage:
fetchPreviousPage
๋ฉ์๋๊ฐ ์ด์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ true์ด๋ค. - hasNextPage: ๊ฐ์ ธ์ฌ ์ ์๋
๋ค์ ํ์ด์ง
๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค. - hasPreviousPage: ๊ฐ์ ธ์ฌ ์ ์๋
์ด์ ํ์ด์ง
๊ฐ ์์ ๊ฒฝ์ฐ true์ด๋ค.
- fetchNextPage:
์ต์
pageParam
์ด๋ผ๋ ํ๋กํผํฐ๊ฐ ์กด์ฌํ๋ฉฐ,queryFn
์ ํ ๋นํด์ค์ผ ํ๋ค. ์ด๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ด๊ธฐ ํ์ด์ง ๊ฐ์ ์ค์ ํด์ค์ผํ๋ค.getNextPageParam
์ ์ด์ฉํด์ ํ์ด์ง๋ฅผ ์ฆ๊ฐ์ํฌ ์ ์๋ค.- getNextPageParam์ ์ฒซ ๋ฒ์งธ ์ธ์
lastPage
๋ fetch ํด์จ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฐ์ ธ์จ ํ์ด์ง ๋ชฉ๋ก์ด๋ค. - ๋ ๋ฒ์งธ ์ธ์
allPages
๋ ํ์ฌ๊น์ง ๊ฐ์ ธ์จ ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ์ด๋ค.
- getNextPageParam์ ์ฒซ ๋ฒ์งธ ์ธ์
getPreviousPageParam
๋ ์กด์ฌํ๋ฉฐ,getNextPageParam
์ ๋ฐ๋์ ์์ฑ์ ๊ฐ๊ณ ์๋ค.
pageParam
queryFn
์ ๋๊ฒจ์ฃผ๋ pageParam๊ฐ ๋จ์ํ ๋ค์ page์ ๊ฐ๋ง์ ๊ด๋ฆฌํ ์ ์๋ ๊ฒ์ ์๋๋ค.- pageParam ๊ฐ์
getNextPageParam
์์ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝ์์ผ์ค ์ ์๋ค. - ๋ฌด์จ ๋ง์ธ์ง ์์๋ฅผ ๋ณด๋ฉด ์ดํด๊ฐ ์ฝ๋ค. ๐ ์๋์ ๊ฐ์ด getNextPageParam์์ ๋ฐํ ๋ฐ์ดํฐ๊ฐ ๋จ์ํ ๋ค์ ํ์ด์ง ๊ฐ์ด ์๋ ๊ฐ์ฒด๋ก ๋ฐํํ๋ค๊ณ ํด๋ณด์.
const { data } = useInfiniteQuery(["colors"], fetchColors, {
getNextPageParam: (lastPage, allPages) => {
return (
allPages.length < 4 && {
page: allPages.length + 1,
etc: "hi",
};
)
},
});
- ๊ทธ๋ฌ๋ฉด
queryFn
์ ๋ฃ์ pageParams์์ getNextPageParam์์ ๋ฐํํ ๊ฐ์ฒด๋ฅผ ๋ฐ์์ฌ ์ ์๋ค.
/**
* pageParam
* { page, etc }
*/
const fetchColors = async ({ pageParam }) => {
const { page = 1, etc } = pageParam;
return await axios.get(`http://localhost:4000/colors?_limit=2&_page=${page}`);
};
- ์ฆ, getNextPageParam์ ๋ฐํ ๊ฐ์ด pageParams๋ก ๋ค์ด๊ฐ๊ธฐ ๋๋ฌธ์ pageParams๋ฅผ ์ํ๋ ํํ๋ก ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด getNextPageParam์ ๋ฐํ ๊ฐ์ ์ค์ ํ๋ฉด ๋๋ค.
refetchPage
- ์ ์ฒด ํ์ด์ง ์ค ์ผ๋ถ๋ง ์ง์ refetchํ๊ณ ์ถ์ ๋์๋,
useInfiniteQuery
๊ฐ ๋ฐํํ๋ refetch ํจ์์refetchPage
๋ฅผ ๋๊ฒจ์ฃผ๋ฉด ๋๋ค. refetchPage
๋ ๊ฐ ํ์ด์ง์ ๋ํด ์คํ๋๋ฉฐ, ์ด ํจ์๊ฐ true๋ฅผ ๋ฐํํ๋ ํ์ด์ง๋ง refetch๊ฐ ๋๋ค.
const { refetch } = useInfiniteQuery(["colors"], fetchColors, {
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 && allPages.length + 1;
},
});
// ์ฒซ๋ฒ์งธ ํ์ด์ง๋ง refetch ํฉ๋๋ค.
refetch({ refetchPage: (page, index) => index === 0 });
- useMutation v4
- react-query์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ Get ํ ๋๋ useQuery๋ฅผ ์ฌ์ฉํ๋ค.
- ๋ง์ฝ ์๋ฒ์ data๋ฅผ post, patch, put, delete์ ๊ฐ์ด ์์ ํ๊ณ ์ ํ๋ค๋ฉด ์ด๋๋ useMutation์ ์ด์ฉํ๋ค.
- ์์ฝํ์๋ฉด
R(read)๋ useQuery
,CUD(Create, Update, Delete)๋ useMutation
์ ์ฌ์ฉํ๋ค.
const CreateTodo = () => {
const mutation = useMutation(createTodo, {
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutation.mutate({ title });
};
return <>...</>;
};
- 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 mutate, mutateAsync ๋ธ๋ก๊ทธ ์ฐธ๊ณ
- ์ฟผ๋ฆฌ๋ฅผ
์๋์ผ๋ก ์ทจ์
ํ๊ณ ์ถ์ ์๋ ์๋ค.- ์๋ฅผ ๋ค์ด ์์ฒญ์ ์๋ฃํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ ์ฌ์ฉ์๊ฐ ์ทจ์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ฒญ์ ์ค์งํ๋๋ก ํ์ฉํ ์ ์๋ค.
- ๋๋, ์์ง HTTP ์์ฒญ์ด ๋๋์ง ์์์ ๋, ํ์ด์ง๋ฅผ ๋ฒ์ด๋ ๊ฒฝ์ฐ์๋ ์ค๊ฐ์ ์ทจ์ํด์ ๋ถ ํ์ํ ๋คํธ์ํฌ ๋ฆฌ์์ค๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
- ์ด๋ ๊ฒ ํ๋ ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ทจ์ํ๊ณ ์ด์ ์ํ๋ก ๋๋๋ฆฌ๊ธฐ ์ํด
queryClient.cancelQueries(queryKey)
๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ๋ํ react-query๋ ์ฟผ๋ฆฌ ์ทจ์๋ฟ๋ง์๋๋ผ queryFn์ Promise๋ ์ทจ์ํ๋ค. - query-cancellation
const query = useQuery(["super-heroes"], {
/* ...options */
});
const queryClient = useQueryClient();
const onCancelQuery = (e) => {
e.preventDefault();
queryClient.cancelQueries(["super-heroes"]);
};
return <button onClick={onCancelQuery}>Cancel</button>;
- invalidateQueries์ ํ๋ฉด์ ์ต์ ์ํ๋ก ์ ์งํ๋ ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ด๋ค.
- ์๋ฅผ ๋ค๋ฉด, ๊ฒ์ํ ๋ชฉ๋ก์์ ์ด๋ค ๊ฒ์๊ธ์
์์ฑ(Post)
ํ๊ฑฐ๋ ๊ฒ์๊ธ์์ ๊ฑฐ(Delete)
ํ์ ๋ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ํ ๋ชฉ๋ก์ ์ค์๊ฐ์ผ๋ก ์ต์ ํ ํด์ผํ ๋๊ฐ ์๋ค. - ํ์ง๋ง ์ด๋,
query Key
๊ฐ ๋ณํ์ง ์์ผ๋ฏ๋ก ๊ฐ์ ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํํ๊ณ ์ต์ ํ๋ฅผ ์งํํด์ผ ํ๋๋ฐ, ์ด๋ฐ ๊ฒฝ์ฐ์invalidateQueries()
๋ฉ์๋๋ฅผ ์ด์ฉํ ์ ์๋ค. - ์ฆ, query๊ฐ ์ค๋๋์๋ค๋ ๊ฒ์ ํ๋จํ๊ณ ๋ค์
refetch
๋ฅผ ํ ๋ ์ฌ์ฉํ๋ค!
import { useMutation, useQuery, useQueryClient } from "react-query";
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation(addSuperHero, {
onSuccess(data) {
queryClient.invalidateQueries(["super-heroes"]); // ์ด key์ ํด๋นํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฌดํจํ!
console.log(data);
},
onError(err) {
console.log(err);
},
});
};
- ๋ง์ฝ ๋ฌดํจํ ํ๋ ค๋ ํค๊ฐ ์ฌ๋ฌ ๊ฐ๋ผ๋ฉด ์๋ ์์ ์ ๊ฐ์ด ๋ค์๊ณผ ๊ฐ์ด ๋ฐฐ์ด๋ก ๋ณด๋ด์ฃผ๋ฉด ๋๋ค.
queryClient.invalidateQueries(["super-heroes", "posts", "comment"]);
- ์์
enabled/refetch
์์๋ ์ธ๊ธํ์ง๋งenabled: false
์ต์ ์ ์ฃผ๋ฉดqueryClient
๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ ์คinvalidateQueries
์refetchQueries
๋ฅผ ๋ฌด์ํ๋ค. - ์์ธํ ๋ด์ฉ์ queryClient.invalidateQueries ์ ๋ฆฌ๋ฅผ ์ฐธ๊ณ ํ์.
- ๋ฐ๋ก ์์์
queryClient.invalidateQueries
๋ฅผ ์ด์ฉํด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ดค๋๋ฐ queryClient.setQueryData๋ฅผ ์ด์ฉํด์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์ ๋ฐ์ดํธํ ์ ์๋ค. queryClient.setQueryData
๋ ์ฟผ๋ฆฌ์ ์บ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ์ ๋ฐ์ดํธํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋๋๊ธฐ ํจ์
์ด๋ค.
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation(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(addSuperHero, {
async onMutate(newHero) {
// ๋๊ด์ ์
๋ฐ์ดํธ๋ฅผ ๋ฎ์ด์ฐ์ง ์๊ธฐ ์ํด ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ญ์ ํ๋ค.
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);
},
// ์ค๋ฅ ๋๋ ์ฑ๊ณต ํ์๋ ํญ์ ๋ฆฌํ๋ ์ฌ
onSettled() {
queryClient.invalidateQueries(["super-heroes"]);
},
});
};
- ์ฐธ๊ณ ๋ก ์ ์์ ์์
cancelQueries
๋ ์ฟผ๋ฆฌ๋ฅผ์๋์ผ๋ก ์ทจ์
์ํฌ ์ ์๋ค. ์ทจ์์ํฌ query์ queryKey๋ฅผ cancelQueries์ ์ธ์๋ก ๋ณด๋ด ์คํ์ํจ๋ค. - ์๋ฅผ ๋ค์ด, ์์ฒญ์ ์๋ฃํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ, ์ฌ์ฉ์๊ฐ ์ทจ์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ฒญ์ ์ค์งํ๋ ๊ฒฝ์ฐ ์ด์ฉํ ์ ์๋ค.
- useQueryErrorResetBoundary v4
- 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
- ์ค์น ํ์ ์๋์ ๊ฐ์ QueryErrorBoundary๋ผ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๊ณ , ๊ทธ ๋ด๋ถ์
useQueryErrorResetBoundary
ํ ์ ํธ์ถํดreset
ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค. - ์๋ ์ฝ๋ ๋ด์ฉ์ ๋จ์ํ๋ค.
- Error๊ฐ ๋ฐ์ํ๋ฉด ErrorBoundary์
fallbackRender
prop์ผ๋ก ๋๊ธด ๋ด์ฉ์ด ๋ ๋๋ง ๋๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด children ๋ด์ฉ์ด ๋ ๋๋ง ๋๋ค. - ๋ํ, fallbackRender์ ๋ฃ์ด์ฃผ๋ ์ฝ๋ฐฑ ํจ์ ๋งค๊ฐ ๋ณ์๋ก
resetErrorBoundary
๋ฅผ ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ํตํด ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ, ์ด๋ฅผ ํตํด ๋ชจ๋ ์ฟผ๋ฆฌ ์๋ฌ๋ฅผ์ด๊ธฐํ
ํ ์ ์๋ค. ์๋ ์ฝ๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ button์ ํด๋ฆญํ๋ฉด ์๋ฌ๋ฅผ ์ด๊ธฐํํ๊ฒ๋ ์์ฑํ๋ค.
- Error๊ฐ ๋ฐ์ํ๋ฉด ErrorBoundary์
import { useQueryErrorResetBoundary } from "react-query"; // (*)
import { ErrorBoundary } from "react-error-boundary"; // (*)
interface Props {
children: React.ReactNode;
}
const QueryErrorBoundary = ({ children, fallback }: 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 ์ต์
์๋ค
{ useErrorBoundary: true }
๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ErrorBoundary
์ปดํฌ๋ํธ๊ฐ ๊ฐ์งํ ์ ์๋ค.
import { QueryClientProvider, QueryClient } from "react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
useErrorBoundary: 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: {
useErrorBoundary: true,
suspense: true, // (*) ์ฌ๊ธฐ์๋ ๊ธ๋ก๋ฒ๋ก ์
ํ
ํ์ง๋ง ๊ฐ๋ณ ์ฟผ๋ฆฌ๋ก ์
ํ
๊ฐ๋ฅ
},
},
});
function App() {
return (
<QueryErrorBoundary>
<Suspense fallback={<Loader />}>{/* ํ์ ์ปดํฌ๋ํธ๋ค */}</Suspense>
</QueryErrorBoundary>;
);
}
- ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ฐ๋ฆฌ๋ ์๋ฒ ์ํ๊ฐ ๋ก๋ฉ์ผ๋ Loader ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๊ฒ ๋ค!๋ผ๊ณ ์ดํดํ ์ ์๋ค.
- Suspense์ปดํฌ๋ํธ ๋ด๋ถ์์ ์ด๋ค ๋ก์ง์ด ๋์ํ๋์ง ์ฐ๋ฆฌ๋ ์ ๊ฒฝ์ฐ์ง ์์๋๋๋ค. ์ด์ฒ๋ผ
๋ด๋ถ ๋ณต์ก์ฑ์ ์ถ์ํ
ํ๋๊ฒ ๋ฐ๋ก์ ์ธํ ์ปดํฌ๋ํธ
์ด๋ค. - ๋ํ, ์์ ๊ฐ์ด react-query์ ๊ฒฐํฉํ Suspense๋ ์๋์ ๊ฐ์ ๊ณผ์ ์ผ๋ก ๋์์ํ๋ค. ์ฐธ๊ณ ํด๋ณด์.
1. Suspense mount
2. MainComponent mount
3. MainComponent์์ useQuery์ ์๋ api Call
4. MainComponent unmount, fallback UI์ธ Loader mount
5. api Call Success์ผ ๊ฒฝ์ฐ, useQuery์ ์๋ onSuccess ๋์
6. onSuccess ์๋ฃ ์ดํ Loader unmount
7. MainComponent mount
- ์ฑ ์ ์ฒด์์ ๋์ผํ ์ฟผ๋ฆฌ ํจ์๋ฅผ ๊ณต์ ํ๊ณ ,
queryKey
๋ฅผ ์ฌ์ฉํด ๊ฐ์ ธ์์ผ ํ ๋ฐ์ดํฐ๋ฅผ ์๋ณํ๊ณ ์ถ๋ค๋ฉดQueryClient
์queryFn
์ต์ ์ ํตํด Default Query Function์ ์ง์ ํด ์ค ์ ์๋ค. - Default Query Function v4
// ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์
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(["super-hero", heroId]);
};
// ๋ค์ ํํ ๋ถ๊ฐ๋ฅ
const useSuperHeroData = (heroId: string) => {
return useQuery(["super-hero", heroId], () => getSuperHero(heroId));
};
- useQuery์ ์ฒซ ๋ฒ์งธ ์ธ์๋ก
queryKey
๋ง ๋ฃ์ด์ฃผ๋ฉด ๋ ๋ฒ์งธ ์ธ์์ ๋ค์ด๊ฐqueryFn
์ ์๋์ผ๋ก ์ค์ ๋ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ค์ด๊ฐ๋ค. - ์ผ๋ฐ์ ์ผ๋ก
useQuery
๋ฅผ ์ฌ์ฉํ ๋์ ๋ฌ๋ฆฌqueryFn
์ ์ง์ ํ์ง ์๊ธฐ์ ์ฟผ๋ฆฌ ํจ์์ ์ง์ ์ธ์๋ฅผ ๋ฃ์ด์ฃผ๋ ํํ์ ์ฌ์ฉ์ ๋ถ๊ฐ๋ฅํ๋ค.
- React Query๋ TypeScript์
์ ๋ค๋ฆญ(Generics)
์ ๋ง์ด ์ฌ์ฉํ๋ค. ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๊ณ API๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ ์ ํ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค. - ๊ณต์ ๋ฌธ์์์๋ ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ๊ทธ๋ค์ง ๊ด๋ฒ์ํ๊ฒ ๋ค๋ฃจ์ง๋ ์๊ณ , useQuery๋ฅผ ํธ์ถํ ๋ ๊ธฐ๋ํ๋ ์ ๋ค๋ฆญ์ ๋ช ์์ ์ผ๋ก ์ง์ ํ๋๋ก ์๋ ค์ค๋ค.
ํ์ฌ 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 = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
// useQuery ํ์
์ ์ฉ ์์
const { data } = useQuery<
SuperHeros,
AxiosError,
SuperHeroName[],
[string, number]
>(["super-heros", id], getSuperHero, {
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
- data:
SuperHeroName[]
- error:
AxiosError<any, any>
- select:
(data: SuperHeros): SuperHeroName[]
useMutation๋ useQuery์ ๋์ผํ๊ฒ ํ์ฌ 4๊ฐ์ด๋ฉฐ, ๋ค์๊ณผ ๊ฐ๋ค.
- TData: useMutaion์ ๋๊ฒจ์ค mutation function์
์คํ ๊ฒฐ๊ณผ
์ ํ์ ์ ์ง์ ํ๋ ์ ๋ค๋ฆญ ํ์ ์ด๋ค.- data์ ํ์ ๊ณผ onSuccess(1๋ฒ์งธ ์ธ์)์ ์ธ์์ ํ์ ์ผ๋ก ํ์ฉ๋๋ค.
- TError: useMutaion์ ๋๊ฒจ์ค 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 useMutaion<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown
>
// useMutation ํ์
์ ์ฉ ์์
const { mutate } = useMutation<Todo, AxiosError, number, number>(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)
,