Skip to content

refactor: Context7 공식 문서 기반 리팩토링 목록 #206

@tnemnorivnelee

Description

@tnemnorivnelee

Context7 공식 문서 기반 리팩토링 목록

담당 파일들을 Next.js, React, TanStack Query 공식 문서(Context7)에서 확인한 권장 패턴과 비교 분석한 결과입니다.


CRITICAL (공식 권장 패턴 위반)

1. 서버 컴포넌트에서 new QueryClient() 매번 생성 — getQueryClient 싱글턴 패턴 미사용

  • 파일: src/app/(with-header)/layout.tsx, src/features/session/components/SessionList/SessionListPrefetch.tsx, src/features/member/hooks/useMemberHooks.ts (prefetchMe, prefetchMeForEdit, prefetchMyReport)
  • 출처: TanStack Query Advanced SSR Guide
  • 현재: 서버 컴포넌트마다 new QueryClient()를 직접 생성. defaultOptions(staleTime, dehydrate 설정 등)이 적용되지 않으며, 같은 request 내에서 QueryClient가 공유되지 않아 중복 fetch 가능
  • 공식 권장: React.cache()를 사용한 request-scoped 싱글턴 패턴
  • 개선:
// src/lib/get-query-client.ts (신규)
import { QueryClient, defaultShouldDehydrateQuery } from "@tanstack/react-query";
import { cache } from "react";

const getQueryClient = cache(() => new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,
    },
    dehydrate: {
      shouldDehydrateQuery: (query) =>
        defaultShouldDehydrateQuery(query) ||
        query.state.status === "pending",
    },
  },
}));

export default getQueryClient;
  • 영향 범위: layout.tsx, SessionListPrefetch.tsx, prefetchMe(), prefetchMeForEdit(), prefetchMyReport() 모두 getQueryClient() 사용으로 교체

2. with-header layout에서 await prefetchQuery — 비동기 스트리밍 차단

  • 파일: src/app/(with-header)/layout.tsx
  • 출처: TanStack Query — Prefetch Without Await
  • 현재: await queryClient.fetchQuery(memberQueries.me())로 사용자 정보를 동기적으로 가져옴. 이 동안 모든 children 렌더링이 블로킹됨
  • 공식 권장: prefetchQueryawait 없이 호출하면 데이터가 준비되는 동안 children이 먼저 스트리밍됨
  • 개선: fetchQueryprefetchQuery (await 제거). dehydrate 설정에서 pending 쿼리도 포함하도록 설정 (위 getQueryClient에 포함)
// Before
const response = await queryClient.fetchQuery(memberQueries.me());
memberProfile = response.result;

// After — 블로킹 없이 prefetch
queryClient.prefetchQuery(memberQueries.me());
// memberProfile은 클라이언트에서 useMe() 훅으로 접근
  • 주의: memberProfile.firstLogin으로 OnboardingModal을 조건부 렌더링하는 로직이 있으므로, 이 부분을 클라이언트 컴포넌트로 분리하여 useMe() 훅으로 판단하도록 변경 필요

3. SessionListPrefetch에서 await prefetchQuery — 불필요한 블로킹

  • 파일: src/features/session/components/SessionList/SessionListPrefetch.tsx
  • 출처: 동일
  • 현재: await queryClient.prefetchQuery(...)prefetchQuery는 반환값이 없으므로 await가 불필요하게 렌더링을 차단
  • 공식 권장: prefetchQuery는 await 없이 호출하여 데이터 스트리밍
  • 개선:
// Before
await queryClient.prefetchQuery({
  queryKey: sessionKeys.list(listParams),
  queryFn: () => sessionApi.getList(listParams),
});

// After — await 제거
queryClient.prefetchQuery({
  queryKey: sessionKeys.list(listParams),
  queryFn: () => sessionApi.getList(listParams),
});

HIGH (중요 개선)

