Vercel React 코드 감사 및 개선 결과
- 기준일: 2026-07-22
- 기준:
vercel-react-best-practices
- 범위:
src/routes, 각 feature의 pages/components/hooks/domain/store, src/shared/ui
- 상태: 구현 및 검증 완료
적용 기준
이번 감사에서는 현재 Vite + React + TanStack Router/Query 구조에 직접 적용되는 항목을 우선했습니다.
- 배럴 import로 인한 불필요한 모듈 로딩 제거
- 큰 라이브러리와 개발 전용 기능의 지연 로딩
- 서로 독립적인 비동기 작업의 병렬 처리
- 페이지 최상위의 과도한 상태 구독과 리렌더링 격리
- 요청 취소, 중복 요청 방지, 타이머 정리
- 로딩·에러·데이터 동기화 시 화면 일관성 보장
- 한 번만 필요한 값의 lazy 초기화
번들 결과
| 항목 | 변경 전 | 변경 후 | 결과 |
|---|
초기 app.js | 405.95 kB / gzip 129.98 kB | 347.60 kB / gzip 112.98 kB | gzip 약 17.00 kB 감소 |
| 이미지 저장 라이브러리 | 초기 그래프에 포함 | 별도 청크 13.00 kB / gzip 5.19 kB | 저장할 때만 로드 |
| Live Studio 관련 청크 합계 | 161.92 kB / gzip 51.34 kB | 141.37 kB / gzip 43.89 kB | 개발용 QA 코드 운영 번들 제외 |
라우트 자동 분할은 TanStack Router의 autoCodeSplitting: true가 이미 담당하므로 페이지에 별도 React.lazy를 중복 적용하지 않았습니다.
페이지별 점검
| 페이지·라우트 | 판정 | 적용 내용 |
|---|
Root (__root) | 수정 완료 | 공용 UI 배럴 import를 직접 import로 교체해 모든 페이지 초기 청크가 전체 UI를 끌어오지 않도록 함 |
| 로그인 | 적합 | 직접 import로 경계를 명확히 했고, 현재 렌더·요청 구조는 추가 메모이제이션이 불필요함 |
| Google 로그인 완료 | 수정 완료 | OAuth 완료 요청을 React Query로 단일화하고 StrictMode에서도 POST와 이동이 중복되지 않게 함 |
| TAPLE 소개 | 적합 | 직접 import로 정리했으며 정적 콘텐츠 중심이라 추가 상태 최적화는 불필요함 |
| Live Studio | 수정 완료 | 프로필 폼 동기화와 블록 배열 구독을 하위 컴포넌트로 격리해 입력 때 페이지 전체가 리렌더링되지 않게 함 |
| Live Studio | 수정 완료 | 미저장 작업을 모두 비운 뒤 미리보기로 이동하며 저장 실패 시 현재 화면에 머물도록 함 |
| Live Studio | 수정 완료 | QA 도구를 DEV 환경에서만 동적 import해 운영 번들에서 제거함 |
| Live Preview | 수정 완료 | 서버 명함과 로컬 초안의 identity/version을 비교해 최신 초안을 보존하고, 동기화 전 기본 명함이 잠깐 보이는 현상을 제거함 |
| Live Preview | 수정 완료 | pending/error/404를 Skeleton, ErrorState, NotFoundState로 명확히 분기함 |
| 공개 명함 | 수정 완료 | 저장 기능에서만 html-to-image를 불러오고 앞·뒤 이미지 생성을 병렬 처리함 |
| 공개 명함 | 수정 완료 | 로그아웃 시 인증 정리와 초안 스토어 로드를 병렬 처리하고 무거운 스토어를 초기 공개 라우트에서 분리함 |
| NFC 진입 | 수정 완료 | 인증 여부를 lazy state로 한 번 읽고, 계정 전환 시 로그아웃·스토어 로드를 병렬 처리함 |
| 마이페이지 | 수정 완료 | 기존 병렬 조회는 유지하고 로그아웃 시 Live Studio 초안도 함께 초기화해 다음 계정으로 상태가 새지 않게 함 |
| 관리자 | 수정 완료 | 검색에 300ms debounce를 적용하고 요청 AbortSignal을 API까지 전달함 |
| 관리자 | 수정 완료 | 펼치지 않은 회원 행은 상세 query observer 자체를 만들지 않도록 상세 영역을 분리함 |
| 사용 불가 명함 | 적합 | 작은 정적 안내 라우트로 유지하고 직접 import만 적용함 |
컴포넌트별 점검
Live Studio
| 컴포넌트 | 판정 | 확인·수정 내용 |
|---|
BlockEditorSection | 적합 | DnD와 블록 상태가 편집 영역 안에 머물며 공개 페이지 번들에서는 분리됨 |
CardCustomizeSheet | 적합 | 단일 UI 책임을 유지하며 불필요한 상태 복제 없음 |
CardImageExporter | 수정 완료 | imperative handle 의존성을 고정하고 실제 저장 시점에만 변환 모듈 사용 |
CardPreviewSection | 적합 | 현재 입력만 받아 표현하며 별도 중복 상태 없음 |
GalleryThumbnail | 적합 | 작은 표시 컴포넌트로 유지 |
ImageViewer | 적합 | 페이지 상태와 분리된 뷰어 책임 유지 |
LinkPlatformIcon | 적합 | 직접 아이콘 import로 불필요한 아이콘 모듈 로딩 방지 |
LiveStudioDevTools | 수정 완료 | 운영 코드 경로에서 제외 |
LiveStudioDevToolsContainer | 신규 | QA query/mutation 구독을 개발 전용 경계 안으로 이동 |
LiveStudioSkeleton | 적합 | 레이아웃 크기를 유지하는 로딩 UI 사용 |
PrivateBusinessCardNotice | 적합 | 개인정보를 조회하지 않는 비공개 상태 표현 유지 |
ProfileEditModal | 적합 | 폼 책임을 모달 내부에 유지 |
ProfileEditorSection | 수정 완료 | 상위 페이지 대신 전용 동기화 컴포넌트가 폼 값을 구독하도록 변경 |
PublicBusinessCardSkeleton | 적합 | 공개 화면과 미리보기에서 공통 사용 |
PublicCardHeader | 적합 | 방문자 상태별 액션만 표현하며 데이터 요청 없음 |
PublicLinkShareBar | 수정 완료 | 복사 완료 타이머를 재실행·unmount 때 정리 |
StudioSummary | 적합 | 표시 책임만 유지하고 상태 구독 없음 |
Admin
| 컴포넌트 | 판정 | 확인·수정 내용 |
|---|
ConnectCardDialog | 적합 | mutation 상태를 상위 훅에서 받아 표시하는 구조 유지 |
DashboardSummary | 적합 | 계산된 요약 데이터를 표현만 함 |
IssueCardDialog | 적합 | 단일 폼 책임 유지 |
UnconnectedCardRegistry | 적합 | 로컬 목록 필터에는 useDeferredValue를 유지하고 서버 검색과 역할을 분리함 |
UserCardTable | 수정 완료 | 펼친 행에서만 상세 컴포넌트와 query를 mount함 |
Login, My Page, Introduction
| 컴포넌트 | 판정 | 확인·수정 내용 |
|---|
GoogleLoginButton | 적합 | 이벤트 기반 OAuth 시작 구조 유지 |
LoginHero | 적합 | 정적 표현 컴포넌트로 유지 |
AccountManagementPanel | 적합 | 계정 액션만 전달받아 표현 |
AccountProfileCard | 적합 | 서버 상태를 복제하지 않고 props로 표현 |
CardStatusModePanel | 적합 | 공개 상태 mutation 책임이 페이지 훅에 유지됨 |
MyPageSkeleton | 적합 | 실제 레이아웃과 유사한 로딩 크기 유지 |
NfcCardPanel | 적합 | NFC 상태 표현 책임 유지 |
InstructionList | 적합 | 정적 목록 표현 |
IntroLogo | 적합 | 정적 로고 표현 |
ShareGuideBottomSheet | 적합 | 열림 상태를 상위에서 제어하는 controlled 구조 유지 |
TapleCardIllustration | 적합 | 정적 SVG로 유지 |
Shared UI
| 컴포넌트 | 판정 | 확인·수정 내용 |
|---|
AsyncState | 적합 | Error/NotFound 공통 표현 유지 |
BlockCard | 적합 | 단일 카드 표시 책임 유지 |
Button | 수정 완료 | 아이콘 직접 import 적용 |
ConfirmDialog | 적합 | 대화상자 상태를 외부에서 제어 |
Header | 수정 완료 | 아이콘 직접 import 적용 |
Icon | 적합 | 개별 아이콘 export 구조 유지 |
ImageSlider | 적합 | 현재 운영 사용처 없음. 추측성 리팩터링은 하지 않음 |
Input | 수정 완료 | 아이콘 직접 import 적용 |
Menu | 적합 | 메뉴 상태를 내부에 국소화 |
NameCard | 적합 | 표시 로직을 한 컴포넌트 경계에 유지 |
Profile | 적합 | 프로필 표현 책임 유지 |
Skeleton | 적합 | reduced-motion 대응 유지 |
Switch | 적합 | controlled input 구조 유지 |
Toast | 수정 완료 | 아이콘 직접 import 적용 |
재발 방지
운영 코드가 다시 @/shared/ui 또는 feature 최상위 배럴을 import하면 ESLint가 실패하도록 규칙을 추가했습니다. 테스트와 Storybook은 편의성을 위해 이 제한에서 제외합니다.
의도적으로 보류한 항목
| 항목 | 이유 | 후속 조건 |
|---|
마이페이지와 Live Studio의 /me/business-card query 통합 | 두 화면이 같은 endpoint에 서로 다른 응답 타입을 기대하고 있어 query key만 합치면 캐시 shape 충돌 위험이 있음 | 백엔드 응답 계약과 프론트 DTO를 하나로 통일한 뒤 진행 |
| 공개 명함 route loader prefetch | 공개 명함은 비공개 전환을 즉시 반영하려고 gcTime: 0, refetchOnMount: always 정책을 사용 중 | 공개 데이터 캐시·개인정보 신선도 정책을 먼저 확정한 뒤 검토 |
| 폰트 자산 축소 | 디자인 지정 폰트를 그대로 유지한다는 제품 결정을 존중 | 폰트 디자인 정책이 바뀔 때만 subset 검토 |
사용하지 않는 ImageSlider의 포인터 이벤트 리팩터링 | 운영 경로의 성능에 영향이 없고 현재 요청 범위를 벗어남 | 실제 사용 페이지가 생기고 프로파일링에서 병목이 확인될 때 진행 |
검증
pnpm quality:check: 타입 검사, ESLint, 47개 테스트 파일·236개 테스트, 3개 빌드 계약 테스트 통과
pnpm analyze: 번들 분석 빌드 통과
pnpm build: 클라이언트·SSR·prerender·공개 자산 계약 검증 통과
- 운영 산출물에서 QA 카드 생성 문구와 개발 도구 코드가 포함되지 않음을 확인