무거운 React 앱에서 Core Web Vitals 해결하기
Source: Dev.to
위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. (코드 블록, URL 및 마크다운 형식은 그대로 유지됩니다.)
Introduction
다양한 AI 도구, i18n, 개발자 포털 및 히어로 슬라이더를 포함한 무거운 React SPA는 종종 “Needs improvement” 페이지 속도 등급과 함께 Lighthouse 85 점수를 받습니다. 주요 원인은 큰 JavaScript 번들, 폰트, i18n 리소스, 그리고 네트워크와 메인 스레드 시간을 경쟁하는 화면 상단 이미지들로, 이로 인해 느린 LCP, 레이아웃 이동(CLS), 그리고 둔한 상호작용(INP)이 발생합니다. 자산이 로드되고 렌더링되는 방식을 작은, 목표 지향적인 변경으로 조정하면 큰 효과를 볼 수 있습니다.
LCP 이미지 우선순위 지정
히어로 이미지는 보통 LCP 후보입니다. 브라우저에 높은 우선순위로 미리 로드하도록 지시하세요:
<!-- Example preload markup (replace with actual image URL) -->
<link rel="preload" as="image" href="/path/to/hero.jpg" fetchpriority="high">
- 첫 번째 슬라이드 (LCP):
loading="eager"및fetchpriority="high". - 이후 슬라이드:
loading="lazy"로 설정하여 화면에 보일 때만 로드됩니다.
폰트 로딩 최적화
폰트가 텍스트 렌더링을 차단합니다. WOFF2 파일을 미리 로드하고 font-display: swap을 사용하여 보이지 않는 텍스트를 방지하세요:
<link rel="preload" href="https://fonts.gstatic.com/s/inter/.../Inter.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: 'Inter';
src: url('https://fonts.gstatic.com/s/inter/.../Inter.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* 폰트가 로드될 때까지 대체 텍스트를 표시 */
}
레이아웃 이동 방지
레이아웃 이동은 초기 레이아웃이 계산된 후에 콘텐츠가 나타날 때 발생합니다. 미리 공간을 확보하세요.
이미지에 aspect-ratio 사용
{/* Image component */}
<img
src={src}
alt={alt}
style={{ aspectRatio: `${width}/${height}` }}
loading="eager"
fetchpriority="high"
/>
- 이미지에는 항상
width와height(또는aspect-ratio)를 설정합니다. - 래퍼가 이미지가 로드되기 전 레이아웃을 안정적으로 유지합니다.
지연 로드 콘텐츠용 플레이스홀더
{!isLoaded && (
<div className="placeholder" style={{ width, height }} />
)}
<img
src={src}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
- 플레이스홀더가 공간을 확보합니다.
- 불투명도 전환을 사용해 시각적 점프를 방지합니다.
라우트 지연 로딩 및 무거운 기능
// Lazy imports for AI tools, content pages, dashboards
const LazyFaceShapeDetector = lazy(() => import('../pages/ai-tools/FaceShapeDetector'));
const LazyDeveloperPortal = lazy(() => import('../pages/DeveloperPortal'));
- 현재 라우트에 필요한 것만 로드합니다.
- 메인 스레드가 유휴 상태일 때 다음 라우트를 미리 가져옵니다:
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('../pages/NextRoute');
});
}
비핵심 작업 연기
- 분석 및 보조 API 호출을 중요 경로에서 분리합니다.
- 첫 번째 페인트에 필요하지 않은 작업은
requestIdleCallback또는 가벼운 스케줄러를 사용합니다.
i18n 및 번들 분할
- 기본 로케일을 초기 로드하고, 다른 로케일은 필요에 따라 지연 로드합니다.
- Vite에서는
manualChunks를 사용해 벤더 번들을 분할하고, i18n과 UI 라이브러리를 별도 청크로 유지합니다.
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('i18next')) return 'i18n';
if (id.includes('react')) return 'react-vendor';
}
}
}
}
}
});
테스트 및 목표
- 시크릿(Incognito) 창에서 Lighthouse 실행.
- 실제 환경을 위해 WebPageTest 사용.
- 실제 사용자 모니터링(RUM)을 위해 web‑vitals 라이브러리 고려.
Core Web Vitals 목표
| 지표 | 목표 |
|---|---|
| LCP | < 2.5 초 |
| CLS | < 0.1 |
| INP | < 200 ms |
Summary of Improvements
| 영역 | 해야 할 일 |
|---|---|
| LCP | LCP 이미지를 preload하고 fetchpriority="high"를 설정하며 폰트를 최적화합니다. |
| CLS | aspect-ratio 또는 명시적인 width/height를 사용하고, 레이아웃을 안정적으로 유지하기 위해 플레이스홀더를 적용합니다. |
| INP | 라우트와 무거운 기능을 lazy‑load하고, 유휴 시간에 prefetch하며, 비핵심 작업은 defer합니다. |
이러한 패턴을 FaceAura AI(React, Vite, Express 로 구축된 AI 기반 스타일 및 분석 앱)에 적용한 결과 눈에 띄는 성능 향상이 나타났습니다. 동일한 기법은 모든 대형 React SPA에도 적용할 수 있습니다.
빠른 시작
- LCP 이미지를 식별하고 미리 로드합니다.
font-display: swap을 사용하여 글꼴을 미리 로드합니다.aspect-ratio/width‑height와 위‑폴드 미디어용 플레이스홀더를 추가합니다.- 라우트와 무거운 모듈을 지연 로드하고, 유휴 시간에 다음에 사용할 가능성이 높은 라우트를 사전 가져옵니다.
이 단계들은 일반적으로 Core Web Vitals에 가장 큰 영향을 줍니다.