4. profile/report 에러 처리 — Next.js error.tsx 파일 컨벤션 활용

  • 파일: src/app/(with-header)/profile/report/ 디렉토리
  • 출처: Next.js Error Handling
  • 현재: StatsContent.tsx, SessionHistoryContent.tsx에서 throw new Error() 사용. src/app/(with-header)/profile/report/error.tsx가 존재하여 catch는 되지만, Suspense 단위의 세분화된 에러 처리가 아닌 전체 페이지 단위 에러 처리
  • 공식 권장: Suspense 내부에서 throw된 에러는 가장 가까운 error boundary에서 잡힘. 현재 구조에서는 profile/report/error.tsx가 두 Suspense를 모두 감싸므로 하나가 실패하면 전체 리포트가 에러 UI로 교체됨
  • 개선: 각 섹션별 ErrorBoundary 래핑으로 부분 에러 처리 지원
// profile/report/page.tsx
<ErrorBoundary fallback={<StatsErrorFallback />}>
  <Suspense fallback={<StatsSkeleton />}>
    <StatsContent />
  </Suspense>
</ErrorBoundary>

<ErrorBoundary fallback={<SessionHistoryErrorFallback />}>
  <Suspense fallback={<SessionHistorySkeleton />}>
    <SessionHistoryContent page={page} />
  </Suspense>
</ErrorBoundary>

5. login 페이지 — searchParamscookies() 병렬 await

  • 파일: src/app/(auth)/login/page.tsx, src/app/@modal/(.)login/page.tsx
  • 출처: Next.js — cookies and headers functions
  • 현재: searchParamscookies() 순차 await
  • 공식 문서: 두 작업은 독립적이므로 병렬 실행 가능
  • 개선:
// Before
const params = await searchParams;
const cookieStore = await cookies();

// After
const [params, cookieStore] = await Promise.all([searchParams, cookies()]);

6. prefetchMe 등 유틸 함수 — dehydrate 반환 패턴 개선

  • 파일: src/features/member/hooks/useMemberHooks.ts (prefetchMe, prefetchMeForEdit, prefetchMyReport)
  • 출처: TanStack Query Advanced SSR Guide
  • 현재: 각 prefetch 함수가 자체 new QueryClient()를 생성하고 dehydrate()까지 수행. 호출 측에서 HydrationBoundary에 직접 전달하는 패턴
  • 공식 권장: getQueryClient() 싱글턴을 사용하면 별도 prefetch 유틸 함수가 불필요. 서버 컴포넌트에서 직접 getQueryClient().prefetchQuery()를 호출
  • 개선: prefetchMe(), prefetchMeForEdit(), prefetchMyReport() 함수 제거 또는 getQueryClient() 활용으로 단순화

MEDIUM (개선 권장)

7. QueryProvider — useState 초기화 패턴 개선

  • 파일: src/providers/QueryProvider.tsx
  • 출처: TanStack Query Advanced SSR Guide
  • 현재: useState(() => new QueryClient({...})) 패턴 사용
  • 공식 권장: Suspense boundary가 QueryClient 생성 아래에 없으면 React가 initial render 중 suspend 시 client를 버릴 수 있음. getQueryClient() 싱글턴 패턴이 더 안전
  • 개선:
// Before
const [queryClient] = useState(() => new QueryClient({...}));

// After
import { getQueryClient } from "@/lib/get-query-client";
const queryClient = getQueryClient();
  • 참고: 현재 useState 패턴도 동작하지만, TanStack Query 공식 문서가 권장하는 getQueryClient 패턴으로 통일하면 서버/클라이언트 모두 동일한 설정 공유 가능

8. React use 훅 활용 가능성 — Promise를 prop으로 전달

  • 파일: src/app/(with-header)/page.tsx 등 서버 컴포넌트
  • 출처: React 19 — use API
  • 현재: 서버 컴포넌트에서 데이터를 prefetch 후 HydrationBoundary로 전달하는 패턴
  • 공식 문서: React 19의 use() 훅으로 Promise를 직접 prop으로 전달하여 Suspense에서 resolve 가능
  • 상태: 현재 TanStack Query 기반 패턴이 잘 동작하고 있으므로 즉시 변경 불필요. 향후 데이터 패칭 레이어 단순화 시 고려할 옵션

