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()로 병렬 실행해야 한다
- 현재:
searchParams와 cookies()를 순차적으로 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 이후에 로드해야 한다
- 현재:
GoogleAnalytics가 next/script의 afterInteractive를 사용하긴 하지만, 컴포넌트 자체가 정적 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 />}
- 상태: ✅ 이미 양호 —
isSearchMode는 Boolean() 결과이므로 항상 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 |
검증 방법
pnpm build — 빌드 성공 확인
pnpm lint — lint 통과 확인
- 브라우저에서 로그인/홈/프로필 페이지 동작 확인
- Network 탭에서 GoogleAnalytics 지연 로딩 확인
- Bundle Analyzer로 번들 사이즈 비교 (선택)
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.tsxPromise.all()로 병렬 실행해야 한다searchParams와cookies()를 순차적으로 await2.
bundle-barrel-imports— optimizePackageImports 설정 추가next.config.tsoptimizePackageImports미설정.@tanstack/react-query,lucide-react등이 barrel import로 사용됨3.
bundle-defer-third-party— GoogleAnalytics 동적 로딩src/app/layout.tsxGoogleAnalytics가next/script의afterInteractive를 사용하긴 하지만, 컴포넌트 자체가 정적 import로 메인 번들에 포함됨4.
server-parallel-fetching— with-header layout의 데이터 패칭 구조 개선src/app/(with-header)/layout.tsxawait cookies()→await queryClient.fetchQuery(memberQueries.me())순차 실행.fetchQuery를 await하므로 children 렌더링이 블로킹됨HIGH (중요)
5.
server-serialization— OnboardingModalWrapper에 전달하는 데이터 최소화src/app/(with-header)/layout.tsxmemberProfile에서nickname,profileImageUrl,firstLogin3개 필드만 추출하여 전달6.
server-cache-react— memberQueries.me() 중복 호출 방지src/app/(with-header)/layout.tsx+ 하위 Server ComponentReact.cache()로 dedup해야 한다fetchQuery(memberQueries.me())로 prefetch하고HydrationBoundary로 전달memberQueries.me()를 호출한다면,React.cache()로 래핑하여 서버 측 dedup 보장 필요fetchQuery+HydrationBoundary패턴으로 클라이언트 dedup은 처리됨. 서버 측 중복 호출 여부 확인 필요MEDIUM (개선 권장)
7.
async-suspense-boundaries— 홈페이지 Suspense 구조 검증src/app/(with-header)/page.tsxSearchFilterSection,RecommendedSection,SessionListPrefetch에 각각 독립 Suspense boundary 적용8.
async-suspense-boundaries— profile/report 페이지 검증src/app/(with-header)/profile/report/page.tsxStatsContent,SessionHistoryContent에 각각 독립 Suspense boundary 적용9.
rendering-conditional-render— isSearchMode 조건부 렌더링 검증src/app/(with-header)/page.tsx:60&&연산자 사용 시 falsy 값(0, NaN)이 렌더링될 수 있으므로 명시적 삼항 연산자 사용 권장{!isSearchMode && <Banner />}isSearchMode는Boolean()결과이므로 항상 boolean 타입.&&사용이 안전함10.
async-defer-await— profile/report의 searchParams await 타이밍src/app/(with-header)/profile/report/page.tsxconst { page: pageParam } = await searchParams;후 바로 사용11.
server-parallel-fetching— profile layout의 ProfileSummary 검증src/app/(with-header)/profile/layout.tsxProfileSummary가"use client"컴포넌트이고useMe()훅으로 데이터 패칭요약
실행 가능한 작업 목록
async-parallelbundle-barrel-importsbundle-defer-third-partyserver-parallel-fetchingserver-cache-react이미 잘 되어 있는 부분
async-suspense-boundariesbundle-dynamic-importsrendering-conditional-renderserver-serializationrerender-memo등server-parallel-fetchingasync-defer-await검증 방법
pnpm build— 빌드 성공 확인pnpm lint— lint 통과 확인