[번역] 실제로 성과를 만들어내는 프론트엔드 성능 최적화
Soshy·

Lighthouse는 출발점이지, 결승선이 아닙니다.

이 글에서는 Lighthouse 점수만으로는 전체 그림을 파악할 수 없는 이유, 대규모 서비스에서 실제로 중요한 지표, 실사용자 모니터링(RUM)이 성능에 대한 사고방식을 어떻게 바꾸는지, 그리고 수백만 명의 사용자를 서비스하는 플랫폼에서 가장 큰 효과를 내는 최적화 기법들을 다룹니다.
Lighthouse는 훌륭한 도구입니다. 무시하라는 말이 아닙니다. 하지만 저는 팀들이 완벽한 Lighthouse 점수를 쫓는 동안, 실제 사용자들은 중저가 안드로이드 기기에서 4G 연결로 4초짜리 로딩을 겪는 상황을 수없이 목격해왔습니다.
점수는 훌륭해 보였지만, 실제 경험은 그렇지 않았습니다.
1,000만 명의 사용자를 위한 서비스를 개발할 때, 성능은 더 이상 보고서의 숫자 문제가 아닙니다. 그것은 실제 기기에서 실제 네트워크 환경을 사용하는 진짜 사람들에 관한 문제입니다. 그리고 실험실 점수와 사용자가 실제로 느끼는 경험 사이의 간극은 대부분의 개발자들이 생각하는 것보다 훨씬 넓습니다.
이 글은 그 간극을 좁히는 것에 관한 이야기입니다.
Lighthouse 점수는 실험실 데이터이지, 현실이 아닙니다
Lighthouse는 통제된 환경에서 실행됩니다. 제한된 CPU, 시뮬레이션된 네트워크, 확장 프로그램도 없고 캐시 데이터도 없으며 백그라운드 탭도 없는 깨끗한 브라우저. 그것은 여러분의 사용자들이 실제로 브라우저를 사용하는 방식이 아닙니다.
실제 사용자들은 15개의 브라우저 탭을 열어놓은 3년 된 스마트폰으로, 불안정한 네트워크의 지하철 안에서 접속합니다. 그동안 여러분의 자바스크립트는 백그라운드 앱과 CPU 자원을 두고 싸우고 있습니다.
바로 이것이 Lighthouse 90+ 점수임에도 사용자 경험이 나쁠 수 있는 이유입니다. 실험실은 거짓말하지 않지만, 진실의 일부만 말해줄 뿐입니다.
실험실 데이터: Lighthouse가 제공하는 것으로, 회귀를 잡아내고 시간 경과에 따른 추세를 추적하는 데 유용합니다. 하지만 절대 유일한 신호가 되어서는 안 됩니다.
현장 데이터: 실제 사용자가 경험하는 것으로, 성능 작업이 실제로 결실을 맺는 곳입니다.
대규모 서비스에서 실제로 중요한 지표들
핵심 웹 지표 (Core Web Vitals)
Google의 Core Web Vitals는 사용자 중심 성능 지표의 표준에 가장 근접한 것입니다. 세 가지가 가장 중요합니다.
LCP - 최대 콘텐츠풀 페인트 (Largest Contentful Paint): 가장 큰 가시적 요소가 렌더링되는 데 얼마나 걸리나요? 대부분의 플랫폼에서 이것은 히어로 이미지, 비디오 썸네일, 또는 헤드라인입니다. 사용자가 "페이지가 로드됐다"고 인식하는 지점입니다.
(목표: 2.5s 이하)
INP - 다음 페인트까지의 상호작용 (Interaction to Next Paint): 사용자 상호작용 후 페이지가 얼마나 빨리 반응하나요? 버튼 클릭, 메뉴 탭, 폼 제출 — 페이지가 시각적으로 반응하기까지 얼마나 걸리나요? 2024년에 FID(최초 입력 지연)를 대체했으며, 실제 인터랙티비티를 훨씬 잘 측정합니다.
(목표: 200ms 이하)
CLS - 누적 레이아웃 이동 (Cumulative Layout Shift): 로딩 중 페이지가 얼마나 많이 움직이나요? 늦게 로드되는 광고, 크기가 지정되지 않은 이미지, 폰트 교체 — 이 모든 것이 CLS에 영향을 줍니다. 콘텐츠가 많은 플랫폼에서는 사용자 경험을 조용히 망칠 수 있습니다.
(목표: 0.1 이하)
이 세 가지 지표는 SEO 순위와 사용자 유지율에 직접적인 영향을 미칩니다. 대규모 서비스에서 CLS 0.1 개선이나 LCP 500ms 단축은 측정 가능한 참여도 및 전환율 향상으로 이어집니다.
TTFB - 첫 번째 바이트까지의 시간 (Time to First Byte)
브라우저가 무언가를 렌더링하기 전에, 서버로부터 응답을 받아야 합니다. TTFB는 그 대기 시간을 측정합니다. 높은 TTFB는 보통 서버 측 문제를 가리킵니다 — 느린 API 응답, CDN 미사용, 또는 최적화되지 않은 서버 렌더링.
글로벌 사용자를 대상으로 하는 플랫폼에서는 CDN 설정만으로도 상당수 사용자의 TTFB를 800ms에서 100ms 이하로 줄일 수 있습니다.
TTI - 상호작용 가능 시간 (Time to Interactive)
사용자가 페이지를 실제로 사용할 수 있는 시점은 언제인가요? 단순히 보이는 것이 아니라, UI가 멈추지 않고 상호작용할 수 있는 시점입니다. 자바스크립트 번들 크기와 실행 시간이 가장 직접적인 영향을 미치는 곳입니다.
로드된 것처럼 보이지만 클릭에 반응하지 않는 페이지는 사용자가 경험할 수 있는 가장 불쾌한 경험 중 하나입니다.
실사용자 모니터링 - 무시할 수 없는 신호
Lighthouse만 실행하고 있다면, 당신은 절반쯤 눈을 감고 비행하는 셈입니다. 실사용자 모니터링(RUM; Real User Monitoring)은 실제 사용자 세션에서 성능 데이터를 캡처하여 여러분에게 전달합니다.
차이는 상당합니다. RUM을 통해 다음을 파악할 수 있습니다:
- 기기 유형, 브라우저, 지역별 성능 차이
- 실제 환경에서 LCP나 INP가 가장 나쁜 페이지
- 코드베이스가 성장함에 따라 성능이 어떻게 저하되는지
- 지금 이 순간 얼마나 많은 사용자가 나쁜 성능을 경험하고 있는지
Datadog RUM, SpeedCurve, Mux Data, 또는 무료 Chrome 사용자 경험 보고서(CrUX) 같은 도구들이 이런 가시성을 제공합니다.
수백만 명의 사용자를 서비스하는 플랫폼에서, 5%의 사용자가 나쁜 성능을 경험하고 있다면, 그것은 50만 명이 불쾌한 경험을 하고 있다는 의미입니다. RUM은 이것을 보이게 만듭니다. Lighthouse는 그렇지 않습니다.
실제로 성과를 만드는 최적화 기법들
1. 자바스크립트 번들 크기
이것은 거의 항상 가장 큰 레버입니다. 자바스크립트는 웹에서 가장 비싼 리소스입니다 — 다운로드, 파싱, 실행이 모두 필요한 후에야 유용한 일을 합니다.
코드 스플리팅은 대규모 서비스에서 선택이 아닙니다. 모든 라우트는 필요한 자바스크립트만 로드해야 합니다.
// Instead of importing everything upfront
import HeavyComponent from './HeavyComponent'
// Load it only when needed
const HeavyComponent = React.lazy(() => import('./HeavyComponent'))
번들을 정기적으로 감사하세요. webpack-bundle-analyzer나 vite-bundle-visualizer 같은 도구들은 번들 안에 무엇이 있고 어디서 용량이 오는지 정확히 보여줍니다. 거의 항상 놀라운 것들을 발견하게 됩니다.
서드파티 스크립트가 보통 가장 큰 문제입니다. 분석, 채팅 위젯, 광고 스크립트 — 이것들은 종종 동기적으로 로드되어 렌더링을 막습니다. 비동기로 로드하거나 페이지가 인터랙티브해질 때까지 완전히 지연시키세요.
2. 이미지 최적화
이미지는 대부분의 페이지에서 가장 큰 에셋입니다. 이것을 잘못 처리하면 LCP에 직접적인 영향을 줍니다.
- 현대적인 포맷을 사용하세요. WebP는 널리 지원되며 JPEG나 PNG보다 훨씬 작습니다. AVIF는 지원되는 곳에서는 더욱 좋습니다.
- 이미지에 항상 명시적인 width와 height를 지정하세요. 레이아웃 이동을 방지하고 브라우저가 이미지 로드 전에 공간을 할당하는 데 도움이 됩니다.
- 폴드 아래의 이미지에는 지연 로딩을 사용하세요.
- 적절한 크기의 이미지를 제공하세요. 400px 너비의 모바일 화면에 2000px 짜리 이미지를 제공하지 마세요.
<img
src="thumbnail.webp"
width="400"
height="225"
loading="lazy"
alt="Video thumbnail"
/>
대규모 콘텐츠 라이브러리를 가진 플랫폼에서, 이미지 최적화만으로 페이지 용량을 40~60% 줄일 수 있습니다.
3. 크리티컬 렌더링 경로
브라우저는 HTML을 다운로드하고 파싱한 후, CSS와 자바스크립트를 발견하고, 다운로드하고, 파싱한 다음 페이지를 렌더링합니다. 그 체인의 모든 단계는 속도를 높이거나 늦출 기회입니다.
- 인라인 크리티컬 CSS — 폴드 위 콘텐츠를 렌더링하는 데 필요한 스타일 — 를 HTML에 직접 넣으세요. 초기 뷰를 위한 렌더 블로킹 네트워크 요청을 제거합니다.
- 브라우저가 파싱 과정 후반에야 발견하는 핵심 리소스를 미리 로드하세요.
<link rel="preload" as="font" href="/fonts/main.woff2" crossorigin>
<link rel="preload" as="image" href="/hero.webp">
- 중요하지 않은 자바스크립트는 지연시키세요. 페이지가 인터랙티브해지기 전에 실행될 필요가 없는 스크립트는 렌더링을 막아서는 안 됩니다.
4. 캐싱 전략
이 시리즈의 이전 글에서 깊이 다뤘지만, 캐싱은 활용 가능한 가장 효과가 큰 성능 최적화 중 하나이므로 언급할 가치가 있습니다.
잘 캐싱된 플랫폼에서 재방문 사용자는 페이지를 거의 전적으로 캐시에서 로드할 수 있습니다. 정적 에셋에 대한 네트워크 요청도 없고, 변경되지 않은 리소스에 대한 서버 왕복도 없습니다. 재방문 사용자에 대한 성능 개선은 극적입니다.
5. 메인 스레드 작업 줄이기
INP와 TTI 모두 메인 스레드가 바쁠 때 악화됩니다. 자바스크립트 실행, 긴 작업, 레이아웃 재계산 — 이것들은 모두 사용자 상호작용을 처리하는 동일한 스레드를 두고 경쟁합니다.
도움이 되는 몇 가지:
- 긴 작업을 쪼개세요. 50ms 이상 걸리는 작업은 눈에 띄는 끊김을 유발할 수 있습니다.
setTimeout이나scheduler.postTask를 사용해 작업 청크 사이에 브라우저에 제어권을 반환하세요. - 레이아웃 스래싱을 피하세요. DOM 읽기와 쓰기를 번갈아 호출하면 브라우저가 레이아웃을 반복적으로 재계산하게 됩니다. 읽기와 쓰기를 배치로 묶으세요.
- Web Workers를 사용해 무거운 연산을 메인 스레드에서 분리하세요.
// Yielding to the browser between heavy tasks
async function processLargeDataset(items) {
for (let i = 0; i < items.length; i++) {
process(items[i])
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0))
}
}
}
성능 예산 - 지속 가능하게 만들기
대규모 성능 작업에서 가장 어려운 부분 중 하나는 개선 사항이 시간이 지나도 퇴보하지 않도록 유지하는 것입니다. 성능 예산이 이 문제를 해결합니다.
성능 예산은 번들 크기, LCP, TTI 같은 지표에 명시적인 한도를 설정합니다. 풀 리퀘스트가 예산을 초과하면, 빌드가 실패합니다.
{
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 1000 }
],
"timings": [
{ "metric": "first-contentful-paint", "budget": 1500 },
{ "metric": "interactive", "budget": 3500 }
]
}
이것은 성능을 한 번 최적화하고 신경 쓴 엔지니어만의 문제가 아니라, 모든 팀원의 레이더 위에 올려놓습니다.
대규모 서비스가 실제로 가르쳐주는 것들
이 규모의 플랫폼에서 일하며 배웠지만, 대부분의 성능 가이드에서는 찾아볼 수 없는 것들이 있습니다.
기기 분포가 생각보다 중요합니다. 개발 머신은 사용자들을 대표하지 않습니다. 중저가 안드로이드 기기에서 프로파일링하면 존재하는지도 몰랐던 문제들을 발견하게 됩니다.
지역이 중요합니다. 글로벌 사용자를 대상으로 하는 플랫폼은 한 지역의 빠른 서버가 아니라, CDN 전략이 필요합니다. 먼 오리진 서버의 네트워크 지연 시간은 특정 지역 사용자의 TTFB에 수 초를 더할 수 있습니다.
성능은 점진적으로 저하됩니다. 의도적으로 느린 앱을 출시하는 사람은 없습니다. 의존성 하나, 기능 하나, 서드파티 스크립트 하나씩 느려집니다. 예산과 정기적인 모니터링 없이는 사용자들이 불평하기 시작할 때까지 알아채지 못할 것입니다.
파레토 법칙이 적용됩니다. 소수의 페이지가 보통 트래픽의 대부분을 차지합니다. 그 페이지들을 찾아내고, 집착적으로 측정하고, 먼저 최적화하세요. 성능 작업의 영향이 가장 클 곳이 바로 거기입니다.
마치며
Lighthouse는 도구이지, 목표가 아닙니다. 초록색 점수는 기본을 올바르게 했다는 의미입니다. 사용자들이 빠른 경험을 하고 있다는 뜻이 아닙니다.
대규모 서비스에서 성능을 제대로 구현하는 팀들은 실제 사용자들의 경험을 측정하고, 퇴보를 방지하는 예산을 설정하며, 자신들의 특정 플랫폼과 사용자층에 실제로 효과가 있는 최적화에 집중하는 팀들입니다.
RUM부터 시작하세요. 실제 사용자들이 어디서 고생하는지 찾으세요. 그것부터 먼저 고치세요.
Lighthouse 점수는 따라올 것입니다.