React 사이트의 Core Web Vitals(이하 CWV)는 한 번 90점을 받기는 어렵지 않아요. 어려운 건 운영 6개월 뒤에도 90점을 유지하는 일이에요. 새 컴포넌트가 추가되고, 마케팅 픽셀이 붙고, 디자이너가 폰트 한 종류를 더 얹는 사이에 점수는 천천히 무너져요.
CWV 는 Google 의 랭킹 신호이자 동시에 사용자 이탈을 가장 솔직하게 보여주는 지표예요. LCP 가 4초 → 2.5초로 떨어지면 모바일 이탈률이 평균 24% 줄어든다는 Chrome UX Report 데이터가 있을 정도예요. 이 글에서는 React 사이트가 90점 라인을 깨지 않게 만드는 6가지 실전 패턴을 다뤄요.
1. LCP · 가장 큰 컨텐츠는 첫 HTML 에 같이 보내라
LCP(Largest Contentful Paint)는 보통 첫 화면의 큰 이미지 또는 헤드라인 텍스트예요. React 의 가장 흔한 LCP 깨짐 원인은 JS 가 다 로드되어야 화면이 그려지는 SPA 패턴이에요.
- SSG/SSR 또는 정적 HTML로 LCP 후보를 첫 응답에 같이 보내세요. Next.js, Astro, Remix 모두 가능. SPA 만 고집하면 LCP 는 JS 다운로드 시간 + 파싱 시간 만큼 밀려요.
- 히어로 이미지는
<link rel="preload" as="image">로 미리 가져오세요. 모바일에서 LCP 가 1.5~2.0 초 빨라져요. - 이미지는
fetchpriority="high"+ WebP/AVIF + 적절한srcset. 같은 비주얼이라도 페이로드 60% 절감이 흔해요.
Chrome DevTools > Performance 탭에서 LCP 마커를 확인하세요. LCP 요소가 <img> 인데 네트워크 워터폴에서 JS 번들 이후에 시작한다면, preload 또는 priority hint 를 적용할 수 있는 자리예요.
2. INP · 메인 스레드를 50ms 이상 잡지 말 것
2024년 3월부터 INP(Interaction to Next Paint)가 FID 를 대체했어요. 즉 클릭 한 번에 200ms 안에 시각적 피드백이 와야 한다는 뜻이에요. React 에서 INP 가 깨지는 가장 흔한 패턴 3가지:
- 거대한 리스트의 동기 렌더. 1,000개 이상의 카드를 한 번에 렌더하면 첫 클릭부터 INP 깨져요.
useTransition+ 가상화(virtualization)로 해결. - useEffect 안에서 무거운 동기 계산. 차트 데이터 가공·정렬·필터를 클라이언트에서 매번 돌리면 인터랙션마다 50ms+ 가 누수돼요.
useMemo로 메모이즈하거나 Web Worker 로 옮기세요. - 전역 리렌더 유발 컨텍스트. Auth context 가 바뀔 때마다 모든 페이지가 리렌더되면 클릭 한 번이 무거워져요. 컨텍스트를 분할(split)하세요.
3. CLS · 폰트와 이미지가 만드는 밀림 현상
CLS(Cumulative Layout Shift)는 한국 사이트에서 가장 자주 깨져요. 두 가지 주범:
- 웹폰트 FOUT/FOIT. Pretendard 같은 한국어 폰트는 파일 크기 때문에 로딩이 늦은데, 시스템 폰트와 글자 폭이 달라서 폰트가 바뀌는 순간 텍스트 블록 전체가 밀려요.
font-display: swap은 기본,size-adjust·ascent-override로 시스템 폰트 메트릭을 본 폰트에 맞추세요. - 이미지에 width/height 미지정. 모든
<img>와 임베드(YouTube, 광고)는 명시적 가로/세로 또는aspect-ratio가 있어야 해요. 안 그러면 이미지가 도착할 때마다 화면이 점프해요.
마케팅팀이 새 픽셀이나 위젯을 붙이는 순간 CLS 가 깨지는 경우도 많아요. <script async> 로 후순위 로드하거나, IntersectionObserver 로 viewport 진입 시에만 활성화하세요.
4. 번들 · route-level 코드 스플리팅과 prefetch
React 18 + Vite/Next 환경에서는 페이지 단위 코드 스플리팅이 기본이지만, 실제로는 한 번의 import 가 부주의하게 거대 라이브러리를 끌어와 번들이 부풀어요.
- 차트(Chart.js, Recharts), 마크다운 렌더러, 코드 하이라이터 같은 무거운 라이브러리는 해당 컴포넌트가 실제 화면에 진입할 때 dynamic import 하세요.
- 아이콘 라이브러리는 named import 가 tree-shaking 되는지 확인하세요.
import { ChevronDown } from 'lucide-react'는 OK,import * as Icons는 NG. <link rel="prefetch">또는 React Router 의 prefetch 로 다음 페이지 번들을 hover 시점에 가져오세요. 클릭 시 체감 속도가 다른 사이트가 돼요.
npx vite-bundle-visualizer 또는 @next/bundle-analyzer 로 번들을 그래프로 보세요. 첫 번들이 200KB(gzip) 를 넘으면 어딘가에서 끌어오는 무거운 패키지가 있어요.
5. 폰트 로딩 · 한국어 사이트의 함정
Pretendard, Gothic A1 같은 한국어 가변 폰트는 5~8MB 짜리 파일이에요. 모두를 받아오면 LCP 도 CLS 도 다 깨져요. 해결 패턴:
- dynamic-subset CSS 사용.
pretendardvariable-dynamic-subset.min.css는 페이지에 실제로 등장한 글자만 다운로드해요. 페그시티 사이트도 이 방식. - Latin 글리프는 다른 폰트로 분리. 한글은 Pretendard, 영문/숫자는 Inter 식으로
unicode-range로 라우팅하면 본문에서 영문이 한글보다 굵게 보이는 시각 불균형이 사라져요. preload는 본문 폰트만, 디스플레이 전용 폰트(Poppins, Gothic A1)는 그대로 lazy 로드하세요.
6. 측정 · CrUX 기다리지 말고 RUM 직접 박을 것
PageSpeed Insights 의 점수는 라이브러리 함수 한 줄 잘 쓴 결과일 뿐, 실제 사용자 환경에서 깨지는 지점은 안 보여요. CrUX 는 28일 누적이라 회귀를 늦게 발견해요. 운영 사이트라면 RUM(Real User Monitoring)을 직접 박아야 해요.
web-vitals라이브러리 +navigator.sendBeacon으로 LCP/INP/CLS 를 자체 엔드포인트에 쏘세요. 한 달이면 어떤 페이지가 어떤 디바이스에서 깨지는지 보여요.- 또는 Vercel Speed Insights, Cloudflare Web Analytics 같은 무료 RUM 을 켜세요. 1줄 추가로 충분.
- 매주 KPI 대시보드에 P75 LCP/INP/CLS 를 함께 보세요. 평균이 아닌 P75 가 Google 이 평가하는 값이에요.
회귀를 막는 운영 체크리스트
새 PR 이 머지될 때마다 자동으로 돌아야 할 검사:
- Lighthouse CI 를 GitHub Actions 에 붙여 PR 별 점수 비교(임계값 90)
- 번들 크기 변동 알림(
size-limit, 임계값 10% 증가) - 새 의존성이 5KB(gzip) 이상이면 PR 코멘트로 경고
- 월 1회 RUM 데이터로 P75 회귀 페이지 리뷰
- CWV 90점은 한 번 받기는 쉽고 유지하기는 어려운 지표예요. 운영 체계가 따라줘야 깨지지 않아요.
- React 의 LCP 깨짐은 보통 SPA 패턴 + 거대 JS 번들 + 늦은 이미지 로딩 세 가지 조합에서 발생해요.
- INP 는 메인 스레드 블로킹이 핵심. useTransition, 메모이즈, 컨텍스트 분할로 잡아요.
- 한국어 사이트는 웹폰트 CLS가 가장 흔한 함정. dynamic subset + size-adjust 로 막으세요.
- 측정은 P75 RUM으로. PageSpeed 점수만 보고 안심하면 운영 6개월에 점수가 무너져요.
페그시티의 무료 SEO 진단은 Technical 영역에서 LCP·INP·CLS 점수와 함께 어떤 자산이 점수를 깎고 있는지를 함께 알려드려요. 60초 안에 요약을 받아보실 수 있어요.
함께 성장할
준비가 되셨나요?
문의 폼을 남겨주시면 영업일 기준 24시간 안에 회신해 드릴게요. 통화가 더 편하시면 오른쪽에서 15분 비디오 미팅을 바로 예약해 주세요.
정리해요.