Repository: dnd-side-project/dnd-12th-9-frontend
Pull request #209: CHORE : TanStack Query prefetch await 하지 않도록 수정
Author: login: DongjaJ
Labels: name: chore
Created: 2025-05-16T11:42:50Z | Merged: 2025-05-22T04:44:54Z

<!-- PR 제목입니다. -->
<!-- 아래 중 타입에 맞는 PR 제목으로 복사/붙여넣기 해 주세요. -->
<!-- [FEAT] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [BUGFIX] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [REFACTOR] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [CHORE] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [TEST] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [PERFORMANCE] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [SET] 작업_내용을_한_줄로_요약해서_작성 -->
<!-- [DOCS] 작업_내용을_한_줄로_요약해서_작성 -->

## 관련 이슈

close #142
related #112

## ✨ 작업 내용

<!-- 이번 PR에 담긴 작업 내용을 작성합니다 -->
### TanStack Query 및 서버 컴포넌트 설정 수정
- tkdodo의 블로그를 보고 20초 정도의 기본 stale time을 설정했습니다.
- 서버 컴포넌트에서 queryClient에 참조할때 getQueryClient만 사용하도록 수정했습니다.
- QueryClient가 pending중인 쿼리도 하이드레이션하도록 했고 prefetch 할때 await으로 기다리지 않도록 했습니다.

### 미들웨어
미들웨어에서 prefetch, 라우트 핸들러는 검사하지 않도록 했습니다. => 그래도 로그인 버그는 아직 안 고쳐졌습니다. 원인을 파악 못하겠네요.

## 🙏 기타 참고 사항
- stale time에는 정답이 없는 것 같아요... 다른 의견 있으면 주시면 감사하겠습니다.
- fetch query도 preFetchQuery로 바꿔도 될 것 같긴한데... 일단 지금은 사이드 이펙트 생길까봐 남겨뒀습니다.

<!-- 없다면 적지 않으셔도 됩니다. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **신규 기능**
  - 글로벌 에러 경계 컴포넌트가 추가되어 오류 발생 시 "문제가 발생했습니다!" 메시지와 재시도 버튼을 제공합니다.

- **버그 수정**
  - 쿠키 키를 상수로 통일하여 로그인 및 인증 관련 쿠키 처리의 일관성이 향상되었습니다.
  - 미들웨어가 닉네임 쿠키를 올바르게 참조하도록 수정되었습니다.

- **리팩터**
  - 데이터 프리패치 및 쿼리 클라이언트 생성 방식이 개선되어, 페이지 렌더링 속도와 안정성이 향상되었습니다.
  - 미들웨어의 조건문이 간소화되고, 적용 경로가 명확하게 지정되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #142: [FEAT] React-query 설정 고도화
<!-- 티켓 이슈 제목 템플릿입니다. -->
<!-- [FEAT] 이_티켓에서_작업할_내용을_한_줄로_요약해서_작성 -->

## TODO

