웹 성능 최적화 기본 원칙
Core Web Vitals와 로딩·상호작용 지표를 측정하고 프론트엔드·백엔드 병목을 증거에 따라 개선합니다.
앞 글들에서는 웹 보안의 기초와 인증, 권한 관리를 살펴봤습니다. 이제 13장의 다른 축인 성능으로 넘어갑니다. 성능 프로파일링에서 Performance 패널과 Lighthouse로 성능을 측정하는 방법을 익혔다면, 이 글에서는 그 측정을 바탕으로 프런트엔드와 백엔드에서 적용할 수 있는 성능 최적화의 기본 원칙과 방법을 정리합니다.
빠른 웹 사이트는 사용자에게 좋은 인상을 주고 다시 찾아오게 만듭니다. 반대로 로딩이 길어질수록 사용자가 떠날 가능성이 커지고, 쇼핑몰 같은 서비스에서는 성능이 매출에도 영향을 줄 수 있습니다. Google은 페이지 경험을 검색 순위의 여러 요소 가운데 하나로 쓰며, 불필요한 전송과 계산을 줄이면 기기의 배터리와 네트워크도 아낄 수 있습니다. 다만 성능 개선의 효과는 서비스마다 다르므로, 다른 회사의 사례에서 나온 수치를 그대로 매출 계산식으로 쓰지는 않습니다.
측정을 바탕으로 개선하기
MEASURE & IMPROVE
측정한 병목을 바꾸고 같은 조건에서 확인합니다
대표 사용자 경로의 기준선을 기록하고 재현한 증거로 가설을 좁힙니다. 개선 뒤에는 다른 지표와 접근성의 회귀도 확인합니다.
- 현장 기준선
대표 경로의 LCP·INP·CLS와 서버 지표를 기록합니다.
- 실험실 재현
같은 기기·네트워크·데이터·동작 순서로 재현합니다.
- 병목 근거
Network·성능 trace·서버 기록에서 지배 비용을 찾습니다.
- 한 가설 변경
원인 가설에 대응하는 변경을 적용합니다.
- 동일 조건 비교
반복 측정한 전후 지표와 접근성·기능 회귀를 확인합니다.
- 예산과 현장 관찰
예산을 정하고 현장 회귀가 생기면 기준선부터 다시 검토합니다.
회색 실선은 검토 진행, 주황 테두리 상자는 근거에 따른 변경, 점선은 다음 반복입니다. 단일 도구 점수나 한 번의 측정으로 실제 사용자 개선을 확정하지 않습니다.
위 그림은 측정을 바탕으로 성능을 개선하는 순환입니다. 먼저 실제 사용자의 현장 데이터에서 대표적인 사용자 경로와 기기의 LCP, INP, CLS를 기준선으로 기록합니다. 그다음 기기, 네트워크, 캐시, 동작 순서를 고정해 실험실에서 같은 상황을 재현하고, 네트워크 기록과 성능 trace, 서버 기록에서 가장 큰 비용을 차지하는 병목을 찾습니다.
병목을 찾으면 그 원인에 맞는 가설 하나만 바꾸되, 다른 기능은 그대로 지킵니다. 바꾼 뒤에는 같은 조건에서 여러 번 측정해 전후를 비교하고, 다른 지표와 접근성이 나빠지지 않았는지도 확인합니다. 마지막으로 성능 예산(performance budget)을 정해 두고 현장 데이터를 계속 지켜보며, 나빠진 것이 발견되면 기준선부터 다시 검토하는 다음 순환으로 들어갑니다. 현장 데이터는 수집 기간과 표본이 실험실과 다르다는 점도 구별합니다.
그림 아래의 설명처럼 도구 하나의 점수나 한 번의 측정으로 실제 사용자의 경험이 나아졌다고 확정하지 않습니다. 여기서 trace는 브라우저가 한 작업과 리소스를 처리한 시점을 시간 순서로 기록한 자료로, 총점만 보는 대신 어떤 작업이 어느 구간을 차지했는지 찾아 병목의 가설을 세우게 해 줍니다.
성능 측정 지표
성능은 느낌이 아니라 객관적인 지표로 측정하고 개선해야 합니다. Google은 페이지의 핵심 사용 경험을 나타내는 지표로 Core Web Vitals를 제시했습니다.
LCP(Largest Contentful Paint)는 페이지 탐색을 시작한 때부터 뷰포트 안에서 가장 큰 이미지, 동영상, 글 덩어리가 표시될 때까지의 시간입니다. 페이지의 모든 리소스를 다 받은 시점과는 다르며, 사용자가 주요 콘텐츠를 언제 볼 수 있는지와 바로 이어집니다. 좋은 기준은 2.5초 이하입니다.
INP(Interaction to Next Paint)는 페이지를 방문하는 동안 일어난 클릭, 탭, 키보드 입력이 다음 화면에 반영되기까지의 지연을 관찰해 대표 반응 시간으로 나타냅니다. 첫 입력 하나가 아니라 방문 전체의 반응성을 평가하며, 좋은 기준은 200ms 이하입니다.
CLS(Cumulative Layout Shift)는 이미지나 광고가 늦게 나타나며 글이나 버튼이 밀려나는 것처럼, 화면의 요소가 예상하지 못하게 움직이는 정도를 나타냅니다. 클릭하려던 요소가 갑자기 움직여 잘못 누르는 불편을 막기 위한 시각적 안정성 지표로, 좋은 기준은 0.1 이하입니다. CLS는 방문 전체의 이동 점수를 무조건 더한 값이 아니라, 이동 사이의 간격이 1초 미만이고 길이가 최대 5초인 세션 창(session window)으로 묶은 점수 가운데 가장 큰 값입니다(web.dev의 CLS).
세 지표의 좋음 판정은 실제 방문을 모바일과 데스크톱으로 나누고, 각 지표의 75번째 백분위가 기준을 만족하는지 봅니다. 75번째 백분위는 측정값을 작은 순서로 늘어놓았을 때 약 75%가 그 값 이하에 놓이는 값으로, 평균과 다릅니다. 한 번 빠르게 열린 방문이나 Lighthouse 점수로 현장의 판정을 대신하지 않습니다(web.dev의 Web Vitals).
이 밖에도 처음으로 글이나 이미지가 그려지는 시점인 FCP(First Contentful Paint), 실험실에서 측정 구간 안의 각 메인 스레드 작업이 50ms를 넘은 부분을 더한 TBT(Total Blocking Time)가 있습니다. TBT는 작업 시간 전체를 더한 값이 아니며 현장의 INP와도 다릅니다. 예전의 TTI(Time to Interactive)는 Lighthouse 10부터 점수 지표에서 빠졌습니다.
측정 도구로는 성능, 접근성, SEO를 종합적으로 분석하는 Lighthouse, 실제 사용자의 현장 데이터와 Lighthouse의 실험실 분석을 함께 보여 주는 PageSpeed Insights, 여러 네트워크 환경에서 로딩 과정을 자세히 분석하는 WebPageTest가 있습니다.
CORE WEB VITALS
LCP·INP·CLS는 서로 다른 경험을 측정합니다
현장 데이터에서 나쁜 지표와 사용자 조건을 찾고 실험실 기록으로 원인 가설을 확인합니다. 지표명만으로 원인을 단정하지 않습니다.
LCP · 주요 콘텐츠 표시
좋음 ≤ 2.5초
가장 큰 적격 콘텐츠가 표시되는 시점을 측정합니다.
확인할 증거 TTFB, 리소스 발견·전송, 렌더 지연을 나눠 확인합니다. LCP 이미지·큰 텍스트 등 실제 후보 요소를 먼저 찾습니다.
INP · 상호작용 반응
좋음 ≤ 200ms
클릭·탭·키보드 상호작용의 다음 화면 표시까지 반응 지연을 평가합니다.
확인할 증거 입력 대기, 이벤트 처리, 다음 표시 지연을 나눕니다. 긴 작업·핸들러 비용·스타일과 레이아웃을 trace에서 확인합니다.
CLS · 시각적 안정성
좋음 ≤ 0.1
예상하지 않은 레이아웃 이동을 누적 이동 점수로 평가합니다.
확인할 증거 크기 없는 이미지·광고, 폰트 교체, 늦게 삽입된 UI를 layout-shift 기록과 대조합니다. 이동을 일으킨 원인을 확인합니다.
현장 판단 · 세 지표의 75백분위
모바일·데스크톱을 구분하고 같은 페이지·사용자 집단의 75백분위에서 세 목표를 함께 확인합니다. 데이터가 없는 지표는 좋다고 추정하지 않습니다.
실험실 · 재현과 원인 분석
고정한 기기·네트워크·시나리오의 trace로 변경 원인을 좁힙니다. Lighthouse의 TBT는 실험실 긴 작업 지표이며 현장 INP와 같지 않습니다.
재측정 · 다른 지표도 확인
같은 조건을 반복 측정하고 현장 데이터의 수집 기간·표본 차이도 고려합니다. LCP 개선이 INP·CLS나 접근성을 악화시키지 않았는지 확인합니다.
좋음 기준은 각 지표의 경험 기준입니다. 한 번의 lab 결과나 종합 점수를 그대로 현장의 75백분위 판정으로 바꾸지 않습니다.
위 그림은 세 지표가 서로 다른 경험을 측정하므로, 나빠진 지표마다 확인할 곳이 다르다는 것을 보여 줍니다. LCP가 나쁘면 실제 LCP 후보 요소를 먼저 찾고, 첫 응답 시간(TTFB), 리소스를 발견하고 받는 시간, 받은 뒤 그리기까지의 지연을 나눠 확인합니다. INP가 나쁘면 입력이 처리되기까지 기다린 시간, 이벤트 처리 시간, 다음 화면이 그려지기까지의 지연을 나누고, trace에서 긴 작업과 핸들러의 비용, 스타일과 레이아웃을 봅니다. CLS가 나쁘면 크기를 정하지 않은 이미지나 광고, 글꼴 교체, 늦게 끼워 넣은 화면 요소를 레이아웃 이동 기록과 맞춰 봅니다.
그림 아래쪽은 판단할 때의 원칙입니다. 현장에서는 모바일과 데스크톱을 나누고 같은 페이지와 사용자 집단의 75번째 백분위에서 세 기준을 함께 확인하며, 데이터가 없는 지표를 좋다고 추정하지 않습니다. 실험실에서는 고정한 기기와 네트워크, 시나리오의 trace로 원인을 좁히고, 다시 측정할 때는 한 지표를 개선하다 다른 지표나 접근성이 나빠지지 않았는지도 확인합니다. 화살표는 원인을 확정한다는 뜻이 아니라 분석할 방향을 가리킵니다.
프런트엔드 성능 최적화
프런트엔드 최적화는 페이지의 첫 로딩 속도와 상호작용의 반응성, 시각적 안정성을 개선하는 데 초점을 맞춥니다. 브라우저가 받아야 할 양을 줄이고, 첫 화면을 그리기 전에 기다리는 일을 줄이고, 그린 뒤에 다시 하는 일을 줄이는 것이 큰 방향입니다. 아래에서는 리소스 크기, 렌더링을 막는 리소스, 번들링과 코드 분할, 캐싱, DOM과 애니메이션의 순서로 봅니다.
리소스 크기 줄이기
자바스크립트, CSS, HTML에서 공백과 주석, 긴 변수 이름을 지워 파일을 줄이는 최소화(minification)를 합니다. webpack이나 Terser 같은 빌드 도구가 이 일을 합니다. 서버는 파일을 보내기 전에 gzip이나 Brotli로 압축하고, Content-Encoding 헤더로 압축 방식을 알려 전송량을 줄입니다.
이미지는 알맞은 형식을 고르고, 압축하고, 화면 크기에 맞게 여러 크기를 준비합니다. 화면 밖의 이미지는 loading="lazy"로 지연 로딩할 수 있는데, 브라우저는 거리와 환경을 보고 뷰포트에 들어오기 전에 미리 불러오기 시작할 수 있습니다. 첫 화면의 LCP 후보 이미지까지 지연 로딩하면 주요 콘텐츠가 늦게 보이므로 빼야 합니다(web.dev의 LCP 리소스 우선순위). 이미지는 사용자와 가까운 CDN에서 보내면 더 빨라집니다. 이미지 최적화는 다음 글에서 자세히 다룹니다.
렌더링을 막는 리소스 줄이기
<head> 안의 <script>나 <link rel="stylesheet">는 페이지를 그리는 것을 막을 수 있습니다. CSS는 가능한 한 작게 유지하고, 첫 화면에 꼭 필요한 CSS(critical CSS)는 <style> 태그 안에 넣어 빠르게 그리게 할 수 있습니다. 외부 자바스크립트에 async나 defer를 붙이면 HTML을 해석하는 동안 스크립트를 함께 내려받습니다. async는 준비되는 대로 실행하므로 그동안 해석이 멈출 수 있고, defer는 해석이 끝난 뒤 실행합니다. 사용자의 상호작용에 바로 필요 없는 스크립트는 나중에 불러옵니다.
더 깊이: 해석이 멈춘 동안 자원을 미리 찾는 프리로드 스캐너
async나 defer가 없는 스크립트를 만나면 HTML 파서는 그 스크립트가 DOM을 바꿀지 알 수 없으므로 스크립트를 받아 실행할 때까지 해석을 멈춥니다. 그렇다고 그동안 네트워크가 쉬는 것은 아닙니다. 브라우저에는 프리로드 스캐너(preload scanner)라는 보조 파서가 있어, 주 파서가 멈춘 동안 아직 해석하지 않은 HTML 원문을 앞질러 훑으며 <img>, <link>, <script>의 주소를 찾아 미리 요청합니다.
문제는 프리로드 스캐너가 HTML의 마크업만 읽는다는 점입니다. 자바스크립트가 나중에 만들어 넣는 <script>, CSS의 background-image로 지정한 이미지, data-src에 주소를 넣었다가 스크립트로 옮기는 지연 로딩 이미지, 클라이언트에서 그린 마크업 안의 자원은 스캐너가 찾지 못합니다. 이런 자원은 주 파서나 스크립트가 거기까지 도달한 뒤에야 요청되므로 늦게 내려받기 시작합니다.
그래서 첫 화면의 LCP 이미지처럼 일찍 받아야 할 자원은 HTML에 일반 src로 적고, CSS 배경 이미지라면 <link rel="preload">로 따로 알려 줍니다. 스크립트도 자바스크립트로 만들어 넣기보다 <script async>처럼 마크업에 적는 편이 일찍 발견됩니다. 이런 패턴은 web.dev의 프리로드 스캐너 설명에 정리되어 있습니다.
번들링과 코드 분할
번들링(bundling)은 여러 자바스크립트와 CSS 파일을 하나로 합쳐 요청 수를 줄이는 일이고, 코드 분할(code splitting)은 특정 화면이나 컴포넌트가 필요할 때만 그 코드를 불러오도록 번들을 나누는 일입니다. React의 lazy()나 동적 import()로 나누면 첫 로딩에 필요 없는 코드를 미룰 수 있습니다.
캐싱
Cache-Control이나 ETag 같은 HTTP 헤더로 정적 파일의 캐시 규칙을 정합니다. 아직 신선한(fresh) 응답은 요청 없이 다시 쓰고, 만료되었거나 규칙상 필요한 경우에는 서버에 다시 확인합니다. 서비스 워커로 오프라인 지원과 세밀한 캐시 전략을 구현하면 네트워크 의존을 더 줄일 수 있습니다.
DOM과 애니메이션
불필요한 DOM 요소를 만들지 않고 DOM을 바꾸는 횟수를 줄입니다. React의 가상 DOM은 DOM 갱신을 효율적으로 처리하지만, React.memo 같은 방법으로 불필요한 컴포넌트 렌더를 줄이는 것도 필요합니다. 애니메이션은 레이아웃과 페인트를 다시 하지 않아도 되는 transform과 opacity를 우선 쓰고, requestAnimationFrame()으로 브라우저의 렌더링 주기에 맞춰 실행합니다.
백엔드 성능 최적화
백엔드 최적화는 API의 응답 시간을 줄이고, 서버 자원을 효율적으로 쓰고, 데이터베이스 조회를 빠르게 하는 데 초점을 맞춥니다. 프런트엔드를 아무리 다듬어도 서버의 첫 응답이 늦으면 LCP 같은 로딩 지표도 그만큼 늦어지므로, 요청 하나가 서버 안에서 어디에 시간을 쓰는지부터 나눠 봅니다. 아래에서는 API 응답 시간, 데이터베이스, 서버 자원을 차례로 다룹니다.
API 응답 시간
불필요한 계산을 없애고 효율적인 알고리즘을 씁니다. 데이터베이스나 외부 API 호출 같은 I/O는 비동기로 처리해, Node.js와 Express.js 입문에서 본 것처럼 이벤트 루프가 막히지 않게 합니다. 자주 바뀌지 않는 데이터는 Redis나 Memcached 같은 캐시 서버에 두어 데이터베이스와 외부 API의 호출을 줄입니다.
데이터베이스
자주 조회하는 열에 인덱스(index)를 만들면, 책의 색인처럼 원하는 행을 빠르게 찾을 수 있습니다. 목록을 조회한 뒤 항목마다 다시 조회하는 N+1 문제나 불필요한 결합 같은 비효율적인 쿼리를 고칩니다. 데이터 모델은 정규화로 정확성을 지키되, 성능 병목이 생기면 일부러 데이터를 중복해 두는 비정규화(denormalization)를 검토할 수도 있습니다. 데이터베이스 연결을 미리 만들어 두고 재사용하는 연결 풀(connection pool)은 연결을 매번 새로 만드는 비용을 줄여 줍니다.
서버 자원
서버의 CPU, 메모리, 디스크, 네트워크를 늘리는 수직 확장(scale-up)과, 부하 분산 장치로 여러 서버에 요청을 나누는 수평 확장(scale-out)이 있습니다. Node.js의 cluster 모듈이나 PM2 같은 프로세스 관리자로 여러 CPU 코어를 함께 쓸 수도 있습니다. 이미지, CSS, 자바스크립트 같은 정적 파일은 사용자와 가까운 CDN의 엣지 서버에서 보내 전송 지연을 줄입니다. 그리고 서버 로그를 분석하고, Prometheus나 Grafana 같은 모니터링 도구로 서버와 데이터베이스의 지표를 꾸준히 추적해 병목을 찾습니다.
결과를 해석하는 연습
실제 측정 대신, 같은 사용자 집단의 75번째 백분위가 주어진 연습 자료를 봅니다. 바꾸기 전이 LCP 3.1초 / INP 180ms / CLS 0.08이고 바꾼 뒤가 LCP 2.4초 / INP 260ms / CLS 0.08이라면, LCP는 좋아졌지만 INP가 좋음 기준을 벗어났습니다. 이미지를 빨리 보이게 한 변경이 긴 스크립트 실행을 늘리지 않았는지 확인해야 하며, 세 지표를 모두 통과했다고 말할 수 없습니다.
또 실험실의 측정 구간 안에서 작업 시간이 40ms, 70ms, 120ms였다면, TBT에 더해지는 몫은 각각 0, 70 - 50 = 20, 120 - 50 = 70ms로 합계 90ms입니다. 작업 시간의 합계 230ms와 TBT 90ms를 구별해야 합니다(web.dev의 TBT).
정리
- 성능 개선은 현장의 기준선을 기록하고, 실험실에서 재현해 병목을 찾고, 가설 하나를 바꿔 같은 조건에서 다시 측정하는 반복입니다.
- Core Web Vitals는 LCP(2.5초 이하), INP(200ms 이하), CLS(0.1 이하)이며, 모바일과 데스크톱을 나눠 실제 방문의 75번째 백분위로 판정합니다.
- 나빠진 지표마다 확인할 곳이 다르므로, LCP는 리소스와 렌더 지연을, INP는 입력 처리와 긴 작업을, CLS는 이동을 일으킨 요소를 봅니다.
- 프런트엔드는 리소스 크기, 렌더링을 막는 리소스, 코드 분할, 캐싱, DOM과 애니메이션 비용을 줄입니다.
- 백엔드는 비동기 I/O와 캐시, 인덱스와 쿼리 개선, 연결 풀, 확장과 CDN, 모니터링으로 응답 시간을 줄입니다.
- 한 지표를 개선할 때는 다른 지표와 접근성이 나빠지지 않았는지 함께 확인합니다.
연습 문제
1. TBT 계산
같은 측정 구간의 작업 시간이 50ms, 51ms, 99ms라면 TBT는 얼마일까요?
해설 보기
0 + 1 + 49 = 50ms입니다. 50ms인 작업은 50ms를 넘지 않았으므로 더하지 않고, 51ms는 1ms, 99ms는 49ms만 더합니다. 50ms를 넘은 작업의 전체 시간을 더하는 것이 아니라 넘은 부분만 더한다는 점이 핵심입니다.
2. 빠진 지표
LCP의 75번째 백분위가 2.3초이고 INP가 180ms라는 데이터는 있지만, CLS의 표본은 없습니다. 이 페이지가 세 지표를 모두 통과했다고 판단할 수 있을까요?
해설 보기
판단할 수 없습니다. LCP와 INP는 기준을 만족하지만 CLS는 데이터가 없으므로, 전체 통과 여부는 아직 확인되지 않은 상태입니다. 없는 값을 0이나 좋음으로 채워 넣지 않고, CLS를 측정할 수 있도록 데이터를 더 모읍니다.
3. 조건이 다른 비교
바꾸기 전에는 저성능 휴대전화와 빈 캐시에서, 바꾼 뒤에는 데스크톱과 이미 채워진 캐시에서 한 번씩 측정했더니 LCP가 크게 줄었습니다. 이것을 코드 변경의 효과라고 말할 수 있을까요?
해설 보기
말할 수 없습니다. 코드뿐 아니라 기기와 캐시 조건도 함께 바뀌었으므로, 무엇 때문에 빨라졌는지 나눌 수 없습니다.
같은 기기, 네트워크, 캐시 조건에서 여러 번 측정해 가설을 좁히고, 현장 데이터에서도 수집 기간과 표본, 사용자 행동을 비교해야 합니다. 파일 크기가 줄었다고 LCP나 INP가 같은 비율로 줄어드는 것도 아닙니다.
다음 글 asset 최적화에서는 이미지, 글꼴, CSS, 자바스크립트 같은 정적 파일을 더 작고 빠르게 전달하는 방법을 살펴봅니다.