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 (
-
- );
+ 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}
-
-
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