9. dehydrate 설정 — pending 쿼리 포함

  • 파일: src/providers/QueryProvider.tsx 또는 새로운 get-query-client.ts
  • 출처: TanStack Query Advanced SSR Guide
  • 현재: dehydrate 기본 설정 사용 — pending 상태의 쿼리는 dehydrate되지 않음
  • 공식 권장: shouldDehydrateQuery에서 pending 상태 쿼리도 포함하면, await 없이 prefetchQuery를 호출해도 클라이언트에서 데이터를 받을 수 있음
  • 개선: getQueryClient 싱글턴 도입 시 함께 설정 (항목 1에 포함)

10. metadata — createPageMetadata 패턴 검증

  • 파일: src/app/(auth)/login/page.tsx, src/app/(with-header)/page.tsx
  • 출처: Next.js Metadata API
  • 현재: createPageMetadata() 유틸로 정적 metadata 생성 — export const metadata
  • 상태: ✅ 이미 양호 — 정적 metadata 패턴이 공식 문서와 일치

11. Intercepting Routes — 모달 패턴 검증

  • 파일: src/app/@modal/(.)login/page.tsx, src/app/@modal/(.)session/[sessionId]/page.tsx
  • 출처: Next.js Parallel Routes & Intercepting Routes
  • 현재: @modal 슬롯 + (.) 인터셉팅 라우트로 모달 구현
  • 상태: ✅ 이미 양호 — 공식 문서의 모달 패턴과 정확히 일치

요약

실행 가능한 작업 목록

# 영향도 파일 작업
1 CRITICAL 신규 get-query-client.ts + 서버 컴포넌트 전체 React.cache() 기반 QueryClient 싱글턴 도입
2 CRITICAL (with-header)/layout.tsx fetchQueryprefetchQuery (await 제거), OnboardingModal 로직 클라이언트 분리
3 CRITICAL SessionListPrefetch.tsx await prefetchQueryprefetchQuery (await 제거)
4 HIGH profile/report/page.tsx Suspense별 ErrorBoundary 래핑
5 HIGH login/page.tsx, modal login/page.tsx searchParams & cookies 병렬 Promise.all
6 HIGH useMemberHooks.ts prefetch 유틸 함수들 getQueryClient() 활용으로 단순화
7 MEDIUM QueryProvider.tsx useStategetQueryClient() 패턴 통일
8 MEDIUM 향후 고려 React 19 use() 훅 활용 가능성
9 MEDIUM get-query-client.ts dehydrate에 pending 쿼리 포함 설정 (항목 1에 포함)

이미 잘 되어 있는 부분

항목 관련 공식 문서 파일
정적 metadata export 패턴 Next.js Metadata API login/page.tsx, home page.tsx
Intercepting Routes 모달 패턴 Next.js Parallel Routes @modal/(.)login, @modal/(.)session
Suspense 기반 스트리밍 구조 React Suspense home page.tsx, report page.tsx
HydrationBoundary 사용 TanStack Query SSR layout.tsx, SessionListPrefetch.tsx
QueryClient staleTime 설정 TanStack Query Defaults QueryProvider.tsx
ReactQueryDevtools 동적 로딩 Next.js dynamic import QueryProvider.tsx
error.tsx 파일 컨벤션 Next.js Error Handling 각 라우트 세그먼트

검증 방법

  1. pnpm build — 빌드 성공 확인
  2. pnpm lint — lint 통과 확인
  3. 홈 페이지 로드 — 세션 목록이 스트리밍으로 표시되는지 확인 (Suspense fallback → 데이터)
  4. 프로필 리포트 페이지 — 통계/히스토리 각각 독립적으로 로드되는지 확인
  5. 로그인 모달 — 정상 동작 확인
  6. Network 탭 — prefetch가 await 없이 동작하여 TTFB 개선 확인
  7. React Query Devtools — 쿼리 상태 (stale, fresh) 확인

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions