๐ก 07. [์ค๋ฌด] TanStack Query ํ๋ก ํธ์๋ ํ ์คํธ(Testing) ์ ๋ต
๐ ๊ฐ์
React Query ํ๊ฒฝ์ ํนํ๋ ํ๋ก ํธ์๋ ํ ์คํธ ๊ตฌ์ถ๋ฒ, QueryClient ๊ฒฉ๋ฆฌ ํจํด, ๊ทธ๋ฆฌ๊ณ MSW๋ฅผ ๊ฒฐํฉํ ์๋ฒฝํ ๋คํธ์ํฌ ๋ชจํน(Mocking) ์ ๋ต์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "ํ ์คํธ๋ง ์ง๋ฉด ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๊ฐ ๋ค ๋ฑ์ด๋ด์!"
- ๐ค ์ ์์์ผ ํ๋๊ฐ: ํ ์คํธ ๊ฒฉ๋ฆฌ์ ๋คํธ์ํฌ ๊ฐ๋ก์ฑ๊ธฐ
- 1. ์ ์ญ ์ค์ผ ์ฐจ๋จ:
QueryClient์ธ์คํด์ค ๊ฒฉ๋ฆฌ ํ ํฌ๋ - 2. API ๋ชจํน(Mocking)์ ์ ์ : MSW (Mock Service Worker)
- ๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
"์์ฒ ๋, ๋จ์ ํ ์คํธ ๋๋ ธ๋๋ 'Network Error: Fetch is not defined' ๋ง 50์ค ๋จ๋๋ฐ, Jest ์์์ ์ง์ง ๋ฐฐํฌ ์๋ฒ๋ฅผ ์ฐ๋ฅด๊ณ ๊ณ์ จ๋์?"
โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "ํ ์คํธ๋ง ์ง๋ฉด ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๊ฐ ๋ค ๋ฑ์ด๋ด์!"
(๊ธ์์ผ ์ ๋ , ๋ง์ง๋ง ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ฑ๊ธฐ๋ ค๋ค ์ค์ดํ๋ ์์ฒ )
๐ฃ ์์ฒ : ์๋ ๋ฆฌ๋ ๋! ์ด์ ์๋ฒฝํ๊ฒ ์ปดํฌ๋ํธ ๋ค ์งฐ์ผ๋๊น ๋ ๋ ํ๊ฒ Vitest(๋๋ Jest) ๋ React Testing Library(RTL) ์ผ์ ํ
์คํธ ๋ ์ด์ด ์น์ผ๋ ค๊ณ ํ๊ฑฐ๋ ์?
๊ทผ๋ฐ ํ
์คํธ(npm run test) ์คํํ์๋ง์... ์๋ฌ๊ฐ ์ฐ๋๋ฏธ์ฒ๋ผ ์์์ ธ์!
No QueryClient set, use QueryClientProvider ์๋ฌ๋ถํฐ ์์ํด์,
๊ธฐ๊ป Provider ๊ฐ์ธ์คฌ๋๋ ์ด๋ฒ์ ๋ฐฑ์๋ ์๋ฒ๊ฐ ์ ์ผ์ ธ ์๋ค๊ณ Network Timeout ๋๊ณ ์,
๊ทธ๊ฑฐ ๊ณ ์ณค๋๋ "1๋ฒ ํ
์คํธ์์ ๋ง๋ ์บ์ ๋ฐ์ดํฐ" ๊ฐ "2๋ฒ ํ
์คํธ" ์ ์ค์ผ๋ผ์ ํ
์คํธ๊ฐ ํํ์ผ๋ก ๊นจ์ ธ์. ๋ฆฌ์กํธ ์ฟผ๋ฆฌ ํ
์คํธ๋ ๋ถ๊ฐ๋ฅํ ์์ญ์ธ๊ฐ์?
๐ฆ ์ํธ: ์์ฒ ๋, ํ๋ก ํธ์๋ ํ
์คํธ ์ธ๊ณ์ ๋ฐ์ ๋ค์ด์
จ๊ตฐ์. useQuery๊ฐ ์ปดํฌ๋ํธ ์์ ์งฑ๋ฐํ ์๋๋ฐ ๊ทธ๊ฑธ ๋ก์ปฌ ๋
ธ๋(Node.js / JSDOM) ํ๊ฒฝ์์ ๊ทธ๋ฅ ๋ ๋๋งํ๋ ค๊ณ ํ๋๊น ์จ๊ฐ ์ค๋ฅ๋ ์ค๋ฅ๋ ๋ค ํฐ์ง๋ ๊ฒ๋๋ค.
QueryClientProvider์์ด๋ ์ฟผ๋ฆฌ๊ฐ ๋์ํ์ง ์์ต๋๋ค. (Provider ๋ํ ์์์ ๋ชจ๋ฅด์ฌ)- ํ ์คํธ ์ค์ ์ง์ง ๋ฐฑ์๋ ์๋ฒ์ ํธ๋ํฝ์ ์ฐ๋ฅด๋ฉด ์ ๋ฉ๋๋ค. ๋น ๋ฅด๊ณ ๊ฒฉ๋ฆฌ๋ ๋ง(Mock) ์ ์จ์ผ์ฃ . MSW(Mock Service Worker)๋ฅผ ๋์ ํด์ผ ํฉ๋๋ค (TkDodo #5).
5๋ ์ฐจ ์ค๋ฌด ํ๋ก ํธ์๋ ํ ์คํธ์ ์ฑ๋ฐฐ, "MSW + 1 ํ ์คํธ 1 QueryClient ํฉํ ๋ฆฌ" ์ธํ ์ ํํค์ณ ๋ด ์๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ: ํ ์คํธ ๊ฒฉ๋ฆฌ์ ๋คํธ์ํฌ ๊ฐ๋ก์ฑ๊ธฐ
๋ฒํผ ๋๋ฅด๋ฉด ์ํ ๋ณํ๋ ๊ฑด ๋๊ตฌ๋ ํ
์คํธํ ์ ์์ต๋๋ค.
ํ์ง๋ง "๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ฐฑ์๋ API๋ฅผ ์ฝํด์ ํ๋กํ์ ๊ทธ๋ ค์ฃผ๋ ์ปดํฌ๋ํธ"๋ฅผ ๋ธ๋ผ์ฐ์ ๋ ์๋ CI/CD ์ฝ์์ฐฝ(Terminal) ์์์ ํ
์คํธํ๋ ค๋ฉด ๋์ด๋๊ฐ ๊ทน์์ผ๋ก ๋๋๋ค.
React Query ํ๊ฒฝ์ ํ ์คํธ๊ฐ ์ด๋ ค์ด ์ด์ ๋ ๊ทธ ๊ฐ๋ ฅํ ์ฅ์ ๋๋ฌธ์ ๋๋ค.
- ์บ์ฑ(Caching)์ ๋ถ์์ฉ: ํ
์คํธ A์์ ์์ฑ๋ ์บ์ ๋ฐ์ดํฐ๊ฐ, ํ
์คํธ B๋ฅผ ์คํํ ๋๊น์ง ๋ฉ๋ชจ๋ฆฌ(
QueryCache) ์ ์ํด(Stale) ์์ง ์์์ ๋จ์๋ฒ๋ฆฝ๋๋ค. (์ํ ์ค์ผ) - ๋น๋๊ธฐ ์ฌ์๋(Retry): ํ ์คํธ ์๋ฒ๊ฐ ์์ด์ ์๋ฌ๊ฐ ๋๋ฉด, ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๊ฐ ๋ฌด๋ ค 3๋ฒ์ด๋ ์ฌ์๋๋ฅผ ํ๋๋ผ ํ ์คํธ ํ๋๋น ์๊ฐ์ด ๋ฌดํ์ ์ง์ฐ๋ฉ๋๋ค.
์ด๋ฅผ ์ ์ํ๋ ์ํคํ ์ฒ ์ต์ ์ ๋ชจ๋ฅด๋ฉด, ํ์ E2E(Cypress) ํ ์คํธ์๋ง ์์กดํ๋ฉฐ ์์ฑํ ์ฑ์ ๋ง๋ค๊ฒ ๋ฉ๋๋ค.
1. ์ ์ญ ์ค์ผ ์ฐจ๋จ: QueryClient ์ธ์คํด์ค ๊ฒฉ๋ฆฌ ํ
ํฌ๋
์ค๋ฌด์์ ๊ฐ์ฅ ๋ง์ด ์ ์ง๋ฅด๋ ์ค์๊ฐ ๋ฐ๋ก "์ด์ ์ฝ๋์ ์๋ QueryClient๋ฅผ ๊ฐ์ ธ์์ ํ
์คํธ์ ๋ฃ๋ ์ง" ์
๋๋ค.
์ด์ ์ฝ๋๋ ์ ์ญ ๋ณ์ ํ๋๋ฅผ ํ์ ์ธ๊ถ๋จน๊ฒ ์บ์ฑ(staleTime: 5๋ถ) ์ธํ
์ ํด๋๊ธฐ ๋๋ฌธ์, ํ
์คํธ ๊ฐ ๋ฐ์ดํฐ ์นจ๋ฒ ๋ฒ๊ทธ๊ฐ ์๋ ฌํฉ๋๋ค.
โ
ํด๊ฒฐ์ฑ
: ํ
์คํธ ์ฝ๋๊ฐ ๋ ๋๋ง๋ค(๋งค it ์ด๋ test ๋ธ๋ก๋ง๋ค) ์์ฅ QueryClient๋ฅผ ์ฐ์ด๋ด๋ ๋ํผ(Wrapper) ํจ์๋ฅผ ๋ง๋ ๋ค!
// ๐ tests/utils.tsx (ํ
์คํธ ํฌํผ ํ์ผ)
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// ๐ ์ปค์คํ
๋ ๋๋ฌ ์์ฑ!
export function renderWithQueryClient(ui: React.ReactElement) {
// ํต์ฌ: ํจ์๊ฐ ๋ถ๋ฆด ๋๋ง๋ค ์์ ํ ์๋กญ๊ณ ํ
๋น ์ฟผ๋ฆฌ ํด๋ผ์ด์ธํธ๋ฅผ ์์ฑํฉ๋๋ค.
const testQueryClient = new QueryClient({
defaultOptions: {
queries: {
// ํ
์คํธ ํ๊ฒฝ์ฉ ์ธํ
์ ๊ตญ๋ฃฐ: ์ฌ์๋ ๋ฐ์
์ ๊บผ๋ฒ๋ฆฐ๋ค! ์๋ฌ ๋๋ฉด ์ฆ์ ์คํจ ๋จ๊ฒ.
retry: false,
// ์ต์
: ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ์ ์ํด ์บ์ ์ฒญ์๋ฅผ ์ด์คํผ๋๋ก ๋๋ฆฝ๋๋ค.
gcTime: 0,
},
},
});
return {
...render(
<QueryClientProvider client={testQueryClient}>
{ui}
</QueryClientProvider>
),
// ๋์ค์ ํ
์คํธ ์์ชฝ์์ ์บ์๋ฅผ ์ฟ๋ด์ผ ํ ๋๋ฅผ ์ํด ์ธ์คํด์ค ๋ฐํ
testQueryClient,
};
}์ด์ ์ค์ ํ
์คํธ(*.test.tsx) ํ์ผ์์๋ ์ด๋ ๊ฒ ์๋๋ค.
test('์ ์ ํ๋กํ์ด ์ฑ๊ณต์ ์ผ๋ก ๋ ๋๋ง ๋๋๊ฐ?', async () => {
// ๋ฐฉ๊ธ ๋ง๋ ํฌํผ๋ก ๋ธ๋ผ์ฐ์ ์์ด ์ปดํฌ๋ํธ ๊ฐ์ ๋ง์ดํธ ์์ฑ!
renderWithQueryClient(<UserProfile id={1} />);
// ์์๊ฐ ํ๋ฐ๋ฐ- ๋ํ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค.
expect(await screen.findByText('์์ฒ ')).toBeInTheDocument();
});2. API ๋ชจํน(Mocking)์ ์ ์ : MSW (Mock Service Worker)
์, ๊ทธ๋ผ ์ UserProfile ์ปดํฌ๋ํธ ์์ ์๋ useQuery(fetchUser) ๋ ๋๋์ฒด ์ด๋๋ก ๋คํธ์ํฌ ์์ฒญ์ ๋ ๋ฆด๊น์? JSDOM ํ๊ฒฝ์์๋ ๊ฐ๊ณ ์ถ์ด๋ ๊ฐ ์๋ฒ๊ฐ ์์ต๋๋ค!
์ด๋ด ๋ jest.mock() ์ผ๋ก axios ํต์ ๋ถ๋ฅผ ๊ฑฐ๋ถํ๊ฒ ๊ฐ์ง ๊ฐ์ฒด๋ก ๊ฐ์๋ผ์ฐ๋ ๊ฑด ๋๋ฌด ๊ณ ํต์ค๋ฝ์ต๋๋ค.
TanStack Query ์ ์์๊ฐ ์ ์ผ ๋งน๋ ฌํ ์ถ์ฒํ๋ ๋ฐฉ๋ฒ์, ์์ ์๋ฒ(๋คํธ์ํฌ ๊ณ์ธต) ์์ฒด๋ฅผ ๊ฐ๋ก์ฑ ๋ฒ๋ฆฌ๋ MSW ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ถ์ด๋ ๊ฒ๋๋ค.
MSW๋ฅผ ์ธํ
ํ๋ฉด fetch ๋ axios ์ฝ๋๋ฅผ 1๋ฐ์ดํธ๋ ์์ ํ์ง ์์ ์ฑ, ๋ด๊ฐ ์ ๋คํธ์ํฌ ์์ฒญ์ด ์ง์ง ๋ฐฑ์๋๊ฐ ์๋ ๋ด ๋ก์ปฌ ํ
์คํธ๊ฐ๋ก์ฑ๊ธฐ ๋ง์ผ๋ก ์์ ๋นจ๋ ค ๋ค์ด์ต๋๋ค.
// ๐ tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// "GET /api/users/1" ์์ฒญ์ด ๋ ๋ผ์ค๋ฉด, ๊ฐ์ง ๊ฐ์ฒด๋ฅผ 200 OK๋ก ๋ฐํํด๋!
http.get('https://my-api.com/users/1', () => {
return HttpResponse.json({ name: '์์ฒ ', role: 'junior' })
}),
// ์ต์
: ์๋ฌ ์ํฉ ์ปดํฌ๋ํธ(Error Boundary) ํ
์คํธ๋ฅผ ์ํด 500 ์๋ฌ๋ฅผ ์๋ ๋ชฉ์
๋ ์์ฑ
http.get('https://my-api.com/users/999', () => {
return new HttpResponse(null, { status: 500 })
})
]์ด์ ํ
์คํธ ์ฝ๋ ํ๊ฒฝ ์
์
ํ์ผ(setupTests.ts) ์์ MSW ์๋ฒ๋ฅผ ์ผ์ค๋๋ค.
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
const server = setupServer(...handlers)
// ํ
์คํธ ์์ ์ ๋ชฉ์
์๋ฒ ON
beforeAll(() => server.listen())
// ํ
์คํธ๋ง๋ค ํธ๋ค๋ฌ ๋ฆฌ์
(๋
๋ฆฝ์ฑ)
afterEach(() => server.resetHandlers())
// ๋ชฝ๋
๋๋๋ฉด ์๋ฒ ๋
afterAll(() => server.close())์ด ๋ ๊ฐ์ง(QueryClient ๊ฒฉ๋ฆฌ + MSW ๊ฒฐํฉ)๊ฐ ๋ญ์น๋ ์๊ฐ, ๋น์ ์ ์ด๋ ํ ๋ณต์กํ useInfiniteQuery ๋ ๊ผฌ์ด๊ณ ๊ผฌ์ธ ์บ์ฑ ์ํฉ๋ ๋ฐฑ์๋ ์ฐ๊ฒฐ ์์ด ์๋ฒฝํ๊ฒ ํ๋ก ํธ์๋ ๋จ๋
์ผ๋ก ๋ธ๋๋ฐ์ค ํ
์คํธํ ์ ์๋ ์ ์ ๊ถ๋ฅ์ ๊ฐ์ง๊ฒ ๋ฉ๋๋ค!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์... ์ค๋ MSW ์ธํ
ํด์ ๋คํธ์ํฌ ๊ฐ๋ก์ฑ๋ ๊ฑฐ ๋์ผ๋ก ๋ณด๋๋ฐ ์๋ฆ ๋์๋ค.
๋ ๊ทธ๋์ API ์ฝํ๋ ํจ์ ์์ฒด๋ฅผ jest.spyOn().mockResolvedValue(...) ์ด๋ฐ ์์ผ๋ก ์ด๊ฑฐ์ง๋ก ๊น์์ ๋ง๋ค์์๋๋ฐ, ๊ทธ๋ฌ๋๊น ์ปดํฌ๋ํธ ์์ชฝ React Query์ ์บ์ ๋ก์ง์ด ์ง์ง๋ก ์ด๋ป๊ฒ ์์ง์ด๋์ง ํ
์คํธ๋ฅผ ๋ชป ํ๋ ๊ฑฐ์์ด.
๐ก ์ค๋์ ๊ตํ: "React Query ์ปดํฌ๋ํธ๋ฅผ ํ ์คํธํ ๋๋ 1) ๋ฌด์กฐ๊ฑด
retry: false์ต์ ์ ์ค ๊ป๋ฐ๊ธฐ 1ํ์ฉ ๋น QueryClient๋ฅผ ์ฐ์ด๋ด์ ์ฃผ์ (Provider)ํด ์ค ๊ฒ! 2) ๋คํธ์ํฌ ์์ฒญ์ ์ ๋mockํจ์๋ก ์น์ง ๋ง๊ณ MSW๋ฅผ ๋ถ์ฌ ์๋ฒ์ ํ์๋ฅผ ์ฌํํ ๊ฒ!"
์ด๋ก์จ ๋ฆฌ๋ ๋์ด ์ง์ฃผ์ 17๊ฐ์ ํ๋ํ TanStack Query ๊ธฐ์ด/์ฌํ ๊ฐ์ด๋๋ผ์ธ ์ฌ์ ์ด ๋ชฝ๋
๋ค ๋๋ฌ๋ค ใ
ใ
!
์ด์ ์ฒ์ ํ๋ก ํธ ์
์ฌํ์ ๋ "queryKey๊ฐ ๋ญ์ฃ ๋จน๋ ๊ฑด๊ฐ์" ํ๋ ๋ ์์ฒ ๋, ์ด๋ ๊ฐ์ ์ ์ญ ์ํคํ
์ฒ ์บ์ ๊ธ๊ณ ๋ฐ๊ณ ๋ค๋๋ 5๋
์ฐจ๋ผ๊ณ ๋ช
ํจ ๋ด๋ฐ ์ ์๊ฒ ์ง?! ์๊ณ ํ๋ค ๋ ์์ !! ๐ฅ (์ฃผ๋ง์ ํน ์์ผ์ง...)
๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
Q. ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ์์ฒ ์ด๋ "์๋ฌ ๋ฌ์ ๋ Fallback ์ปดํฌ๋ํธ๋ฅผ ์ ๊ทธ๋ ค์ฃผ๋๊ฐ?"(์๋ฌ ๋ฐ์ด๋๋ฆฌ) ํ
์คํธ๋ฅผ ๋๋ฆฌ๊ณ ์์๋๋ฐ, Jest ์ฝ์ ์ฐฝ์ ๋นจ๊ฐ์ Timeout ์๋ฌ๊ฐ ๋จ๋ฉด์ ๋ฌด๋ ค ํ
์คํธ ํ๋๊ฐ ์๋ฃ๋๋ ๋ฐ 3์ด ์ด์์ด ๋๋ ์ด๋๋ ํ์์ ๊ฒช์์ต๋๋ค. MSW๋ ์ ์์ ์ผ๋ก 500 ์๋ฌ๋ฅผ ๋ฑ๊ฒ ์ง ๋จ๋๋ฐ๋ ๋ง์ด์ฃ . ์ด ์ฌ๋ก์ฐ ํ
์คํธ์ ๊ทผ๋ณธ์ ์ธ ์์ธ์ ๋ฌด์์
๋๊น?
โ
์ ๋ต: ํ
์คํธ๋ฅผ ๋ฎ์ด์ฃผ๋ QueryClientProvider ๊ป๋ฐ๊ธฐ์ ํ
์คํธ ์ ์ฉ ์ต์
์ธ retry: false (์ฌ์๋ ๋) ์ธํ
์ ๊น๋นกํ๊ธฐ ๋๋ฌธ์
๋๋ค.
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช : TkDodo ์ํฐํด #5์ ํต์ฌ! React Query์ ์์ฒญ๋ ๊ฐ์ ์ธ "API ์์ฒญ ์คํจ ์ ๊ธฐ๋ณธ 3๋ฒ ์ฌ์๋ (Exponential Backoff ์ง์ฐ์๊ฐ ํฌํจ)" ๊ธฐ๋ฅ์, ์ผ์ํ๊ฒ๋ ํ ์คํธ ํ๊ฒฝ(Node.js)์์๋ ๋๊ฐ์ด ๋์ํฉ๋๋ค. MSW๊ฐ 500 ์๋ฌ๋ฅผ ์ฃผ๋ฉด, React Query๋ "์ด? ์๋ฌ ๋ฌ์ด? ์ ๊น ๊ธฐ๋ค๋ ค๋ด. 1์ด ๋ค์ ๋ค์ ์ฐ๋ฌ๋ณผ๊ฒ... ๋ ์๋ฌ ๋๋ค? 2์ด ๋ค์ ๋ค์..." ํ๋ฉด์ ํ ์คํธ ์ฐฝ์ ํ์ผ์์ด ๋ฉ์ถฐ ์ธ์๋๋ค. ๊ฒฐ๊ตญ 3๋ฒ ๋ฐ์ ์ ๋ค ๋๋ด๊ณ ์์ ํ ์ฅ๋ ฌํ ์ ์ฌํ์ ๋์์ผ ์ปดํฌ๋ํธ ์ชฝ์ ์๋ฌ ๊ฒฐ๊ณผ๊ฐ์ ๋๊ฒจ์ฃผ๋ฉฐ 3์ด๋ฅผ ํ๋นํ๊ฒ ๋๋ ๊ฒ์ ๋๋ค.
- ์ค๋ต ํผ๋๋ฐฑ: "์์ฒ ๋, ํ
์คํธ ํ๋ ์์ํฌ๊ฐ ๋๋ฆฐ ๊ฒ ์๋๋๋ค! React Query๊ฐ ์ด๋ ค๋ณด๋ ค๊ณ ๋ค์์ ์์๋ ํ๋ฆฌ๋ฉด์ ์ฌ์๋(Retry) ํ๊ณ ์๋ ๊ฑฐ์์. ํ
์คํธ ๋ ๋ ํฌํผ์ ์ฟผ๋ฆฌ ์ต์
์ ๋ฌด์กฐ๊ฑด
retry: falseํ ์ค์ ์ํํด์ 1ํธ๋ง์ ์ฆ์ฌ(?)ํ๋๋ก ๋ง๋์ ์ผ ์จ ๋งํ๋ ํ ์คํธ ๋ณ๋ชฉ ์ง์ฐ์ ๋ฐ์ด ๋ผ ์ ์์ต๋๋ค." - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ํ
์คํธ ํ๊ฒฝ์ QueryClient๋ ๋ฌด์กฐ๊ฑด ์๋น ์๋ ๋
ธ๊ฐ๋ค ๋ฐฉ์ง,
retry: false!