Skip to content

refactor: Vercel React Best Practices 기반 리팩토링 목록 #203

@tnemnorivnelee

Description

@tnemnorivnelee

Vercel React Best Practices 기반 리팩토링 목록

담당 파일들을 Vercel React Best Practices 45개 규칙 기준으로 분석한 결과입니다.
reactCompiler: true가 이미 활성화되어 있어 수동 memo/useMemo 관련 규칙은 제외했습니다.


CRITICAL (우선순위 높음)

1. async-parallel — login 페이지의 searchParams & cookies 병렬화

  • 파일: src/app/(auth)/login/page.tsx, src/app/@modal/(.)login/page.tsx
  • 규칙: 독립적인 async 작업은 Promise.all()로 병렬 실행해야 한다
  • 현재: searchParamscookies()를 순차적으로 await
// Before
const params = await searchParams;
const cookieStore = await cookies();

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

2. bundle-barrel-imports — optimizePackageImports 설정 추가

  • 파일: next.config.ts
  • 규칙: barrel file import는 사용하지 않는 모듈까지 로드하므로 번들 사이즈가 커진다
  • 현재: optimizePackageImports 미설정. @tanstack/react-query, lucide-react 등이 barrel import로 사용됨
// next.config.ts에 추가
experimental: {
  optimizePackageImports: ["lucide-react", "@tanstack/react-query"],
}

3. bundle-defer-third-party — GoogleAnalytics 동적 로딩

  • 파일: src/app/layout.tsx
  • 규칙: 애널리틱스, 로깅 등 비핵심 서드파티는 hydration 이후에 로드해야 한다
  • 현재: GoogleAnalyticsnext/scriptafterInteractive를 사용하긴 하지만, 컴포넌트 자체가 정적 import로 메인 번들에 포함됨
// Before
import GoogleAnalytics from "@/lib/GoogleAnalytics";

// After
import dynamic from "next/dynamic";

const GoogleAnalytics = dynamic(
  () => import("@/lib/GoogleAnalytics"),
  { ssr: false }
);

4. server-parallel-fetching — with-header layout의 데이터 패칭 구조 개선

  • 파일: src/app/(with-header)/layout.tsx
  • 규칙: React Server Component는 순차적으로 실행되므로, 컴포넌트 합성을 통해 병렬화해야 한다
  • 현재: Layout에서 await cookies()await queryClient.fetchQuery(memberQueries.me()) 순차 실행. fetchQuery를 await하므로 children 렌더링이 블로킹됨
  • 개선 방안: 인증 체크 + HydrationBoundary 로직을 별도 async Server Component로 분리하여, layout이 children을 즉시 렌더링할 수 있도록 구성 (Suspense 활용)

HIGH (중요)

5. server-serialization — OnboardingModalWrapper에 전달하는 데이터 최소화

  • 파일: src/app/(with-header)/layout.tsx
  • 규칙: RSC → Client Component 경계에서는 실제 사용하는 필드만 전달해야 한다
  • 현재: memberProfile에서 nickname, profileImageUrl, firstLogin 3개 필드만 추출하여 전달
  • 상태: ✅ 이미 양호 — 별도 작업 불필요

6. server-cache-react — memberQueries.me() 중복 호출 방지

  • 파일: src/app/(with-header)/layout.tsx + 하위 Server Component
  • 규칙: 동일 request 내 중복 async 호출은 React.cache()로 dedup해야 한다
  • 현재: layout에서 fetchQuery(memberQueries.me())로 prefetch하고 HydrationBoundary로 전달
  • 확인 필요: 동일 request 내에서 다른 Server Component도 memberQueries.me()를 호출한다면, React.cache()로 래핑하여 서버 측 dedup 보장 필요
  • 상태: ⚠️ 확인 후 판단 — React Query의 fetchQuery + HydrationBoundary 패턴으로 클라이언트 dedup은 처리됨. 서버 측 중복 호출 여부 확인 필요