<!-- 이번 티켓에서 작업할 내용(투두 리스트)을 작성해 주세요. -->
- 지금 데이터 패칭 대부분은 서버에서 prefetch -> 하위에서 suspenseQuery로 프리패치한 데이터를 넘겨받는 형태에요
따라서 서버에서 데이터를 prefetch하는 동안 하위 클라이언트 컴포넌트에서 렌더링이 블로킹되는 문제가 있습니다. [공식 문서](https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr#streaming-with-server-components)를 참고해서 개선해보려고 합니다
- 서버에서 에러를 잡으려면 prefetchQuery => fetchQuery로 바꿔야 할 것 같습니다.
`If you do use it, a good rule of thumb is to avoid queryClient.fetchQuery unless you need to catch errors.`
- 서버에서도 [단일 쿼리 클라이언트](https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr#alternative-use-a-single-queryclient-for-prefetching)를 사용하는 것을 고려해봐야 할 것 같아요. 교체했을 때 성능이 안좋아질수도, 좋아질 수도 있어서 측정해봐야 할 것 같습니다.

## 기타 참고사항
[TanStack query 공식 문서](https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr)
[레퍼런스 블로그 1](https://medium.com/wraffle-tech/server-component%EC%97%90%EC%84%9C-suspense-query%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-next-js-streaming-%EC%84%9C%EB%B2%84-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-3a5bfd1efb55)
[레퍼런스 블로그 2](https://velog.io/@stakbucks/React-Query%EC%99%80-streaming-%ED%95%98%EA%B8%B0?source=post_page-----3a5bfd1efb55---------------------------------------)
[마이리얼트립](https://medium.com/myrealtrip-product/%EB%A7%88%EC%9D%B4%EB%A6%AC%EC%96%BC%ED%8A%B8%EB%A6%BD-ssr-%EC%B5%9C%EC%A0%81%ED%99%94-a73815c6cdfc)

<!-- 없다면 적지 않으셔도 됩니다. -->

Diff:
diff --git a/apps/web/app/_api/ClientQueryProvider.tsx b/apps/web/app/_api/ClientQueryProvider.tsx
index 370132d9..d592df90 100644
--- a/apps/web/app/_api/ClientQueryProvider.tsx
+++ b/apps/web/app/_api/ClientQueryProvider.tsx
@@ -1,30 +1,8 @@
 'use client';
 
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { QueryClientProvider } from '@tanstack/react-query';
 
-function makeQueryClient() {
-  return new QueryClient({
-    defaultOptions: {
-      queries: {
-        retry: 3,
-        refetchOnWindowFocus: false,
-      },
-    },
-  });
-}
-
-let clientQueryClient: QueryClient | undefined = undefined;
-
-function getQueryClient() {
-  if (typeof window === 'undefined') {
-    // Server: always make a new query client
-    return makeQueryClient();
-  } else {
-    // Browser: make a new query client if we don't already have one
-    if (!clientQueryClient) clientQueryClient = makeQueryClient();
-    return clientQueryClient;
-  }
-}
+import { getQueryClient } from 'app/_util/queryClient';
 
 export const ClientQueryProvider = ({ children }: React.PropsWithChildren) => {
   const queryClient = getQueryClient();
diff --git a/apps/web/app/_util/queryClient.ts b/apps/web/app/_util/queryClient.ts
index 1d2fc338..bff4c7bc 100644
--- a/apps/web/app/_util/queryClient.ts
+++ b/apps/web/app/_util/queryClient.ts
@@ -1,5 +1,43 @@
-import { QueryClient } from '@tanstack/react-query';
+import { isServer, QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query';
 
-export const getQueryClient = () => {
-  return new QueryClient();
-};
+// https://tkdodo.eu/blog/react-query-as-a-state-manager#customize-staletime
+const STALE_TIME_MS = 20 * 1000; // 20 seconds
+
+function makeQueryClient() {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        staleTime: STALE_TIME_MS,
+      },
+      dehydrate: {
+        // include pending queries in dehydration
+        shouldDehydrateQuery: (query) =>
+          defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
+        shouldRedactErrors: () => {
+          // We should not catch Next.js server errors
+          // as that's how Next.js detects dynamic pages
+          // so we cannot redact them.
+          // Next.js also automatically redacts errors for us
+          // with better digests.
+          return false;
+        },
+      },
+    },
+  });
+}
+
+let browserQueryClient: QueryClient | undefined = undefined;
+
+export function getQueryClient() {
+  if (isServer) {
+    // Server: always make a new query client
+    return makeQueryClient();
+  } else {
+    // Browser: make a new query client if we don't already have one
+    // This is very important, so we don't re-make a new client if React
+    // suspends during the initial render. This may not be needed if we
+    // have a suspense boundary BELOW the creation of the query client
+    if (!browserQueryClient) browserQueryClient = makeQueryClient();
+    return browserQueryClient;
+  }
+}
diff --git a/apps/web/app/auth/callback/route.ts b/apps/web/app/auth/callback/route.ts
index d2e12c6e..12919dc4 100644
--- a/apps/web/app/auth/callback/route.ts
+++ b/apps/web/app/auth/callback/route.ts
@@ -30,11 +30,11 @@ export async function GET(request: NextRequest) {
       return NextResponse.redirect(new URL(`${ROUTES.LOGIN}?error=auth_failed`, request.url));
     }
 
-    cookieList.set('accessToken', accessToken);
-    cookieList.set('memberId', memberId);
+    cookieList.set(COOKIE_ID.ACCESS_TOKEN, accessToken);
+    cookieList.set(COOKIE_ID.MEMBER_ID, memberId);
 
     if (nickname) {
-      cookieList.set('nickname', nickname);
+      cookieList.set(COOKIE_ID.NICKNAME, nickname);
       return NextResponse.redirect(new URL(ROUTES.HOME, request.url));
     }
 
diff --git a/apps/web/app/bookShelf/[ownerId]/page.tsx b/apps/web/app/bookShelf/[ownerId]/page.tsx
index 90caa07b..5361428b 100644
--- a/apps/web/app/bookShelf/[ownerId]/page.tsx
+++ b/apps/web/app/bookShelf/[ownerId]/page.tsx
@@ -28,10 +28,8 @@ const BookListPage = async ({ searchParams }: Props) => {
 
   const queryClient = getQueryClient();
 
-  await Promise.all([
-    queryClient.prefetchQuery(bookQueryOptions.list(memberId, getFilterBySearchParams(filter))),
-    queryClient.prefetchQuery(bookQueryOptions.count({ ownerId: memberId })),
-  ]);
+  queryClient.prefetchQuery(bookQueryOptions.list(memberId, getFilterBySearchParams(filter)));
+  queryClient.prefetchQuery(bookQueryOptions.count({ ownerId: memberId }));
 
   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
diff --git a/apps/web/app/books/[id]/page.tsx b/apps/web/app/books/[id]/page.tsx
index 9f5fad66..34a32b12 100644
--- a/apps/web/app/books/[id]/page.tsx
+++ b/apps/web/app/books/[id]/page.tsx
@@ -19,7 +19,7 @@ const BookDetailPage = async ({ params }: BookDetailPageProps) => {
 
   const queryClient = getQueryClient();
 
-  await queryClient.prefetchQuery(bookQueryOptions.detail(param.id));
+  queryClient.prefetchQuery(bookQueryOptions.detail(param.id));
 
   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
diff --git a/apps/web/app/books/[id]/review/page.tsx b/apps/web/app/books/[id]/review/page.tsx
index bef7d2dc..5b81cd9f 100644
--- a/apps/web/app/books/[id]/review/page.tsx
+++ b/apps/web/app/books/[id]/review/page.tsx
@@ -1,10 +1,11 @@
-import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
 
 import { Header } from '@sbooky/ui/components/Header';
 import { PageLayout, Stack } from '@sbooky/ui/components/Layout';
 import { Text } from '@sbooky/ui/components/Text';
 import { evaluaionQueryOptions } from 'app/_api/queries/evaluation';
 import { BackButton } from 'app/_components/BackButton';
+import { getQueryClient } from 'app/_util/queryClient';
 
 import { ReviewContent } from './_components/ReviewContent';
 
@@ -12,14 +13,11 @@ type Props = {
   params: Promise<{ id: string }>;
 };
 
-const queryClient = new QueryClient();
-
 const ReviewPage = async ({ params }: Props) => {
   const param = await params;
+  const queryClient = getQueryClient();
 
-  //TODO 추후 getQueryClient util 함수로 교체
-
-  await queryClient.fetchQuery(evaluaionQueryOptions.list(param.id));
+  queryClient.fetchQuery(evaluaionQueryOptions.list(param.id));
 
   return (
     <PageLayout
diff --git a/apps/web/app/closet/page.tsx b/apps/web/app/closet/page.tsx
index a90d5a4e..271c323a 100644
--- a/apps/web/app/closet/page.tsx
+++ b/apps/web/app/closet/page.tsx
@@ -1,16 +1,15 @@
 import { cookies } from 'next/headers';
 import { redirect } from 'next/navigation';
 
-import { HydrationBoundary, dehydrate, QueryClient } from '@tanstack/react-query';
+import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
 
 import { itemQueryOptions } from 'app/_api/queries/item';
 import { COOKIE_ID } from 'app/_constants/cookie';
 import { ROUTES } from 'app/_constants/route';
+import { getQueryClient } from 'app/_util/queryClient';
 
 import { ClosetSection } from './_components/ClosetSecion';
 
-const queryClient = new QueryClient();
-
 const ClosetPage = async () => {
   const cookieStore = await cookies();
   const memberId = cookieStore.get(COOKIE_ID.MEMBER_ID)?.value;
@@ -19,10 +18,10 @@ const ClosetPage = async () => {
     redirect(ROUTES.LOGIN);
   }
 
-  await Promise.all([
-    queryClient.prefetchQuery(itemQueryOptions.list(memberId)),
-    queryClient.prefetchQuery(itemQueryOptions.equipped(memberId)),
-  ]);
+  const queryClient = getQueryClient();
+
+  queryClient.prefetchQuery(itemQueryOptions.list(memberId));
+  queryClient.prefetchQuery(itemQueryOptions.equipped(memberId));
 
   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx
new file mode 100644
index 00000000..e5daf129
--- /dev/null
+++ b/apps/web/app/global-error.tsx
@@ -0,0 +1,31 @@
+'use client'; // Error boundaries must be Client Components
+//TODO 임시로 글로벌 에러바운더리 설정
+
+import { useEffect } from 'react';
+
+export default function Error({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string };
+  reset: () => void;
+}) {
+  useEffect(() => {
+    // Log the error to an error reporting service
+    console.error(error);
+  }, [error]);
+
+  return (
+    <div>
+      <h2>Something went wrong!</h2>
+      <button
+        onClick={
+          // Attempt to recover by trying to re-render the segment
+          () => reset()
+        }
+      >
+        Try again
+      </button>
+    </div>
+  );
+}
diff --git a/apps/web/app/home/page.tsx b/apps/web/app/home/page.tsx
index c0ea41d6..88aae701 100644
--- a/apps/web/app/home/page.tsx
+++ b/apps/web/app/home/page.tsx
@@ -1,18 +1,17 @@
 import { cookies } from 'next/headers';
 import { redirect } from 'next/navigation';
 
-import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
 
 import { bookQueryOptions } from 'app/_api/queries/book';
 import { itemQueryOptions } from 'app/_api/queries/item';
 import { COOKIE_ID } from 'app/_constants/cookie';
 import { ROUTES } from 'app/_constants/route';
+import { getQueryClient } from 'app/_util/queryClient';
 
 import { Home } from './_components/Home';
 
 const HomePage = async () => {
-  const queryClient = new QueryClient();
-
   const cookieStore = await cookies();
   const memberId = cookieStore.get(COOKIE_ID.MEMBER_ID)?.value;
   const nickname = cookieStore.get(COOKIE_ID.NICKNAME)?.value;
@@ -25,13 +24,11 @@ const HomePage = async () => {
     redirect(ROUTES.NICKNAME);
   }
 
-  await Promise.all([
-    queryClient.prefetchQuery(
-      bookQueryOptions.count({ ownerId: memberId, readStatus: 'COMPLETED' })
-    ),
-    queryClient.prefetchQuery(bookQueryOptions.point()),
-    queryClient.prefetchQuery(itemQueryOptions.equipped()),
-  ]);
+  const queryClient = getQueryClient();
+
+  queryClient.prefetchQuery(bookQueryOptions.count({ ownerId: memberId, readStatus: 'COMPLETED' }));
+  queryClient.prefetchQuery(bookQueryOptions.point());
+  queryClient.prefetchQuery(itemQueryOptions.equipped());
 
   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
diff --git a/apps/web/app/profile/page.tsx b/apps/web/app/profile/page.tsx
index 315d1bce..786d55a0 100644
--- a/apps/web/app/profile/page.tsx
+++ b/apps/web/app/profile/page.tsx
@@ -1,17 +1,16 @@
 import { cookies } from 'next/headers';
 import { redirect } from 'next/navigation';
 
-import { HydrationBoundary, dehydrate, QueryClient } from '@tanstack/react-query';
+import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
 
 import { bookQueryOptions } from 'app/_api/queries/book';
 import { itemQueryOptions } from 'app/_api/queries/item';
 import { COOKIE_ID } from 'app/_constants/cookie';
 import { ROUTES } from 'app/_constants/route';
+import { getQueryClient } from 'app/_util/queryClient';
 
 import { Profile } from './_components/Profile';
 
-const queryClient = new QueryClient();
-
 const ProfilePage = async () => {
   const cookieStore = await cookies();
   const memberId = cookieStore.get(COOKIE_ID.MEMBER_ID)?.value;
@@ -20,10 +19,10 @@ const ProfilePage = async () => {
     redirect(ROUTES.LOGIN);
   }
 
-  await Promise.all([
-    queryClient.fetchQuery(itemQueryOptions.list(memberId)),
-    queryClient.fetchQuery(bookQueryOptions.count({ ownerId: memberId, readStatus: 'COMPLETED' })),
-  ]);
+  const queryClient = getQueryClient();
+
+  queryClient.prefetchQuery(itemQueryOptions.list(memberId));
+  queryClient.prefetchQuery(bookQueryOptions.count({ ownerId: memberId, readStatus: 'COMPLETED' }));
 
   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 7378cff0..843820f4 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -7,16 +7,14 @@ import { ROUTES } from 'app/_constants/route';
 export async function middleware(request: NextRequest) {
   const cookieStore = await cookies();
   const accessToken = cookieStore.get(COOKIE_ID.ACCESS_TOKEN)?.value;
-  const nickname = cookieStore.get(COOKIE_ID.MEMBER_ID)?.value;
+  const nickname = cookieStore.get(COOKIE_ID.NICKNAME)?.value;
 
   const isRootPage = request.nextUrl.pathname === '/';
-  const isAuthCallbackPage = request.nextUrl.pathname.startsWith('/auth/callback');
-  const isAuthPage = isRootPage || isAuthCallbackPage;
 
   const isLoggedIn = Boolean(accessToken);
   const hasNickname = Boolean(nickname);
 
-  if (isAuthPage) {
+  if (isRootPage) {
     if (isLoggedIn) {
       return hasNickname
         ? NextResponse.redirect(new URL(ROUTES.HOME, request.url))
@@ -28,3 +26,15 @@ export async function middleware(request: NextRequest) {
 
   return NextResponse.next();
 }
+
+export const config = {
+  matcher: [
+    {
+      source: '/((?!auth/callback|_next/static|_next/image|favicon.ico|error).*)',
+      missing: [
+        { type: 'header', key: 'next-router-prefetch' },
+        { type: 'header', key: 'purpose', value: 'prefetch' },
+      ],
+    },
+  ],
+};


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0bddaf504e2c0c429683af9a69b7548d6e425fea and 3c7e89bbacaa538304ea27511e891b132559761a.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `apps/web/app/_api/ClientQueryProvider.tsx` (1 hunks)
* `apps/web/app/auth/callback/route.ts` (1 hunks)
* `apps/web/app/bookShelf/[ownerId]/page.tsx` (1 hunks)
* `apps/web/app/books/[id]/page.tsx` (1 hunks)
* `apps/web/app/books/[id]/review/page.tsx` (1 hunks)
* `apps/web/app/closet/page.tsx` (2 hunks)
* `apps/web/app/global-error.tsx` (1 hunks)
* `apps/web/app/home/page.tsx` (2 hunks)
* `apps/web/app/profile/page.tsx` (2 hunks)
* `apps/web/middleware.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (7)</summary>

<details>
<summary>apps/web/app/books/[id]/page.tsx (1)</summary><blockquote>

<details>
<summary>apps/web/app/_api/queries/book.ts (1)</summary>

* `bookQueryOptions` (29-62)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/bookShelf/[ownerId]/page.tsx (2)</summary><blockquote>

<details>
<summary>apps/web/app/_api/queries/book.ts (1)</summary>

* `bookQueryOptions` (29-62)

</details>
<details>
<summary>apps/web/app/bookShelf/[ownerId]/_utils/getFilterBySearchParams.ts (1)</summary>

* `getFilterBySearchParams` (3-11)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/home/page.tsx (3)</summary><blockquote>

<details>
<summary>apps/web/app/_util/queryClient.ts (1)</summary>

* `getQueryClient` (3-5)

</details>
<details>
<summary>apps/web/app/_api/queries/book.ts (1)</summary>

* `bookQueryOptions` (29-62)

</details>
<details>
<summary>apps/web/app/_api/queries/item.ts (1)</summary>

* `itemQueryOptions` (12-23)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/auth/callback/route.ts (1)</summary><blockquote>

<details>
<summary>apps/web/app/_constants/cookie.ts (1)</summary>

* `COOKIE_ID` (1-6)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/profile/page.tsx (3)</summary><blockquote>

<details>
<summary>apps/web/app/_util/queryClient.ts (1)</summary>

* `getQueryClient` (3-5)

</details>
<details>
<summary>apps/web/app/_api/queries/item.ts (1)</summary>

* `itemQueryOptions` (12-23)

</details>
<details>
<summary>apps/web/app/_api/queries/book.ts (1)</summary>

* `bookQueryOptions` (29-62)

</details>

</blockquote></details>
<details>
<summary>apps/web/middleware.ts (1)</summary><blockquote>

<details>
<summary>apps/web/app/_constants/cookie.ts (1)</summary>

* `COOKIE_ID` (1-6)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/closet/page.tsx (2)</summary><blockquote>

<details>
<summary>apps/web/app/_util/queryClient.ts (1)</summary>

* `getQueryClient` (3-5)

</details>
<details>
<summary>apps/web/app/_api/queries/item.ts (1)</summary>

* `itemQueryOptions` (12-23)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/web/app/global-error.tsx</summary>

[error] 9-10: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>apps/web/app/books/[id]/page.tsx (1)</summary>

`22-22`: **prefetchQuery 비동기 동작 수정 적용됨**

prefetchQuery가 더 이상 await를 사용하지 않도록 수정되었습니다. 이렇게 하면 데이터 페칭을 기다리지 않고 렌더링을 진행할 수 있어 초기 로딩 속도가 향상될 수 있습니다.

</details>
<details>
<summary>apps/web/app/bookShelf/[ownerId]/page.tsx (1)</summary>

`31-32`: **Promise.all 대신 순차적 prefetch 방식으로 수정**

기존의 await Promise.all을 사용한 동시 prefetch 방식에서 await 없는 순차적 prefetch 방식으로 변경되었습니다. 이는 PR 목표에 맞게 prefetch를 기다리지 않고 페이지 렌더링을 진행하도록 하는 변경사항입니다.

</details>
<details>
<summary>apps/web/app/auth/callback/route.ts (2)</summary>

`33-35`: **하드코딩된 쿠키 키값을 상수로 대체**

문자열로 하드코딩되어 있던 쿠키 키('accessToken', 'memberId')를 COOKIE_ID 상수를 사용하도록 변경하여 코드 일관성과 유지보수성이 향상되었습니다.

---

`37-37`: **nickname 쿠키도 상수 사용으로 일관성 유지**

nickname 쿠키 설정에도 하드코딩된 문자열 대신 COOKIE_ID.NICKNAME 상수를 사용하여 일관된 코드 스타일을 유지하였습니다.

</details>
<details>
<summary>apps/web/app/books/[id]/review/page.tsx (4)</summary>

`1-1`: **QueryClient import 제거**

QueryClient 직접 import를 제거하고 HydrationBoundary와 dehydrate만 import하도록 수정되었습니다. 이는 getQueryClient 유틸리티를 사용하는 패턴으로 변경된 것과 일치합니다.

---

`8-8`: **getQueryClient 유틸리티 사용**

PR 목표에 맞게 QueryClient를 직접 생성하지 않고 getQueryClient 유틸리티를 import하여 사용하도록 변경되었습니다.

---

`18-18`: **QueryClient 일관된 생성 방식 적용**

전역적으로 생성하던 QueryClient 인스턴스 대신 getQueryClient() 함수를 사용하여 인스턴스를 얻도록 변경되었습니다. 이는 서버 컴포넌트에서 일관된 queryClient 접근 방식을 구현하려는 PR 목표와 일치합니다.

---

`20-20`: **await 제거로 비동기 동작 최적화**

fetchQuery에서 await를 제거하여 데이터 페칭 완료를 기다리지 않고 페이지 렌더링을 진행할 수 있도록 수정되었습니다. 이는 PR 제목 "TanStack Query prefetch await 하지 않도록 수정"의 핵심 목표를 구현한 것입니다.

</details>
<details>
<summary>apps/web/app/home/page.tsx (2)</summary>

`4-10`: **TanStack Query 임포트 변경 확인**

React Query 사용 방식이 개선되었습니다. 단일 QueryClient 인스턴스를 사용하는 방식으로 변경한 것이 좋습니다.

---

`27-32`: <details>
<summary>❓ Verification inconclusive</summary>

**Prefetch 작업을 await 하지 않도록 변경**

PR의 목적대로 prefetch 작업을 await 하지 않도록 변경되었습니다. 이는 서버 컴포넌트의 렌더링 속도를 향상시킬 수 있습니다.

하지만 몇 가지 고려할 점이 있습니다:
1. 데이터가 완전히 로딩되기 전에 페이지가 렌더링될 가능성이 높아집니다.
2. 추가된 20초 staleTime 설정과 함께 pending 상태의 쿼리도 hydrate하도록 설정한 것과 일관성이 있는 접근법입니다.



클라이언트 컴포넌트에서 이 데이터에 접근할 때 로딩 상태를 적절히 처리하고 있는지 확인해 주세요:

---

🌐 Web query:

```web
TanStack Query에서 prefetchQuery를 사용할 때 await 하지 않는 패턴의 장단점
```

💡 Result:

TanStack Query에서 `prefetchQuery`를 사용할 때 `await` 하지 않는 패턴의 장단점을 정리해드리겠습니다.

### `await` 하지 않는 패턴의 장점
1. **비동기 실행과 병렬 처리**  
   `await`을 하지 않으면 여러 `prefetchQuery` 호출이 병렬로 실행됩니다. 이를 통해 여러 쿼리를 동시에 미리 불러와서 전체 로딩 시간을 줄일 수 있습니다.
   
2. **UI 지연 최소화**  
   프리패칭이 백그라운드에서 이루어지므로, 사용자 인터페이스가 즉시 반응하여 사용자 경험이 더 매끄러워질 수 있습니다.

3. **코드 간결**  
   `await`을 쓰지 않으면 코드가 좀 더 간결해지고, 굳이 프리패칭이 끝날 때까지 기다릴 필요가 없는 상황에서 적합합니다.

---

### `await` 하지 않는 패턴의 단점
1. **에러 처리 어려움**  
   `await`을 하지 않으면 프리패칭 과정에서 발생하는 에러를 즉각적으로 잡거나 처리하기 어려울 수 있습니다. 에러가 어디서 발생했는지 추적하기 힘들어질 수 있습니다.

2. **후속 처리 제어 어려움**  
   특정 쿼리 프리패칭이 완료된 뒤에만 실행되어야 하는 후속 로직이 있을 경우, `await`하지 않으면 순서 제어나 타이밍 제어가 어렵습니다.

3. **과도한 네트워크 요청 가능성**  
   여러 프리패칭이 동시에 일어나면서 네트워크에 과부하가 걸릴 수 있고, 서버에 불필요한 트래픽이 발생할 수 있습니다.

---

요약하면, UI 반응성을 최대화하고 병렬 처리로 빠른 프리패칭이 필요하면 `await`하지 않는 것이 유리합니다. 반면, 프리패칭 완료 시점을 제어하거나 에러 처리를 명확히 해야 한다면 `await`를 사용하는 것이 안전합니다. 사용 시나리오에 따라 적절히 선택하는 것이 중요합니다.

---



</details>

**PrefetchQuery 비동기 호출 검토 필요**

서버 컴포넌트에서 렌더링 최적화를 위해 `prefetchQuery`를 `await` 없이 호출하셨습니다. 이 패턴의 주요 장단점은 다음과 같습니다:

- 장점  
  • 여러 쿼리를 병렬로 실행하여 초기 로딩 시간 단축  
  • UI 지연 없이 즉시 렌더링 가능  

- 단점  
  • 에러 발생 시 즉각적인 감지·처리가 어려움  
  • 특정 쿼리 완료 후 실행해야 할 후속 로직 제어 불가  
  • 클라이언트에서 로딩·에러 상태를 명확히 관리해야 함  

apps/web/app/home/page.tsx (27–32)에서 아래 호출을 확인해 주세요:
```tsx
const queryClient = getQueryClient();

queryClient.prefetchQuery(bookQueryOptions.count({ ownerId: memberId, readStatus: 'COMPLETED' }));
queryClient.prefetchQuery(bookQueryOptions.point());
queryClient.prefetchQuery(itemQueryOptions.equipped());
```
- 클라이언트 컴포넌트에서 해당 데이터를 사용할 때 로딩·에러·완료 상태를 적절히 처리하고 있는지  
- 후속 로직(예: 특정 쿼리 완료 후 작업) 또는 에러 핸들러가 필요한지  

위 사항을 검토하신 뒤, 문제가 없다면 approve 부탁드립니다.

</details>
<details>
<summary>apps/web/app/profile/page.tsx (2)</summary>

`4-10`: **TanStack Query 임포트 변경 확인**

React Query 사용 방식이 개선되었습니다. 중앙화된 QueryClient 관리를 위해 getQueryClient 유틸리티를 사용하는 것이 좋습니다.

---

`22-26`: **Prefetch 작업을 await 하지 않도록 변경**

PR의 목적대로 prefetch 작업을 await 하지 않도록 변경되었습니다. 이는 서버 컴포넌트의 렌더링 성능을 개선할 수 있습니다.

다음 사항들을 고려해보세요:
1. 데이터가 완전히 로딩되지 않은 상태로 클라이언트에 전달될 수 있으므로, HydrationBoundary 내부의 Profile 컴포넌트에서 로딩 및 에러 상태를 적절히 처리하고 있는지 확인해야 합니다.
2. 이 접근 방식은 동일한 리포지토리의 다른 파일들에서도 일관되게 사용되어야 합니다.

</details>
<details>
<summary>apps/web/app/_api/ClientQueryProvider.tsx (2)</summary>

`3-10`: **TanStack Query 임포트 및 staleTime 설정 개선**

`defaultShouldDehydrateQuery`를 임포트하고 staleTime 상수를 도입한 것은 좋은 변화입니다. tkdodo의 블로그를 참조하여 20초 간격의 staleTime을 설정한 것이 적절합니다.

---

`18-32`: **Query Client 설정 개선**

다음과 같은 개선사항이 적용되었습니다:
1. staleTime 설정 (20초)
2. pending 상태의 쿼리도 dehydrate하도록 설정
3. 에러 redact 비활성화

특히 pending 상태의 쿼리를 hydrate하도록 한 것은 prefetch를 await 하지 않는 패턴과 함께 사용할 때 매우 유용합니다. 이렇게 하면 데이터 페칭이 완료되지 않은 상태에서도 클라이언트에서 쿼리 상태를 적절히 관리할 수 있습니다.

에러 redact를 비활성화한 것도 Next.js의 에러 처리 메커니즘과 더 잘 통합되어 동적 페이지 감지 및 에러 digest 처리가 개선됩니다.

</details>
<details>
<summary>apps/web/app/closet/page.tsx (3)</summary>

`4-4`: **import 변경이 적절합니다.**

QueryClient 직접 import 대신 커스텀 getQueryClient 유틸 함수를 사용하는 방식으로 변경한 것은 좋은 접근입니다.



Also applies to: 9-9

---

`21-22`: **QueryClient 인스턴스화 방식 개선**

getQueryClient() 함수를 통해 QueryClient 인스턴스를 가져오는 방식으로 변경했습니다. 이는 팀 내 일관된 QueryClient 생성 방식을 유지하는데 도움이 됩니다.

---

`23-24`: **await 제거에 대한 검토**

prefetchQuery 호출에서 await를 제거했습니다. TanStack Query의 prefetch 작업은 서버 측에서 데이터를 미리 로드하는 것이 목적이므로, 완료를 기다리지 않는 것이 성능 측면에서 이점이 있을 수 있습니다. 다만, 데이터가 로드되기 전에 페이지가 렌더링될 가능성이 있으므로 클라이언트 측에서 적절한 로딩 상태 처리가 필요한지 확인해보세요.

```diff
-  await Promise.all([
-    queryClient.prefetchQuery(itemQueryOptions.list(memberId)),
-    queryClient.prefetchQuery(itemQueryOptions.equipped(memberId))
-  ]);
+  queryClient.prefetchQuery(itemQueryOptions.list(memberId));
+  queryClient.prefetchQuery(itemQueryOptions.equipped(memberId));
```

PR 목표에 따라 await 제거가 의도적인 변경이라면 문제 없습니다.


ClosetSection 컴포넌트가 로딩 상태를 적절히 처리하고 있는지 확인해보세요.

</details>
<details>
<summary>apps/web/middleware.ts (3)</summary>

`10-10`: **인증 확인 방식 변경**

memberId 대신 nickname을 확인하도록 변경했습니다. 이는 사용자 인증 로직에 영향을 줄 수 있습니다. nickname이 회원 인증 상태를 더 정확하게 나타내는 지표라면 적절한 변경입니다.


이 변경이 PR 목표에 언급된 "로그인 버그"와 관련이 있을 수 있으니, 모든 인증 관련 시나리오에서 제대로 작동하는지 확인해보세요.

---

`17-17`: **경로 체크 간소화**

경로 체크 로직이 간소화되었습니다. 이전에는 root 페이지와 auth/callback 경로를 모두 확인했을 것으로 추정되지만, 이제는 root 페이지만 확인합니다. 이렇게 하면 로직이 단순해지고 이해하기 쉬워집니다.

---

`30-40`: **미들웨어 매처(matcher) 구성 추가**

미들웨어에 매처 구성을 추가하여 특정 경로와 조건에서만 실행되도록 설정했습니다. 이는 다음과 같은 이점이 있습니다:
1. auth/callback, 정적 자산, 이미지, 파비콘, 오류 페이지에는 미들웨어가 실행되지 않습니다.
2. 특정 프리페치 관련 헤더가 없는 경우에만 미들웨어가 실행됩니다.

이는 PR 목표에 언급된 "프리페치 및 라우트 핸들러는 특정 검사에서 제외" 부분과 일치합니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:47:09Z
user: login: keemsebin
body: 이슈와 관련된 `suspenseQuery`로 프리패치한데이터를 넘겨 받는 형태가 존재하지 않는데, 어떤 맥락에서 수정이 진행된건지 궁금합니다. 

> 따라서 서버에서 데이터를 prefetch하는 동안 하위 클라이언트 컴포넌트에서 렌더링이 블로킹되는 문제가 있습니다.

또 이에 대해 적어주셨는데, 렌더링이 블로킹 되는 캡쳐본이나 수정 후 개선된 점이 있으면 비교해주시면 좋을 것 같아요. 어떤 맥락에서 수정이 진행됐는지 이슈만으로는 알기 어려워 상황 공유해주시면 좋을 것 같습니다~!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T14:53:32Z
user: login: DongjaJ
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T15:03:42Z
user: login: keemsebin
body: 그렇군용 자세하게 답변해주셔서 감사합니다~! 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T03:26:34Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+'use client'; // Error boundaries must be Client Components
+//TODO 임시로 글로벌 에러바운더리 설정
+
+import { useEffect } from 'react';
+
+export default function Error({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string };
+  reset: () => void;
+}) {
+  useEffect(() => {
+    // Log the error to an error reporting service
+    console.error(error);
+  }, [error]);
+
+  return (
+    <div>
+      <h2>Something went wrong!</h2>
+      <button
+        onClick={
+          // Attempt to recover by trying to re-render the segment
+          () => reset()
+        }
+      >
+        Try again
+      </button>
+    </div>
+  );
+}
path: apps/web/app/global-error.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**전역 에러 바운더리 구현 검토**

에러 바운더리를 클라이언트 컴포넌트로 구현한 것은 적절합니다. 다음 개선점을 고려해 주세요:

1. 현재 에러 정보가 단순히 콘솔에만 출력되고 있습니다. 실제 프로덕션 환경에서는 Sentry 같은 에러 추적 서비스로 전송하는 것이 좋습니다.
2. 사용자에게 보여지는 UI가 매우 기본적입니다. 앱의 디자인 시스템에 맞게 스타일링을 추가하는 것이 좋겠습니다.
3. `error.digest` 속성이 정의되어 있지만 사용되지 않고 있습니다. 이 정보를 활용하여 개발자에게 더 유용한 정보를 제공할 수 있습니다.


```diff
- 'use client'; // Error boundaries must be Client Components
- //TODO 임시로 글로벌 에러바운더리 설정
+ 'use client'; // Error boundaries must be Client Components
 
 import { useEffect } from 'react';
 
-export default function Error({
+export default function GlobalError({
   error,
   reset,
 }: {
   error: Error & { digest?: string };
   reset: () => void;
 }) {
   useEffect(() => {
     // Log the error to an error reporting service
     console.error(error);
+    // TODO: 에러 추적 서비스로 에러 전송 구현
   }, [error]);
 
   return (
-    <div>
-      <h2>Something went wrong!</h2>
+    <div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
+      <h2 className="text-xl font-bold mb-4">오류가 발생했습니다!</h2>
+      {process.env.NODE_ENV === 'development' && error.digest && (
+        <p className="text-sm text-gray-500 mb-4">Error ID: {error.digest}</p>
+      )}
       <button
+        className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark"
         onClick={
           // Attempt to recover by trying to re-render the segment
           () => reset()
         }
       >
-        Try again
+        다시 시도하기
       </button>
     </div>
   );
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
'use client'; // Error boundaries must be Client Components

import { useEffect } from 'react';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
    // TODO: 에러 추적 서비스로 에러 전송 구현
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
      <h2 className="text-xl font-bold mb-4">오류가 발생했습니다!</h2>
      {process.env.NODE_ENV === 'development' && error.digest && (
        <p className="text-sm text-gray-500 mb-4">Error ID: {error.digest}</p>
      )}
      <button
        className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark"
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        다시 시도하기
      </button>
    </div>
  );
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 9-10: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/web/app/global-error.tsx lines 1 to 31, enhance the error boundary by
integrating an error tracking service like Sentry to send error details instead
of just logging to the console, update the UI to match the app's design system
with appropriate styling for better user experience, and utilize the
error.digest property to display or log additional useful error information for
developers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-16T11:47:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+'use client'; // Error boundaries must be Client Components
+//TODO 임시로 글로벌 에러바운더리 설정
+
+import { useEffect } from 'react';
+
+export default function Error({
path: apps/web/app/global-error.tsx
user: login: keemsebin
body: 요 에러 바운더리는 왜 생긴건가요?? 
created_at: 2025-05-19T14:50:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+'use client'; // Error boundaries must be Client Components
+//TODO 임시로 글로벌 에러바운더리 설정
+
+import { useEffect } from 'react';
+
+export default function Error({
path: apps/web/app/global-error.tsx
user: login: DongjaJ
body: 에러 바운더리 설정 안 해놓은 페이지가 많길래 최상단에 하나 설정해뒀어요.
created_at: 2025-05-19T15:03:42Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-16T11:42:57Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

이 변경사항은 React Query의 서버 사이드 prefetch 및 클라이언트 쿼리 설정을 개선하고, 쿠키 관리에 상수 사용을 통일하며, 글로벌 에러 바운더리를 추가합니다. 또한, 미들웨어의 경로 매칭과 인증 로직을 단순화하고, 쿼리 클라이언트 인스턴스화 방식을 통일합니다.

## Changes

| 파일/경로 그룹                                                                 | 변경 요약                                                                                                   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| apps/web/app/_api/ClientQueryProvider.tsx                                     | React Query 기본 옵션에 staleTime 추가, dehydrate 설정 커스터마이즈, 에러 redaction 비활성화                |
| apps/web/app/auth/callback/route.ts, apps/web/middleware.ts                   | 쿠키 키를 상수(COOKIE_ID)로 통일, 미들웨어에서 닉네임 쿠키 키 변경 및 인증 페이지 조건 단순화, config 추가   |
| apps/web/app/bookShelf/[ownerId]/page.tsx<br>apps/web/app/books/[id]/page.tsx<br>apps/web/app/closet/page.tsx<br>apps/web/app/home/page.tsx<br>apps/web/app/profile/page.tsx<br>apps/web/app/books/[id]/review/page.tsx | QueryClient 인스턴스화를 getQueryClient() 함수로 통일, prefetchQuery를 await 없이 순차 호출로 변경          |
| apps/web/app/global-error.tsx                                                 | 글로벌 에러 바운더리 컴포넌트(Error) 추가                                                                  |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant ServerComponent
    participant getQueryClient
    participant QueryClient
    participant ReactQuery
    participant HydrationBoundary

    ServerComponent->>getQueryClient: 쿼리 클라이언트 인스턴스 요청
    getQueryClient-->>ServerComponent: QueryClient 반환
    ServerComponent->>QueryClient: prefetchQuery (await 없이 호출)
    ServerComponent->>HydrationBoundary: dehydrate(QueryClient)로 상태 전달
    HydrationBoundary->>ReactQuery: 상태 복원 및 렌더링
```

```mermaid
sequenceDiagram
    participant User
    participant Middleware
    participant COOKIE_ID

    User->>Middleware: 요청 (예: '/')
    Middleware->>COOKIE_ID: 닉네임 쿠키 조회
    Middleware->>Middleware: 경로 및 쿠키 조건 확인
    alt 루트 페이지 & 닉네임 존재
        Middleware->>User: 홈으로 리다이렉트
    else 루트 페이지 & 닉네임 없음
        Middleware->>User: 닉네임 설정 페이지로 리다이렉트
    else 기타
        Middleware->>User: 요청 계속 진행
    end
```

## Assessment against linked issues

| Objective (이슈 번호)                                                                                       | Addressed | Explanation                                               |
|------------------------------------------------------------------------------------------------------------|-----------|-----------------------------------------------------------|
| 서버 prefetch 중 하위 컴포넌트 렌더링 블로킹 개선, prefetchQuery await 제거 (#142)                          | ✅        |                                                           |
| prefetchQuery → fetchQuery로 변경하여 서버에서 에러 핸들링 (#142)                                           | ❌        | fetchQuery 대신 prefetchQuery를 사용하고 있음              |
| 단일 쿼리 클라이언트 사용 고려 및 인스턴스화 방식 통일 (#142)                                               | ✅        |                                                           |
"""

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3c7e89bbacaa538304ea27511e891b132559761a and cdc478ed89e129cb576ca5654a01013dcd21aed2.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/web/app/_api/ClientQueryProvider.tsx` (1 hunks)
* `apps/web/app/_util/queryClient.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/web/app/_api/ClientQueryProvider.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: vercel-preview

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>apps/web/app/_util/queryClient.ts (4)</summary>

`1-4`: **적절한 스테일 타임 설정을 통한 최적화.**

TkDodo의 블로그를 참조하여 20초의 스테일 타임을 설정한 것은 좋은 접근입니다. 이는 PR 목표에 언급된 대로 쿼리 최적화를 위한 의도적인 선택으로 보입니다. 상수로 분리하여 관리하는 방식도 유지보수성을 높여줍니다.

---

`12-24`: **탁월한 dehydration 설정 개선.**

pending 상태의 쿼리도 dehydration에 포함시키는 커스텀 로직은 SSR 환경에서 중요한 개선사항입니다. 또한 Next.js의 서버 에러 처리 메커니즘을 고려하여 `shouldRedactErrors`를 false로 설정한 것은 상당히 고민된 접근입니다. 이 부분에 대한 주석 설명도 매우 명확합니다.

---

`29-43`: **서버/클라이언트 환경 분리 처리가 잘 구현되었습니다.**

서버에서는 항상 새로운 QueryClient를 생성하고, 브라우저에서는 싱글톤 패턴을 적용한 접근 방식이 아주 적절합니다. 특히 React Suspense를 고려한 조건부 인스턴스 생성 로직과 그에 대한 상세한 주석은 팀원들의 이해를 돕는 좋은 예시입니다.

한 가지 고려할 점은 브라우저 환경에서 싱글톤 인스턴스가 메모리 누수를 일으킬 가능성입니다. 애플리케이션의 라이프사이클에 따라 필요시 인스턴스를 정리하는 메커니즘도 고려해볼 수 있습니다.

---

`1-43`: **전체적으로 잘 구현된 QueryClient 관리 코드.**

이 구현은 다음과 같은 측면에서 매우 좋습니다:

1. PR 목표대로 pending 상태 쿼리의 hydration 지원
2. 서버와 클라이언트 환경의 명확한 구분
3. 적절한 스테일 타임 설정
4. 상세한 주석을 통한 코드 의도 명확화

TanStack Query의 모범 사례를 잘 따르고 있으며, 전체적인 아키텍처 개선에 기여할 것으로 보입니다.

한 가지 제안하자면, PR 설명에 언급된 "fetch query 함수명을 preFetchQuery로 변경"을 고려하는 것도 좋을 것 같습니다. 이 변경이 지금은 미루어졌지만, 함수명이 그 역할을 명확히 표현하는 것이 유지보수성을 높이는 데 도움이 됩니다.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dnd-side-project/dnd-12th-9-frontend&utm_content=209):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-16T11:44:06Z
body: ## [Codecov](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/pull/209?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project) Report
Attention: Patch coverage is `0%` with `38 lines` in your changes missing coverage. Please review.
> Project coverage is 0.28%. Comparing base [(`66b314c`)](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/commit/66b314c05e2b792794e9309b4aa434ddedcc22bf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project) to head [(`cdc478e`)](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/commit/cdc478ed89e129cb576ca5654a01013dcd21aed2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project).
> Report is 16 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/pull/209/graphs/tree.svg?width=650&height=150&src=pr&token=V0THDWMVL4&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project)](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/pull/209?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project)

```diff
@@           Coverage Diff            @@
##            main    #209      +/-   ##
========================================
- Coverage   0.29%   0.28%   -0.02%     
========================================
  Files        116     117       +1     
  Lines        668     697      +29     
  Branches     103     105       +2     
========================================
  Hits           2       2              
- Misses       610     638      +28     
- Partials      56      57       +1     
```

| [Components](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/pull/209/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project) | Coverage Δ | |
|---|---|---|
| [@sbooky/web](https://app.codecov.io/gh/dnd-side-project/dnd-12th-9-frontend/pull/209/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dnd-side-project) | `0.28% <0.00%> (-0.02%)` | :arrow_down: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T11:44:35Z
body: 🎮 웹 Preview 보러가기: https://dnd-12th-9-frontend-2l4apgac1-dongja.vercel.app
⏰ 배포 일시: Mon May 19 2025 00:39:49 GMT+0900
<!-- thollander/actions-comment-pull-request "209-vercel" -->
author_association: NONE
user: login: DongjaJ
created_at: 2025-05-19T15:35:33Z
body: 거의 모든 데이터 패칭이 server component에서 prefetch => client에서 prefetch한 데이터를 참조하는 방식으로 구현되어 있어요.

아래에서 보듯 queryClient.prefetchQuery()를 await로 Promise를 기다린 후에 HydrationBoundary가 렌더링되므로 데이터를 패칭하는 동안 HydrationBoundary를 포함한 ui는 **블로킹**됩니다.

Before [공식문서](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
```ts
//server component

const ServerComponent = ()=>{
 // 서버에서 prefetch
  await queryClient.prefetchQuery()

  return <HydrationBoundary state={dehydrate(queryClient)}>
      <ClientComponent/>
    </HydrationBoundary>
}

// Client component
const ClientComponent = ()=>{
  //... 
  const { data } = useQuery() // or useSuspenseQuery()를 이용해 prefetch한 데이터 참조
   
  // ...
}
```

아래처럼 pending 중인 query도 수화할 수 있도록 QueryClient를 설정하면 prefetchQuery를 await할 필요가 없게 됩니다.

>공식문서 번역 글 
React Query v5.40.0부터는 이 기능을 사용하기 위해 모든 프리페치를 기다릴 필요가 없습니다 . 보류 중인 쿼리도 디하이드레이션되어 클라이언트로 전송될 수 있기 때문입니다. 이를 통해 전체 Suspense 경계를 차단하지 않고 프리페치를 최대한 빨리 시작할 수 있으며, 쿼리가 완료되면 클라이언트로 데이터를 스트리밍 할 수 있습니다. 
>
>이 기능을 사용하려면 queryClient 에 보류 중인 쿼리 도 디하이드레이션 하도록 지시해야 합니다 . 이 작업은 전역적으로 수행하거나 해당 옵션을 dehydration 에 직접 전달하여 수행할 수 있습니다 .


```ts
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      dehydrate: {
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === 'pending',
        shouldRedactErrors: (error) => {
          return false
        },
      },
    },
  })
}
```

After [공식문서](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#streaming-with-server-components) [관련 예제](https://tanstack.com/query/latest/docs/framework/react/examples/nextjs-app-prefetching)
```ts
//server component

const ServerComponent = ()=>{
 // await 제거 
  queryClient.prefetchQuery()

  return <HydrationBoundary state={dehydrate(queryClient)}>
      <ClientComponent/>
    </HydrationBoundary>
}

// Client component는 변화 x
```

[이런 방법](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs)도 있긴 했는데 실험적 api이기도 하고 SEO에 악영향을 준다는 블로그 글을 봐서 이 방식은 제외했습니다.

before, after 비교 캡쳐는 해보려고 했는데 캐시 때문인지 잘 관찰이 안되네요. 설정을 변경할 근거는 공식문서로 충분하다고 생각해서 진행했어요.
author_association: COLLABORATOR