diff --git a/src/content/docs/kr/docs/guides/tech/with-react-query.mdx b/src/content/docs/kr/docs/guides/tech/with-react-query.mdx index eac07b71ef..02f1e79fab 100644 --- a/src/content/docs/kr/docs/guides/tech/with-react-query.mdx +++ b/src/content/docs/kr/docs/guides/tech/with-react-query.mdx @@ -1,453 +1,551 @@ --- -title: Usage with React Query +title: Usage with TanStack Query sidebar: - order: 10 + order: 2 --- -import { FileTree, Aside } from '@astrojs/starlight/components'; - -## Query Key 배치 문제 - -### entities별 분리 - -각 요청이 특정 entity에 대응한다면, -`src/entities/{entity}/api` 폴더에 관련 코드를 모아두세요: - - -- src/ - - app/ - - ... - - pages/ - - ... - - entities/ - - \{entity\}/ - - ... - - api/ - - `{entity}.query` Query-factory where are the keys and functions - - `get-{entity}` Entity getter function - - `create-{entity}` Entity creation function - - `update-{entity}` Entity update function - - `delete-{entity}` Entity delete function - - ... - - features/ - - ... - - widgets/ - - ... - - shared/ - - ... - - -entities 간에 데이터를 참조해야 하면 [공용 Public API][public-api-for-cross-imports]를 사용하거나, -아래 예시처럼 `shared/api/queries`에 모아두는 방법도 있습니다. - -### 대안 — shared에 모아두기 - -entity별 분리가 어려울 때는 예시 처럼 `src/shared/api/queries`에 Query Factory를 정의하세요. - - -- src/ - - ... - - shared/ - - api/ - - ... - - queries Query-factories - - document.ts - - background-jobs.ts - - ... - - index.ts - - -이후 `@/shared/api/index.ts`에서 다음과 같이 사용합니다: - -```ts title="@/shared/api/index.ts" -export { documentQueries } from "./queries/document"; +import { Tabs, TabItem, FileTree, Aside } from '@astrojs/starlight/components'; + +## query key를 어디에 둘 것인가 \{#where-to-store-keys\} + +query key는 보통 query factory와 API 호출 함수를 같은 곳에 관리합니다. +어느 layer에 두는지는 프로젝트 구성에 따라 달라집니다. + + + + 프로젝트 전반의 API를 `shared/api` 한곳에 모아 관리하려는 경우에 사용합니다. + query factory는 `shared/api/queries` 아래에 두고, `shared/api/index.ts`의 public API로 노출합니다. + + + - src/ + - app/ + - pages/ + - widgets/ + - features/ + - entities/ + - shared/ + - api/ + - queries/ Query factories + - example.ts + - another-example.ts + + + ```ts title="src/shared/api/index.ts" + export { exampleQueries } from './queries/example'; + ``` + + + endpoint 수가 많아지면 `shared/api/queries` 한 폴더에 모두 모으는 방식은 관리하기 어려워질 수 있습니다. + 이 경우에는 controller 단위로 폴더를 나누고, 각 controller마다 `index.ts`로 public API를 따로 둡니다. + + + - src/ + - app/ + - pages/ + - widgets/ + - features/ + - entities/ + - shared/ + - api/ + - example/ + - index.ts + - example.query.ts Query factory with keys and functions for the example controller + - get-example.ts + - create-example.ts + - update-example.ts + - delete-example.ts + - another-example/ + - index.ts + - another-example.query.ts Query factory with keys and functions for the another-example controller + - get-another-example.ts + - create-another-example.ts + - update-another-example.ts + - delete-another-example.ts + + + ```ts title="src/shared/api/example/index.ts" + export { exampleQueries } from "./example.query"; + ``` + + + 프로젝트가 이미 entity 단위로 나뉘어 있고, 각 요청이 하나의 entity에 대응한다면 entity 단위로 나누는 방식이 가장 자연스럽습니다. + 해당 entity의 `api` segment에 query factory와 실제 API 호출 함수를 함께 둡니다. + + + - src/ + - app/ + - pages/ + - widgets/ + - features/ + - entities/ + - example/ + - api/ + - example.query.ts Query factory with keys and functions + - get-example.ts + - create-example.ts + - update-example.ts + - delete-example.ts + - shared/ + + + 한 entity가 다른 entity를 참조한다면(예: `Country` entity가 `City` entity 목록을 필드로 갖는 경우) [public API를 이용한 cross-import][public-api-for-cross-imports]를 사용합니다. + + + +## mutation은 어디에 둘 것인가 \{#where-to-store-mutations\} + +mutation은 query와 같은 파일에 함께 두지 않는 것을 권장합니다. 배치 방식은 여러 가지가 있습니다. + + + + mutation은 저장, 삭제, 수정처럼 특정 사용자 동작 뒤에 실행되는 경우가 많고, 이후의 캐시 갱신이나 UI 처리도 함께 달라지기 쉽습니다. + 화면 흐름과 밀접하게 연결되는 mutation은 사용하는 위치와 가까운 `api` segment에 custom hook 형태로 둡니다. + + ```tsx title="src/pages/example/api/use-update-example.ts" + export const useUpdateExample = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, newTitle }) => { + const { data } = await apiClient.patch(`/posts/${ id }`, { title: newTitle }); + return data; + }, + onSuccess: newPost => { + queryClient.setQueryData(postsQueries.ids(id), newPost); + }, + }); + }; + ``` + + + mutation 함수는 `shared`나 `entities`에 두고, 컴포넌트에서는 `useMutation`을 직접 구성하면서 `mutationFn`으로 연결합니다. + mutation 로직의 재사용 단위와 hook을 구성하는 위치를 나누는 방식입니다. + + ```tsx title="src/pages/example/ui/example.tsx" + export const Example = () => { + const [title, setTitle] = useState(''); + + const { mutate, isPending } = useMutation({ + mutationFn: mutations.createExample, + }); + + const handleChange = ({ target: { value } }: ChangeEvent) => { + setTitle(value); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + mutate({ title, userId: DEFAULT_USER_ID }); + }; + + return ( +
+ + +
+ ); + }; + ``` +
+
+ +## query factory로 요청 구성하기 \{#organizing-queries\} + +### Query factory \{#query-factory\} + +Query factory는 query key를 생성하는 함수를 모아 둔 객체입니다. + +```ts +const keyFactory = { + all: () => ["entity"], + lists: () => [...keyFactory.all(), "list"], +}; ``` -## Mutation 배치 문제 + - return useMutation({ - mutationFn: ({ id, newTitle }) => - apiClient - .patch(`/posts/${id}`, { title: newTitle }) - .then((data) => console.log(data)), +### 1. query factory 만들기 \{#creating-query-factory\} - onSuccess: (newPost) => { - queryClient.setQueryData(postsQueries.ids(id), newPost); - }, - }); -}; -``` +```tsx title="src/shared/api/post/post.queries.ts" +import { queryOptions } from '@tanstack/react-query'; +import { getPosts } from './get-posts'; +import { getDetailPost, type DetailPostQuery } from './get-detail-post'; -### entities 또는 shared에 함수만 정의하고, 컴포넌트에서 `useMutation` 사용 +export const POST_QUERIES = { + all: () => ['posts'], -```tsx -const { mutateAsync, isPending } = useMutation({ - mutationFn: postApi.createPost, -}); -``` + lists: () => [...POST_QUERIES.all(), 'list'], + list: (page: number, limit: number) => queryOptions({ + queryKey: [...POST_QUERIES.lists(), page, limit], + queryFn: () => getPosts(page, limit), + placeholderData: prev => prev, + }), -```tsx title="@/pages/post-create/ui/post-create-page.tsx" -export const CreatePost = () => { - const { classes } = useStyles(); - const [title, setTitle] = useState(""); - - const { mutate, isPending } = useMutation({ - mutationFn: postApi.createPost, - }); - - const handleChange = (e: ChangeEvent) => - setTitle(e.target.value); - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - mutate({ title, userId: DEFAULT_USER_ID }); - }; - - return ( -
- - - Create - - - ); + details: () => [...POST_QUERIES.all(), 'detail'], + detail: (query?: DetailPostQuery) => queryOptions({ + queryKey: [...POST_QUERIES.details(), query?.id], + queryFn: () => getDetailPost({ id: query?.id }), + }), }; ``` -## Request 조직화 +### 2. 프로젝트 코드에서 query factory 사용하기 \{#using-query-factory\} -### Query Factory +```tsx title="src/pages/post/ui/post.tsx" +import { useParams } from 'react-router'; +import { postApi } from '@/shared/api/post'; +import { useQuery } from '@tanstack/react-query'; -Query Factory는 Query Key와 Query Function을 한곳에서 관리합니다. -다음 예시처럼 객체로 정의하세요: +interface Params { + postId: string; +} -```ts -const keyFactory = { - all: () => ["entity"], - lists: () => [...postQueries.all(), "list"], -}; -``` +export const Post = () => { + const { postId } = useParams(); + const { + data: post, + error, + isLoading, + isError + } = useQuery(postApi.POST_QUERIES.detail({ id: parseInt(postId ?? '', 10) })); + + if (isLoading) { + return ( +
Loading...
+ ); + } - +## 무한 스크롤 \{#infinite-scroll\} -### Query Factory 생성 예시 +무한 스크롤이나 '더보기' 버튼 형태의 UI는 `useInfiniteQuery`로 구성합니다. 앞에서 정의한 query factory 패턴은 `infiniteQueryOptions`로도 그대로 확장할 수 있습니다. -```tsx title="@/entities/post/api/post.queries.ts" -import { keepPreviousData, queryOptions } from "@tanstack/react-query"; -import { getPosts } from "./get-posts"; -import { getDetailPost } from "./get-detail-post"; -import { PostDetailQuery } from "./query/post.query"; +### 1. `infiniteQueryOptions`로 구성한 query factory \{#infinite-query-factory\} -export const postQueries = { - all: () => ["posts"], +```tsx title="src/shared/api/post/post.queries.ts" +import { infiniteQueryOptions } from '@tanstack/react-query'; +import { getPosts } from './get-posts'; - lists: () => [...postQueries.all(), "list"], - list: (page: number, limit: number) => - queryOptions({ - queryKey: [...postQueries.lists(), page, limit], - queryFn: () => getPosts(page, limit), - placeholderData: keepPreviousData, - }), - - details: () => [...postQueries.all(), "detail"], - detail: (query?: PostDetailQuery) => - queryOptions({ - queryKey: [...postQueries.details(), query?.id], - queryFn: () => getDetailPost({ id: query?.id }), - staleTime: 5000, +export const POST_QUERIES = { + all: () => ['posts'], + lists: () => [...POST_QUERIES.all(), 'list'], + infinite: (limit: number) => infiniteQueryOptions({ + queryKey: [...POST_QUERIES.lists(), 'infinite', limit], + queryFn: ({ pageParam }) => getPosts(pageParam, limit), + initialPageParam: 0, + getNextPageParam: (lastPage) => + lastPage.skip + lastPage.limit < lastPage.total + ? lastPage.skip / lastPage.limit + 1 + : undefined, }), }; ``` -### 애플리케이션 코드에서의 Query Factory 사용 예시 -```tsx -import { useParams } from "react-router-dom"; -import { postApi } from "@/entities/post"; -import { useQuery } from "@tanstack/react-query"; +### 2. 컴포넌트에서 사용 \{#infinite-query-usage\} -type Params = { - postId: string; -}; +```tsx title="src/pages/post-feed/ui/post-feed.tsx" +import { useInfiniteQuery } from '@tanstack/react-query'; +import { postApi } from '@/shared/api/post'; -export const PostPage = () => { - const { postId } = useParams(); - const id = parseInt(postId || ""); - const { - data: post, - error, - isLoading, - isError, - } = useQuery(postApi.postQueries.detail({ id })); - - if (isLoading) { - return
Loading...
; - } - - if (isError || !post) { - return <>{error?.message}; - } - - return ( -
-

Post id: {post.id}

-
-

{post.title}

-
-

{post.body}

-
-
-
Owner: {post.userId}
-
- ); +export const PostFeed = () => { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery(postApi.POST_QUERIES.infinite(10)); + + const posts = data?.pages.flatMap((page) => page.posts) ?? []; + + return ( + <> + + { hasNextPage && ( + + ) } + + ); }; ``` -### Query Factory 사용의 장점 -- **Request 구조화**: 모든 API 호출을 Factory 패턴으로 통합 관리해, 코드 가독성과 유지보수성을 개선합니다. -- **Query와 Key에 대한 편리한 접근**: 다양한 Query Type과 해당 Key를 메서드로 제공해, 언제든 간편하게 참조할 수 있습니다. -- **Query Invalidation 용이성**: Query Key를 직접 수정하지 않고도 원하는 Query를 손쉽게 무효화할 수 있습니다. - -## Pagination +## Suspense 모드 \{#suspense-mode\} -Pagination을 적용해 `getPosts` 함수로 게시물 목록을 가져오는 과정을 설명합니다. +`useSuspenseQuery`를 사용하면 로딩 상태를 React Suspense로 처리할 수 있습니다. 컴포넌트에서 `isLoading`을 직접 확인할 필요가 없어지고, 로딩 표시는 상위 `Suspense` 경계가 담당합니다. -### `getPosts` 함수 생성하기 +### 1. query factory는 그대로 재사용합니다 \{#suspense-query-factory\} -`src/pages/post-feed/api/get-posts.ts` 파일에 다음과 같이 정의됩니다. +`useSuspenseQuery`는 `queryOptions` 기반 구성과 호환되므로, 앞에서 정의한 query factory를 그대로 재사용할 수 있습니다. -```tsx title="@/pages/post-feed/api/get-posts.ts" -import { apiClient } from "@/shared/api/base"; +### 2. 컴포넌트에서 사용 \{#suspense-usage\} -import { PostWithPaginationDto } from "./dto/post-with-pagination.dto"; -import { PostQuery } from "./query/post.query"; -import { mapPost } from "./mapper/map-post"; -import { PostWithPagination } from "../model/post-with-pagination"; +```tsx title="src/pages/post/ui/post.tsx" +import { useSuspenseQuery } from '@tanstack/react-query'; +import { postApi } from '@/shared/api/post'; -const calculatePostPage = (totalCount: number, limit: number) => - Math.floor(totalCount / limit); +interface PostProps { + id: number; +} -export const getPosts = async ( - page: number, - limit: number, -): Promise => { - const skip = page * limit; - const query: PostQuery = { skip, limit }; - const result = await apiClient.get("/posts", query); +// isLoading is no longer needed — the component only renders when data is ready +export const Post = ({ id }: PostProps) => { + const { data: post } = useSuspenseQuery(postApi.POST_QUERIES.detail({ id })); - return { - posts: result.posts.map((post) => mapPost(post)), - limit: result.limit, - skip: result.skip, - total: result.total, - totalPages: calculatePostPage(result.total, limit), - }; + return ( +
+

