웹사이트 성능 최적화: 실전 팁 모음
웹사이트 성능은 사용자 경험과 검색 순위 모두에 직접적인 영향을 미칩니다. 인디 개발자로서 제가 실무에서 반복적으로 적용하는 최적화 기법들을 정리했습니다. 이론보다 실전 위주로, 지금 당장 프로젝트에 적용할 수 있는 팁들을 모았습니다.
Core Web Vitals 이해하기
Google이 정의한 Core Web Vitals는 세 가지 핵심 지표로 구성됩니다.
LCP (Largest Contentful Paint) — 페이지에서 가장 큰 콘텐츠 요소가 화면에 렌더링되기까지 걸리는 시간입니다. 이미지, 비디오, 큰 텍스트 블록이 대상이 되며, 2.5초 이내를 목표로 해야 합니다. LCP를 개선하려면 서버 응답 시간을 줄이고, 렌더링 차단 리소스를 제거하며, 히어로 이미지를 미리 로드하는 것이 효과적입니다.
FID (First Input Delay) — 사용자가 페이지와 처음 상호작용했을 때 브라우저가 이벤트 핸들러를 실행하기까지의 지연 시간입니다. 현재는 INP(Interaction to Next Paint)로 대체되고 있으며, 200ms 이내가 권장됩니다. 메인 스레드를 장시간 점유하는 JavaScript 작업을 분할하는 것이 핵심입니다.
CLS (Cumulative Layout Shift) — 페이지 로드 중 예기치 않게 레이아웃이 이동하는 현상의 누적 점수입니다. 0.1 이하를 유지해야 합니다. 이미지와 광고 영역에 명시적인 크기를 지정하고, 웹 폰트 로드 시 FOUT을 관리하면 크게 개선됩니다.
이미지 최적화
이미지는 대부분의 웹사이트에서 가장 큰 용량을 차지합니다. 다음 전략을 조합하면 극적인 효과를 볼 수 있습니다.
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="히어로 이미지" width="1200" height="600" loading="lazy" />
</picture>
AVIF 포맷은 WebP 대비 평균 20~30% 더 작은 파일 크기를 제공합니다. <picture> 태그로 포맷 폴백을 구성하면 브라우저 호환성을 유지하면서 최신 포맷의 이점을 누릴 수 있습니다. 반드시 width와 height 속성을 명시하여 CLS를 방지하세요.
반응형 이미지의 경우 srcset과 sizes 속성을 활용합니다. 뷰포트에 따라 적절한 크기의 이미지를 제공하면 모바일 환경에서 불필요하게 큰 이미지를 다운로드하는 일을 방지할 수 있습니다.
코드 스플리팅과 레이지 로딩
코드 스플리팅은 JavaScript 번들을 여러 청크로 나누어 초기 로드 시 필요한 코드만 전송하는 기법입니다.
// React에서의 동적 임포트
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
);
}
라우트 기반 스플리팅은 가장 기본적인 접근입니다. Next.js나 Remix 같은 프레임워크는 이를 자동으로 처리하지만, 라우트 안에서도 무거운 컴포넌트(차트 라이브러리, 에디터 등)는 동적 임포트로 분리하는 것이 좋습니다.
레이지 로딩은 이미지뿐 아니라 iframe, 비디오, 심지어 컴포넌트 단위로도 적용할 수 있습니다. Intersection Observer API를 활용하면 뷰포트에 진입하는 시점에 리소스를 로드하도록 세밀하게 제어할 수 있습니다.
폰트 최적화
웹 폰트는 렌더링 차단의 주요 원인 중 하나입니다.
@font-face {
font-family: 'Pretendard';
src: url('/fonts/Pretendard-Regular.subset.woff2') format('woff2');
font-display: swap;
unicode-range: U+AC00-D7A3; /* 한글 범위 */
}
font-display: swap을 사용하면 폰트 로드 전에 시스템 폰트로 먼저 텍스트를 표시하여 FOIT(Flash of Invisible Text)를 방지합니다. 한글 폰트는 용량이 크므로 서브셋팅이 필수입니다. 실제 사용하는 글리프만 포함한 서브셋 폰트를 만들면 수 MB짜리 폰트를 수백 KB로 줄일 수 있습니다.
<link rel="preload">를 사용해 중요 폰트를 미리 로드하는 것도 효과적입니다.
캐싱 전략
적절한 캐싱 전략은 재방문 사용자의 로딩 속도를 획기적으로 개선합니다.
정적 자산(JS, CSS, 이미지)에는 콘텐츠 해시를 파일명에 포함시키고 Cache-Control: public, max-age=31536000, immutable 헤더를 설정합니다. 파일 내용이 바뀌면 해시가 달라지므로 캐시 무효화가 자동으로 이루어집니다.
HTML 파일에는 Cache-Control: no-cache 또는 짧은 max-age를 설정하여 항상 최신 버전을 제공합니다. Service Worker를 활용하면 오프라인 지원과 함께 더 세밀한 캐싱 전략을 구현할 수 있습니다.
번들 분석과 성능 측정
최적화의 시작은 측정입니다. 저는 다음 도구들을 주로 사용합니다.
- Lighthouse: Chrome DevTools에 내장된 종합 성능 감사 도구
- WebPageTest: 다양한 네트워크 조건에서의 실제 로딩 성능 측정
- Bundle Analyzer: webpack-bundle-analyzer 또는 @next/bundle-analyzer로 번들 구성 시각화
번들 분석을 통해 불필요하게 큰 의존성을 발견하고 대체하는 것만으로도 큰 개선을 이룰 수 있습니다. 예를 들어 moment.js를 day.js로 교체하면 번들 크기를 수십 KB 줄일 수 있습니다.
CDN 활용과 크리티컬 렌더링 경로
CDN은 정적 자산을 사용자와 가까운 엣지 서버에서 제공합니다. Vercel, Cloudflare, AWS CloudFront 등을 활용하면 전 세계 어디서든 빠른 응답 속도를 보장할 수 있습니다.
크리티컬 렌더링 경로 최적화는 초기 렌더링에 필요한 리소스만 먼저 로드하는 전략입니다. 인라인 크리티컬 CSS를 <head>에 삽입하고, 나머지 CSS는 비동기로 로드합니다. JavaScript에는 defer 또는 async 속성을 적절히 사용하여 렌더링 차단을 방지합니다.
마무리
성능 최적화는 한 번에 끝나는 작업이 아니라 지속적으로 모니터링하고 개선하는 과정입니다. Core Web Vitals를 정기적으로 확인하고, 새로운 기능을 추가할 때마다 성능 영향을 함께 고려하는 습관을 들이는 것이 중요합니다. 작은 개선이 쌓여 큰 차이를 만든다는 점을 기억하세요.