프런트엔드 성능 최적화 실전: Core Web Vitals 만점 공략

Lighthouse 점수는 60점인데 일주일 안에 90점까지 올려야 합니다. Chrome DevTools를 열고 LCP, FID, CLS 같은 지표가 무엇인지부터 파악해야 합니다. 프런트엔드 개발자에게 성능 최적화는 흔히 갑자기 추가되는 업무입니다.
시행착오도 적지 않았습니다. 처음 최적화할 때 모든 이미지에 지연 로딩을 적용했다가 오히려 LCP 점수가 떨어졌습니다. 첫 화면의 큰 이미지는 애초에 지연 로딩하면 안 됩니다. 또 한 번은 Service Worker 캐시 전략을 이틀 동안 연구했지만 점수는 고작 2점 올랐습니다. 이 글은 이론을 설명하는 입문서가 아니라 2주 안에 실제 효과를 볼 수 있는 실전 가이드입니다. 어떤 최적화의 ROI가 가장 높은지, 어떤 함정을 피해야 하는지, 우선순위에 따라 Lighthouse 점수를 60점에서 90점 이상으로 높이는 방법을 살펴봅니다.
1장: Core Web Vitals의 세 가지 핵심 지표 이해하기
영문 약어에 겁먹을 필요는 없습니다. 쉽게 말하면 로딩이 빠른가, 클릭에 잘 반응하는가, 화면이 흔들리지 않는가를 보는 지표입니다.
Core Web Vitals란 무엇인가
Google은 2020년에 세 가지 사용자 경험 지표를 발표했습니다. 이 지표는 사용자 경험뿐 아니라 SEO 순위에도 직접 영향을 줍니다. 콘텐츠가 아무리 좋아도 성능이 나쁘면 순위는 떨어집니다. 2024년 3월에는 중요한 변화도 있었습니다. INP가 FID를 대체해 핵심 지표가 됐습니다. 아직 FID를 최적화하고 있다면 이미 시대에 뒤처진 셈입니다.
LCP - 최대 콘텐츠 렌더링
LCP는 페이지의 주요 콘텐츠가 로드되는 속도를 측정합니다. 대개 첫 화면의 큰 이미지, 제목 또는 동영상이 대상입니다. 기준은 다음과 같습니다.
- 2.5초 미만: 좋음(초록색)
- 2.5~4초: 개선 필요(노란색)
- 4초 초과: 나쁨(빨간색)
저는 이를 식당에 비유하곤 합니다. LCP는 식당에서 메인 요리가 나오는 속도와 같습니다. 10분을 기다려도 메인 요리가 나오지 않으면 당연히 불쾌하겠죠?
실제 데이터도 있습니다. 페이지 로딩 시간이 3초에서 5초로 늘어나면 이탈률은 38% 증가합니다. 모바일에서는 로딩이 3초를 넘으면 사용자의 53%가 바로 이탈합니다. LCP를 잘 최적화하면 전환율을 7~15% 높일 수 있습니다.
INP - 다음 페인트와의 상호작용
INP는 2024년 3월에 FID를 공식적으로 대체한 새로운 지표입니다. 클릭, 키보드 입력, 터치 등 사용자 상호작용의 전체 응답 주기를 측정합니다. 기준은 다음과 같습니다.
- 200ms 미만: 좋음(초록색)
- 200~500ms: 개선 필요(노란색)
- 500ms 초과: 나쁨(빨간색)
Google은 왜 FID를 교체했을까요? FID는 ‘최초 입력 지연’만 측정하지만 INP는 전체 상호작용 과정을 다루기 때문입니다. 주문에 비유하면 FID는 직원이 내 말을 들었는지만 확인하고, INP는 주문한 시점부터 음식이 테이블에 나올 때까지의 전 과정을 확인합니다.
한 프로젝트에서 처음 확인한 INP는 650ms였습니다. 버튼 하나를 눌러도 0.5초 넘게 기다려야 했으니 사용자가 버벅인다고 말할 만했습니다. 원인은 YouTube 자동 삽입이었고, 이를 제거하자 INP가 220ms로 줄어 사용자가 확실히 더 부드럽다고 느꼈습니다.
CLS - 누적 레이아웃 이동
CLS는 페이지의 시각적 안정성을 측정합니다. 어떤 버튼을 누르려는 순간 페이지가 갑자기 움직여 광고를 누른 경험이 있나요? 바로 CLS가 일으키는 문제입니다. 기준은 다음과 같습니다.
- 0.1 미만: 좋음(초록색)
- 0.1~0.25: 개선 필요(노란색)
- 0.25 초과: 나쁨(빨간색)
처음 CLS가 0.5인 것을 보고 0.5면 작다고 생각했지만, 나중에 이것이 이미 심각한 수준이라는 걸 알았습니다. 흔한 CLS 문제로는 너비와 높이를 지정하지 않은 이미지, 갑자기 삽입되는 광고, 글꼴 깜빡임(FOIT)이 있습니다.
2장: LCP 최적화 - ROI가 가장 높은 성능 최적화
제가 LCP를 최우선으로 두는 이유는 다음과 같습니다.
- 영향이 가장 직접적임: 사용자가 즉시 체감합니다.
- 최적화 여지가 가장 큼: 흔히 5초인 LCP를 2초 미만으로 줄일 수 있습니다.
- 기술 방안이 성숙함: 이미지 최적화와 CDN 가속은 이미 성숙한 방법입니다.
- ROI가 가장 높음: 적게 투입해 빠르게 효과를 내므로 가성비가 가장 좋습니다.
Core Web Vitals 전체 최적화 과정
LCP, INP, CLS를 아우르는 전체 최적화 방안으로 2주 안에 Lighthouse 점수를 60점에서 90점 이상으로 높입니다.
Estimated time: PT2W
-
1
Step 1: P0 우선순위: 이미지 최적화(LCP)
이미지 최적화는 LCP 문제의 70% 이상을 차지해 ROI가 가장 높습니다. -
2
Step 2: P0 우선순위: 서드파티 스크립트 지연(INP)
서드파티 스크립트는 INP의 주요 병목입니다. -
3
Step 3: P0 우선순위: 이미지 너비와 높이 지정(CLS)
CLS는 가장 쉽게 고칠 수 있는 지표입니다. -
4
Step 4: P1 우선순위: 코드 분할과 리소스 최적화
코드 분할 및 리소스 최적화 방법입니다. -
5
Step 5: P1 우선순위: 글꼴 및 서버 최적화
글꼴과 서버 최적화 방법입니다. -
6
Step 6: P2 우선순위: 고급 최적화
ROI는 보통이고 난이도는 높은 고급 최적화입니다.
1. 이미지 최적화(ROI: ⭐⭐⭐⭐⭐)
가장 중요한 최적화 항목으로, LCP 문제의 70% 이상을 차지합니다. 저는 성능을 최적화할 때마다 이미지부터 시작합니다.
a) 최신 이미지 형식 사용
이미지 형식을 얕보지 마세요. JPEG를 AVIF로 바꾸면 이미지 한 장에서 300KB를 줄일 수 있습니다.
<!-- 방법 1: <picture> 태그를 사용해 브라우저가 최적 형식을 자동 선택 -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image"> <!-- fallback, 구형 브라우저용 -->
</picture>
// 방법 2: Next.js 자동 최적화(권장)
import Image from 'next/image'
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // 중요: LCP 이미지에는 반드시 priority를 적용하고 지연 로딩하지 마세요!
/>
실제 데이터를 보겠습니다.
- AVIF는 JPEG보다 압축률이 41% 더 높습니다.
- WebP는 JPEG보다 압축률이 30% 더 높습니다.
- 실전 사례: 한 전자상거래 홈페이지의 hero 이미지를 500KB에서 120KB(AVIF 형식)로 줄였더니 LCP가 4.2초에서 2.1초로 절반이 됐습니다.
b) 이미지 지연 로딩(LCP 이미지 제외)
여기에는 제가 직접 겪은 함정이 있습니다. LCP 이미지까지 지연 로딩했더니 오히려 점수가 떨어졌습니다.
<!-- ❌ 잘못된 예: LCP 이미지를 지연 로딩하지 마세요! -->
<img src="hero.jpg" loading="lazy">
<!-- ✅ 올바른 방법: 첫 화면의 큰 이미지는 eager, 나머지 이미지만 지연 로딩 -->
<img src="hero.jpg" loading="eager"> <!-- LCP 이미지 -->
<img src="product1.jpg" loading="lazy"> <!-- 아래쪽 이미지만 지연 로딩 -->
<img src="product2.jpg" loading="lazy">
기억하세요. 첫 화면에 보이는 이미지는 단 한 장도 지연 로딩하면 안 됩니다. 지연 로딩은 첫 화면 밖의 이미지에 사용하는 기능입니다.
c) 반응형 이미지
모바일 사용자는 데스크톱용 큰 이미지를 로드할 필요가 없습니다. srcset을 사용하면 트래픽을 60% 절감할 수 있습니다.
<img
src="hero-800w.jpg"
srcset="hero-400w.jpg 400w,
hero-800w.jpg 800w,
hero-1200w.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
alt="Hero"
/>
실제 효과: 모바일 사용자가 1200w 대신 400w 이미지를 로드하면 LCP를 1초 단축할 수 있습니다.
d) 이미지 CDN과 미리 로드
CDN은 사치품이 아니라 필수품입니다. Alibaba Cloud OSS도 1년에 수십 위안이면 사용할 수 있습니다.
<!-- 핵심 이미지를 Preload해 브라우저가 우선 로드하도록 함 -->
<link rel="preload" as="image" href="hero.jpg">
<!-- 이미지 CDN 사용(형식 자동 변환 + 압축) -->
<!-- Tencent Cloud COS, Alibaba Cloud OSS, Cloudflare Images 모두 지원 -->
<img src="https://cdn.example.com/hero.jpg?x-oss-process=image/format,webp/quality,80">
e) 이미지 크기와 압축
권장 도구:
- TinyPNG: 간단하고 직관적인 온라인 압축 도구
- ImageOptim: Mac에서 일괄 압축하기 좋은 도구
- Squoosh: Google에서 만든 AVIF 지원 도구
압축 규칙: - 첫 화면의 큰 이미지는 품질 80%(시각적 차이가 매우 작음)
- 나머지 이미지는 70%(충분한 수준)
- 크기는 디자인 시안 너비의 2배(고해상도 화면 대응)
f) 큰 이미지를 Base64로 인라인 처리하지 않기
// ❌ 큰 이미지(10KB 초과)를 인라인으로 넣지 마세요.
// HTML 크기가 커져 렌더링이 지연됩니다.
const heroImage = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...' // 500KB
// ✅ 작은 아이콘(10KB 미만)에만 Base64를 사용하세요.
// 예: loading 아이콘, 단순한 SVG 아이콘
const icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR...' // 2KB
2. 서버 응답 시간 최적화(ROI: ⭐⭐⭐⭐)
a) CDN 사용
모든 정적 리소스를 CDN에 올리고, HTML도 SSG/ISR와 함께 CDN에 캐시할 수 있습니다. TTFB를 200ms에서 50ms로 줄인 사례를 본 적이 있는데 사용자가 차이를 확실히 체감했습니다.
b) 서버 사이드 렌더링(SSR) 또는 정적 생성(SSG)
// Next.js - 정적 생성(우선 선택, 성능이 가장 좋음)
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // ISR: 60초 후 다시 생성
}
}
// 또는 서버 사이드 렌더링(데이터 실시간성이 중요할 때 사용)
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
c) 데이터베이스 쿼리 최적화
- 인덱스 추가(explain 분석을 잊지 마세요)
- Redis로 자주 쓰는 데이터 캐시
- N+1 쿼리 방지(join 또는 dataloader 사용)
3. 리소스 로딩 최적화(ROI: ⭐⭐⭐)
a) 핵심 CSS 인라인 처리
<!-- 첫 화면의 핵심 CSS를 <head>에 인라인으로 넣어 렌더링 차단 방지 -->
<style>
.hero {
width: 100%;
height: 600px;
background: #f0f0f0;
}
.nav {
position: fixed;
top: 0;
width: 100%;
}
</style>
<!-- 중요하지 않은 CSS 지연 로딩 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
b) 글꼴 최적화
/* font-display로 FOIT(Flash of Invisible Text) 방지 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* fallback 글꼴을 즉시 표시해 빈 화면 방지 */
}
<!-- 핵심 글꼴 Preload -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
3장: INP 최적화 - 더 부드러운 상호작용 만들기
서드파티 스크립트는 INP의 가장 큰 적입니다! 가장 심한 사례로, 한 페이지에서 Google Analytics, Facebook Pixel, 고객 지원 플러그인, 광고 스크립트 등 서드파티 스크립트 12개를 로드해 INP가 바로 800ms를 넘은 적도 있습니다.
1. JavaScript 실행 최적화(ROI: ⭐⭐⭐⭐)
a) 코드 분할(Code Splitting)
코드 분할은 거창하게 들리지만 실제로는 ‘필요할 때 로드하기’입니다. 사용자가 특정 페이지를 열 때에만 그 페이지의 코드를 로드하는 방식입니다.
// React - 라우트 단위 코드 분할
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))
const Profile = lazy(() => import('./Profile'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}
// Vue 3 - 비동기 컴포넌트도 지원
const Dashboard = defineAsyncComponent(() => import('./Dashboard.vue'))
실제 효과: 한 관리자 페이지를 1.2MB에서 첫 화면 200KB로 줄여 INP를 450ms에서 180ms로 단축했습니다. 사용자가 페이지를 여는 속도가 두 배 이상 빨라졌습니다.
b) 긴 작업 분할
메인 스레드를 50ms 넘게 차단하면 긴 작업으로 간주되며 INP가 급격히 높아집니다.
// ❌ 메인 스레드를 차단하는 긴 작업(멈춤 현상 발생)
function processLargeData(data) {
for (let i = 0; i < 10000; i++) {
// 복잡한 연산으로 1만 개 항목을 한 번에 처리
heavyCalculation(data[i])
}
}
// ✅ requestIdleCallback으로 작은 작업으로 분할
function processLargeData(data) {
let index = 0
function processChunk() {
const chunkSize = 100 // 한 번에 100개 처리
let count = 0
while (index < data.length && count < chunkSize) {
heavyCalculation(data[index])
index++
count++
}
if (index < data.length) {
// 아직 끝나지 않았다면 다음 유휴 시간에 계속 처리
requestIdleCallback(processChunk)
}
}
requestIdleCallback(processChunk)
}
c) Web Workers 사용
복잡한 연산을 Worker로 옮기면 메인 스레드를 차단하지 않습니다.
// worker.js
self.onmessage = (e) => {
const result = complexCalculation(e.data) // Worker에서 복잡한 연산 실행
self.postMessage(result)
}
// main.js
const worker = new Worker('worker.js')
worker.postMessage(data)
worker.onmessage = (e) => {
console.log('Result:', e.data)
// 결과를 받아 UI 업데이트
}
2. 서드파티 스크립트 최적화(ROI: ⭐⭐⭐⭐⭐)
서드파티 스크립트는 손님을 초대해 식사하는 것과 같습니다. 사람이 많이 올수록 더 혼잡해지고 각 스크립트가 리소스를 차지하려 듭니다.
a) 중요하지 않은 스크립트 지연 로딩
<!-- ❌ 로딩 차단(페이지 렌더링을 막음) -->
<script src="analytics.js"></script>
<!-- ✅ 페이지 로딩이 끝날 때까지 지연 -->
<script defer src="analytics.js"></script>
<!-- ✅ 또는 수동으로 3초 지연(더 적극적인 방법) -->
<script>
window.addEventListener('load', () => {
setTimeout(() => {
const script = document.createElement('script')
script.src = 'analytics.js'
document.body.appendChild(script)
}, 3000) // 사용자가 3초 동안 페이지를 본 뒤 분석 스크립트 로드
})
</script>
b) Facade 패턴으로 무거운 컴포넌트 지연 로딩
YouTube 동영상 embed는 1MB가 넘으므로 바로 로드하면 INP가 크게 느려집니다.
// YouTube 경량 썸네일을 표시하고 클릭할 때 실제 동영상 로드
function VideoFacade({ videoId }) {
const [showVideo, setShowVideo] = useState(false)
if (!showVideo) {
return (
<div
className="video-facade"
style={{
backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
cursor: 'pointer'
}}
onClick={() => setShowVideo(true)}
>
<button className="play-button">▶ 동영상 재생</button>
</div>
)
}
return <iframe src={`https://www.youtube.com/embed/${videoId}`} />
}
실제 효과: 한 블로그 페이지에서 YouTube 자동 embed를 제거해 INP를 650ms에서 220ms로 줄였습니다.
3. 이벤트 처리 최적화(ROI: ⭐⭐⭐)
a) 디바운스와 스로틀링
import { debounce, throttle } from 'lodash'
// 검색창 디바운스(사용자가 입력을 멈추고 300ms 뒤에 실행)
const handleSearch = debounce((value) => {
fetchSearchResults(value)
}, 300)
// 스크롤 이벤트 스로틀링(100ms마다 최대 한 번 실행)
const handleScroll = throttle(() => {
updateScrollPosition()
}, 100)
b) Passive 이벤트 리스너 사용
// preventDefault를 호출하지 않는다고 브라우저에 알려 스크롤 성능 향상
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('touchmove', handleTouch, { passive: true })
4장: CLS 최적화 - 화면 흔들림 방지하기
CLS는 책을 읽는 도중 누군가 갑자기 책을 위로 밀어 방금 읽던 줄을 다시 찾아야 하는 상황과 같습니다. 무척 성가시죠. 다행히 CLS는 가장 쉽게 고칠 수 있습니다.
1. 이미지와 동영상 크기 지정(ROI: ⭐⭐⭐⭐⭐)
이미지에 너비와 높이를 지정하지 않는 것은 CLS의 가장 큰 원인이지만, 동시에 가장 쉽게 고칠 수 있는 문제입니다.
<!-- ❌ 너비와 높이를 지정하지 않아 로딩할 때 흔들림 발생 -->
<img src="photo.jpg" alt="Photo">
<!-- ✅ 너비와 높이를 지정해 브라우저가 미리 공간을 확보 -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- ✅ 또는 CSS aspect-ratio 사용(최신 브라우저 지원) -->
<style>
img {
width: 100%;
aspect-ratio: 16 / 9;
}
</style>
실제 사례: 한 뉴스 사이트가 모든 이미지에 너비와 높이를 추가해 CLS를 0.35에서 0.05로 줄였습니다. 이렇게 간단합니다.
2. 글꼴 로딩 최적화(ROI: ⭐⭐⭐⭐)
a) font-display: swap 사용
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* FOIT(글꼴 로딩 중 빈 화면) 방지 */
}
b) 핵심 글꼴 Preload
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
c) 시스템 글꼴 또는 Variable Font 사용
/* 시스템 글꼴은 로드할 필요가 없어 지연 시간 0 */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
/* Variable Font는 하나의 파일에 모든 굵기(100~900)를 포함 */
@font-face {
font-family: 'Inter';
src: url('Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
}
3. 동적 콘텐츠를 위한 공간 미리 확보(ROI: ⭐⭐⭐⭐)
a) 광고 영역 공간 확보
.ad-container {
min-height: 250px; /* 광고 높이를 확보해 로드 전에도 흔들리지 않음 */
background: #f0f0f0; /* 플레이스홀더 배경 */
}
b) 스켈레톤 화면(Skeleton Screen)
스켈레톤 화면은 보기 좋게 만드는 데 그치지 않고 레이아웃을 안정시키는 데 더 큰 의미가 있습니다.
// 로딩 전에 스켈레톤 화면을 표시해 콘텐츠가 갑자기 나타나면서 레이아웃이 흔들리는 현상 방지
function ProductCard({ loading, data }) {
if (loading) {
return (
<div className="skeleton">
<div className="skeleton-image" style={{ width: '100%', height: '200px', background: '#e0e0e0' }} />
<div className="skeleton-title" style={{ width: '80%', height: '20px', background: '#e0e0e0', margin: '10px 0' }} />
<div className="skeleton-price" style={{ width: '40%', height: '20px', background: '#e0e0e0' }} />
</div>
)
}
return (
<div className="product">
<img src={data.image} alt={data.title} />
<h3>{data.title}</h3>
<p>{data.price}</p>
</div>
)
}
4. 기존 콘텐츠 위에 콘텐츠 삽입하지 않기(ROI: ⭐⭐⭐⭐⭐)
// ❌ 상단에 banner를 삽입하면 페이지 콘텐츠가 아래로 밀림(CLS 폭증)
<div>
{showBanner && <Banner />}
<Content />
</div>
// ✅ fixed 위치 지정을 사용해 레이아웃에 영향을 주지 않음
<div>
{showBanner && <Banner style={{ position: 'fixed', top: 0, zIndex: 1000 }} />}
<Content style={{ marginTop: showBanner ? '60px' : '0' }} />
</div>
5. 애니메이션에 top/left 대신 transform 사용(ROI: ⭐⭐⭐)
멋을 내려고 top으로 애니메이션을 구현했다가 CLS가 폭증한 사례도 봤습니다.
/* ❌ layout을 일으켜 CLS 발생 */
.element {
position: relative;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { top: -100px; } /* top 변경은 리플로를 일으킴 */
to { top: 0; }
}
/* ✅ transform을 사용하면 composite만 발생(GPU 가속) */
.element {
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { transform: translateY(-100px); }
to { transform: translateY(0); }
}
5장: 종합 실전 - 전체 최적화 과정
우선순위가 중요합니다. 처음부터 SSR에 매달리지 말고 이미지 최적화부터 제대로 하세요. 처음 최적화했을 때 이미지에 너비와 높이를 추가하는 것만으로 10점이 올랐습니다. 정말 거저 얻은 점수였습니다.
최적화 우선순위 매트릭스
| 최적화 항목 | ROI | 난이도 | 우선순위 |
|---|---|---|---|
| 이미지 너비와 높이 추가 | 매우 높음 | 매우 낮음 | P0 |
| 이미지 형식 변환 | 매우 높음 | 낮음 | P0 |
| LCP 이미지 최적화 | 매우 높음 | 낮음 | P0 |
| 서드파티 스크립트 지연 | 매우 높음 | 낮음 | P0 |
| 글꼴 최적화 | 높음 | 낮음 | P1 |
| 코드 분할 | 높음 | 보통 | P1 |
| CDN | 높음 | 낮음 | P1 |
| SSR/SSG | 보통 | 높음 | P2 |
| Web Workers | 낮음 | 높음 | P3 |
측정 도구와 명령어
# 1. Lighthouse(가장 신뢰할 수 있음)
# Chrome DevTools > Lighthouse > Generate report
# Chrome 확장 프로그램이 점수에 영향을 주지 않도록 시크릿 모드 사용
# 2. WebPageTest(실제 기기 테스트)
# https://webpagetest.org
# 다양한 지역과 네트워크 속도를 선택할 수 있음
# 3. Chrome DevTools Performance 패널
# 로딩 과정을 기록해 병목 분석
# 각 작업의 소요 시간을 확인할 수 있음
# 4. npm 패키지 분석
npx webpack-bundle-analyzer
# 가장 큰 패키지를 시각화
# 5. 이미지 분석
npx sharp-cli info image.jpg
# 이미지 크기, 형식, 용량 확인
주의 사항
- ❌ 프로덕션 환경에서 Lighthouse를 테스트하지 마세요(시크릿 모드 사용 또는 확장 프로그램 비활성화).
- ❌ 한 번만 테스트하지 마세요(네트워크 변동이 크므로 최소 3회 측정 후 평균을 사용하세요).
- ❌ 데스크톱만 테스트하지 마세요(트래픽의 75%가 모바일에서 발생하므로 모바일이 더 중요합니다).
- ❌ Network throttling을 무시하지 마세요(느린 네트워크, Fast 3G를 시뮬레이션하세요).
- ❌ LCP 이미지를 절대 지연 로딩하지 마세요(제가 직접 겪은 함정입니다).
마무리 정리
성능 최적화는 일회성 작업이 아니라 지속적인 과정입니다. 운동처럼 꾸준함과 절제가 필요합니다. 하지만 Lighthouse 점수가 60점에서 90점으로 오르는 순간은 정말 큰 성취감을 줍니다.
성능 최적화는 단순한 기술 작업이 아니라 사용자 경험에 대한 존중이기도 합니다. 1초를 줄일 때마다 더 많은 사용자를 붙잡을 수 있습니다. 모바일 사용자의 인내심은 단 3초뿐이지만, 이제 여러분에게는 이 3초를 1초로 줄일 능력이 있습니다.
함께 노력해 모든 사용자가 매끄러운 브라우징 경험을 누릴 수 있게 합시다.
FAQ
Core Web Vitals의 세 가지 핵심 지표 기준은 무엇인가요?
• 2.5초 미만은 좋음, 2.5~4초는 개선 필요, 4초 초과는 나쁨
INP(다음 페인트와의 상호작용):
• 200ms 미만은 좋음, 200~500ms는 개선 필요, 500ms 초과는 나쁨
• 2024년 3월에 INP가 FID를 대체해 핵심 지표가 됨
CLS(누적 레이아웃 이동):
• 0.1 미만은 좋음, 0.1~0.25는 개선 필요, 0.25 초과는 나쁨
핵심 데이터:
• 페이지 로딩 시간이 3초에서 5초로 늘어나면 이탈률이 38% 증가
• 모바일 로딩이 3초를 넘으면 사용자의 53%가 바로 이탈
• LCP를 잘 최적화하면 전환율을 7~15% 높일 수 있음
LCP(최대 콘텐츠 렌더링)는 어떻게 최적화하나요?
1) 최신 이미지 형식 사용(AVIF는 압축률이 41%, WebP는 30% 더 높음)
• 실전 사례에서 hero 이미지를 500KB에서 120KB로 줄여 LCP를 4.2초에서 2.1초로 단축
2) LCP 이미지에는 반드시 priority를 적용하고 지연 로딩하지 않으며, 첫 화면의 큰 이미지는 eager 사용
3) 반응형 이미지 srcset으로 트래픽 60% 절감
4) CDN과 핵심 이미지 preload 사용
5) 서버 응답 시간 최적화(CDN, SSR/SSG, 데이터베이스 쿼리 최적화)
우선순위: 이미지 최적화는 투입 비용이 적고 효과가 빨라 ROI가 가장 높습니다.
INP(다음 페인트와의 상호작용)는 어떻게 최적화하나요?
1) 코드 분할(라우트 단위, React lazy, Vue defineAsyncComponent)
• 실전 사례에서 1.2MB를 첫 화면 200KB로 줄여 INP를 450ms에서 180ms로 단축
2) 서드파티 스크립트 지연 로딩(defer 또는 수동으로 3초 지연), Facade 패턴으로 무거운 컴포넌트 지연 로딩
3) 자동 삽입되는 서드파티 콘텐츠 제거(예: YouTube 자동 embed 제거 후 INP가 650ms에서 220ms로 감소)
4) requestIdleCallback으로 긴 작업 분할
5) Web Workers로 복잡한 연산 처리
6) 디바운스, 스로틀링, Passive 이벤트 리스너 사용
CLS(누적 레이아웃 이동)는 어떻게 최적화하나요?
1) 이미지에 너비와 높이 지정(width 및 height 속성 또는 CSS aspect-ratio)
• 실전 사례에서 CLS를 0.35에서 0.05로 감소
2) 글꼴 최적화(font-display:swap으로 FOIT 방지, 핵심 글꼴 preload, 시스템 글꼴 또는 Variable Font 사용)
3) 동적 콘텐츠 공간 미리 확보(광고 영역 min-height, 스켈레톤 화면으로 레이아웃 안정화)
4) 기존 콘텐츠 위에 콘텐츠를 삽입하지 않기(fixed 위치 지정 사용)
5) 애니메이션에 top/left 대신 transform 사용(layout 없이 composite만 발생)
성능 최적화의 우선순위는 어떻게 되나요?
• 이미지에 너비와 높이 추가
• 이미지 형식 변환(AVIF/WebP)
• LCP 이미지 최적화(priority 적용, 지연 로딩 금지)
• 서드파티 스크립트 지연 로딩
P1 우선순위(ROI 높음):
• 글꼴 최적화(font-display:swap)
• 코드 분할
• CDN
P2 우선순위(ROI 보통, 난이도 높음):
• SSR/SSG
P3 우선순위(ROI 낮음, 난이도 높음):
• Web Workers
권장 사항: 먼저 P0부터 진행하세요. 이미지에 너비와 높이만 추가해도 10점을 높일 수 있습니다. 처음부터 SSR에 매달리지 말고 이미지 최적화부터 제대로 하세요.
LCP 이미지를 지연 로딩해도 되나요?
LCP 이미지(첫 화면의 큰 이미지)에는 반드시 priority 또는 loading="eager"를 적용하고 지연 로딩하지 마세요. 지연 로딩은 첫 화면 밖의 이미지에 사용하는 기능입니다.
잘못된 방법:
<img src="hero.jpg" loading="lazy">를 사용하면 오히려 LCP 점수가 떨어집니다.
올바른 방법:
• 첫 화면의 큰 이미지는 eager를 사용하고 나머지 이미지만 지연 로딩
• 기억하세요. 첫 화면에 보이는 이미지는 단 한 장도 지연 로딩하면 안 됩니다.
Core Web Vitals는 어떻게 측정하고 테스트하나요?
1) Lighthouse(Chrome DevTools, 가장 신뢰할 수 있으며 확장 프로그램의 영향을 피하려면 시크릿 모드 사용)
2) WebPageTest(실제 기기 테스트, 지역과 네트워크 속도 선택 가능)
3) Chrome DevTools Performance 패널(로딩 과정을 기록해 병목 분석)
4) webpack-bundle-analyzer(가장 큰 패키지를 시각화)
5) sharp-cli info(이미지 크기, 형식, 용량 확인)
주의 사항:
• 프로덕션 환경에서 테스트하지 않기
• 한 번만 테스트하지 않기(최소 3회 측정 후 평균 사용)
• 데스크톱만 테스트하지 않기(트래픽의 75%가 모바일에서 발생하므로 모바일이 더 중요)
• Network throttling을 무시하지 않기(느린 네트워크 시뮬레이션)
5분 읽기 · 게시일: 2025년 11월 24일 · 수정일: 2026년 9월 8일



댓글
GitHub로 로그인하여 댓글을 남기세요