{ post.title }

+

{ post.body }

+
+ ); }; ``` -### 페이지네이션용 Query Factory 정의 +### 3. `app` layer에 Suspense 경계 두기 \{#suspense-app-wrapper\} -페이지 번호(`page`)와 한도(`limit`)를 인자로 받아 게시물 목록을 가져오는 Query를 설정합니다. +Suspense 경계는 `app` layer의 provider로 두고, 앱 전역 또는 필요한 라우트 단위에서 감싸 사용할 수 있습니다. -```tsx -import { keepPreviousData, queryOptions } from "@tanstack/react-query"; -import { getPosts } from "./get-posts"; +```tsx title="src/app/providers/suspense-provider.tsx" +import { Suspense, type ReactNode } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; -export const postQueries = { - all: () => ["posts"], - lists: () => [...postQueries.all(), "list"], - list: (page: number, limit: number) => - queryOptions({ - queryKey: [...postQueries.lists(), page, limit], - queryFn: () => getPosts(page, limit), - placeholderData: keepPreviousData, - }), -}; +interface SuspenseProviderProps { + children: ReactNode; +} + +export const SuspenseProvider = ({ children }: SuspenseProviderProps) => ( + Something went wrong }> + Loading... }> + { children } + + +); ``` +## `useMutationState` \{#use-mutation-state\} -### 애플리케이션 코드 사용 예시 - -페이지네이션된 게시물을 화면에 렌더링하는 방법입니다. -`useQuery` 훅으로 `postQueries.list`를 호출하고, `Pagination` 컴포넌트와 연동하세요. - -```tsx title="@/pages/home/ui/index.tsx" -export const HomePage = () => { - const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; - const [page, setPage] = usePageParam(DEFAULT_PAGE); - const { data, isFetching, isLoading } = useQuery( - postApi.postQueries.list(page, itemsOnScreen), - ); - return ( - <> - setPage(page)} - page={page} - count={data?.totalPages} - variant="outlined" - color="primary" - /> - - - ); -}; -``` +`useMutationState`를 사용하면 이 상태를 다른 컴포넌트에서도 확인할 수 있습니다. 예를 들어 페이지 내부의 폼에서 mutation이 진행되는 동안, 폼과 별도의 전역 헤더에서 진행 상태를 표시할 때 유용합니다. mutation key는 query factory와 비슷한 방식으로 한곳에서 관리합니다. - +```ts title="src/shared/api/post/post.queries.ts" +export const POST_MUTATIONS = { + updateTitle: () => ['post', 'update-title'], + create: () => ['post', 'create'], +}; +``` -## Query 관리를 위한 QueryProvider +### 2. `mutationKey`로 mutation 식별하기 \{#naming-mutations\} -QueryProvider 구성 방법을 안내합니다. +```tsx title="src/features/update-post/api/use-update-post-title.ts" +import { POST_MUTATIONS } from '@/shared/api/post'; + +interface UpdatePostTitle { + id: number; + newTitle: string; +} -### `QueryProvider` 생성하기 +export const useUpdatePostTitle = () => + useMutation({ + mutationKey: POST_MUTATIONS.updateTitle(), + mutationFn: ({ id, newTitle }: UpdatePostTitle) => + apiClient.patch(`/posts/${id}`, { title: newTitle }), + }); +``` -`src/app/providers/query-provider.tsx`에 QueryProvider 컴포넌트를 정의합니다. +### 3. 다른 컴포넌트에서 mutation 상태 읽기 \{#reading-mutation-state\} -```tsx title="@/app/providers/query-provider.tsx" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { ReactNode } from "react"; +```tsx title="src/widgets/save-indicator/ui/save-indicator.tsx" +import { useMutationState } from '@tanstack/react-query'; +import { POST_MUTATIONS } from '@/shared/api/post'; -type Props = { - children: ReactNode; - client: QueryClient; -}; +export const SaveIndicator = () => { + const isPending = useMutationState({ + filters: { mutationKey: POST_MUTATIONS.updateTitle(), status: 'pending' }, + select: mutation => mutation.state.status, + }).length > 0; -export const QueryProvider = ({ client, children }: Props) => { - return ( - - {children} - - - ); + return isPending && ( + Saving... + ); }; ``` -### 2. `QueryClient` 생성 +## `QueryProvider` 구성하기 \{#query-provider\} -React Query의 캐싱과 기본 옵션을 설정할 `QueryClient` 인스턴스를 만듭니다. -아래 코드를 `@/shared/api/query-client.ts`에 정의하세요. +`QueryProvider`는 `QueryClient` 설정을 앱 전역에 적용하는 위치입니다. query와 mutation의 기본 옵션, 그리고 `QueryCache`와 `MutationCache`의 공통 에러 처리도 이곳에서 함께 구성합니다. -```tsx title="@/shared/api/query-client.ts" -import { QueryClient } from "@tanstack/react-query"; +```tsx title="src/app/providers/query-provider.tsx" +import { type ReactNode } from 'react'; +import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { toast } from 'sonner'; + +interface QueryProviderProps { + children: ReactNode; + client: QueryClient; +} -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, - gcTime: 5 * 60 * 1000, +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: error => { + toast.error(error.message); + }, + }), + mutationCache: new MutationCache({ + onError: error => { + toast.error(error.message); + }, + }), + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, + gcTime: 5 * 60 * 1000, + }, }, - }, }); -``` - -## 코드 자동 생성 -API 코드 자동 생성 도구를 사용하면 반복 작업을 줄일 수 있습니다. -다만, 직접 작성하는 방식보다 유연성이 떨어질 수 있습니다. -Swagger 파일이 잘 정의되어 있다면 자동 생성 도구를 활용해 코드를 생성하세요. -생성된 코드는 `@/shared/api` 디렉토리에 배치해 일관되게 관리합니다. - -## React Query를 조직화하기 위한 추가 조언 +export const QueryProvider = ({ client, children }: QueryProviderProps) => { + return ( + + { children } + + + ); +}; +``` -### API Client +## 코드 생성 \{#code-generation\} -`shared/api`에 커스텀 APIClient 클래스를 정의하면 다음 기능을 한곳에서 일괄 설정할 수 있습니다: +API 코드를 자동으로 생성하는 도구를 사용할 수도 있지만, 위에서 설명한 수동 구성만큼 세밀하게 맞추기는 어렵습니다. Swagger 파일이 잘 정리되어 있고 생성 도구를 중심으로 API 계층을 운영한다면, 생성된 코드를 `@/shared/api`에 모아 두는 방식을 고려할 수 있습니다. -- response, request 로깅 및 에러 처리를 일관되게 적용 -- 공통 헤더와 인증 설정, 데이터 직렬화 방식을 한곳에서 설정 -- API endpoint 변경이나 옵션 업데이트를 단일 수정 지점에서 반영 +## API 구성에 대한 추가 권장사항 \{#api-client\} +`shared` layer에 API client 클래스를 두면, 프로젝트 전반의 API 호출 방식을 공통으로 관리할 수 있습니다. 로깅, 헤더, 데이터 형식(JSON, XML 등) 같은 설정도 이 위치에 함께 둘 수 있습니다. 호출 규칙이 바뀌거나 공통 설정을 추가해야 할 때는 이 위치만 수정하면 됩니다. -```tsx title="@/shared/api/api-client.ts" +```tsx title="src/shared/api/api-client.ts" import { API_URL } from "@/shared/config"; export class ApiClient { - private baseUrl: string; + #baseUrl: string; - constructor(url: string) { - this.baseUrl = url; - } - - async handleResponse(response: Response): Promise { - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); + constructor(url: string) { + this.#baseUrl = url; } - try { - return await response.json(); - } catch (error) { - throw new Error("Error parsing JSON response"); + async handleResponse(response: Response): Promise { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${ response.status }`); + } + + try { + return await response.json(); + } catch (error) { + throw new Error("Error parsing JSON response"); + } } - } - - public async get( - endpoint: string, - queryParams?: Record, - ): Promise { - const url = new URL(endpoint, this.baseUrl); - - if (queryParams) { - Object.entries(queryParams).forEach(([key, value]) => { - url.searchParams.append(key, value.toString()); - }); + + public async get(endpoint: string, queryParams?: Record): Promise { + const url = new URL(endpoint, this.#baseUrl); + + if (queryParams) { + Object.entries(queryParams).forEach(([key, value]) => { + url.searchParams.append(key, value.toString()); + }); + } + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + return this.handleResponse(response); } - const response = await fetch(url.toString(), { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - return this.handleResponse(response); - } - - public async post>( - endpoint: string, - body: TData, - ): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); + public async post>(endpoint: string, body: TData): Promise { + const response = await fetch(`${ this.#baseUrl }${ endpoint }`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); - return this.handleResponse(response); - } + return this.handleResponse(response); + } } export const apiClient = new ApiClient(API_URL); ``` -## 참고 자료 \{#see-also} +## 참고 자료 \{#see-also\} - [(GitHub) 예제 프로젝트](https://github.com/ruslan4432013/fsd-react-query-example) - [(CodeSandbox) 예제 프로젝트](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main) -- [Query Options 가이드](https://tkdodo.eu/blog/the-query-options-api) +- [query factory 관련 글](https://tkdodo.eu/blog/the-query-options-api) [public-api-for-cross-imports]: /kr/docs/reference/public-api#public-api-for-cross-imports