MEDIUM (개선 권장)

7. async-suspense-boundaries — 홈페이지 Suspense 구조 검증

  • 파일: src/app/(with-header)/page.tsx
  • 규칙: Suspense를 사용하여 래퍼 UI를 먼저 보여주고 데이터를 스트리밍해야 한다
  • 현재: SearchFilterSection, RecommendedSection, SessionListPrefetch에 각각 독립 Suspense boundary 적용
  • 상태: ✅ 이미 양호 — 각 섹션이 독립적으로 스트리밍됨

8. async-suspense-boundaries — profile/report 페이지 검증

  • 파일: src/app/(with-header)/profile/report/page.tsx
  • 규칙: 동일
  • 현재: StatsContent, SessionHistoryContent에 각각 독립 Suspense boundary 적용
  • 상태: ✅ 이미 양호

9. rendering-conditional-render — isSearchMode 조건부 렌더링 검증

  • 파일: src/app/(with-header)/page.tsx:60
  • 규칙: && 연산자 사용 시 falsy 값(0, NaN)이 렌더링될 수 있으므로 명시적 삼항 연산자 사용 권장
  • 현재: {!isSearchMode && <Banner />}
  • 상태: ✅ 이미 양호isSearchModeBoolean() 결과이므로 항상 boolean 타입. && 사용이 안전함

10. async-defer-await — profile/report의 searchParams await 타이밍

  • 파일: src/app/(with-header)/profile/report/page.tsx
  • 규칙: await는 실제로 값이 필요한 분기에서만 수행해야 한다
  • 현재: const { page: pageParam } = await searchParams; 후 바로 사용
  • 상태: ✅ 이미 양호 — await 이후 즉시 사용하므로 defer할 여지 없음

11. server-parallel-fetching — profile layout의 ProfileSummary 검증

  • 파일: src/app/(with-header)/profile/layout.tsx
  • 규칙: 서버 컴포넌트 간 데이터 패칭을 병렬화해야 한다
  • 현재: ProfileSummary"use client" 컴포넌트이고 useMe() 훅으로 데이터 패칭
  • 상태: ✅ 이미 양호 — 클라이언트 컴포넌트이므로 서버 워터폴 해당 없음

요약

실행 가능한 작업 목록

# 규칙 파일 작업 영향도
1 async-parallel login/page.tsx, modal login/page.tsx searchParams & cookies 병렬화 CRITICAL
2 bundle-barrel-imports next.config.ts optimizePackageImports 추가 CRITICAL
3 bundle-defer-third-party layout.tsx (root) GoogleAnalytics dynamic import CRITICAL
4 server-parallel-fetching layout.tsx (with-header) 인증 로직 분리로 children 블로킹 제거 CRITICAL
5 server-cache-react layout.tsx (with-header) + 하위 SC 서버 측 me() 중복 호출 확인 및 React.cache() 적용 HIGH

이미 잘 되어 있는 부분

항목 관련 규칙 파일
각 섹션별 독립 Suspense 경계 async-suspense-boundaries 홈 page.tsx, 리포트 page.tsx
OnboardingModalWrapper dynamic import bundle-dynamic-imports with-header layout.tsx
조건부 렌더링에서 boolean 타입 안전 rendering-conditional-render 홈 page.tsx
Server→Client 데이터 직렬화 최소화 server-serialization with-header layout.tsx
React Compiler 활성화 rerender-memo next.config.ts
클라이언트 컴포넌트의 데이터 패칭 패턴 server-parallel-fetching profile layout.tsx
searchParams await 즉시 사용 async-defer-await profile/report page.tsx

검증 방법

  1. pnpm build — 빌드 성공 확인
  2. pnpm lint — lint 통과 확인
  3. 브라우저에서 로그인/홈/프로필 페이지 동작 확인
  4. Network 탭에서 GoogleAnalytics 지연 로딩 확인
  5. Bundle Analyzer로 번들 사이즈 비교 (선택)

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions