웹 성능 측정과 최적화
🎯 핵심 질문
왜 당신의 웹 페이지는 느리게 로딩되고, 사용자들은 여전히 끊김 현상에 불만을 토로할까요? 이것은 마치 "왜 식당은 음식이 늦게 나오고 손님들은 짜증을 내는가?"라고 묻는 것과 같습니다. 이 장에서는 프론트엔드 성능 최적화의 핵심 개념을 깊이 있게 이해하여, 당신의 웹 페이지를 "날아다니게" 만들어 보겠습니다.
1. 왜 "성능 최적화"가 필요한가?
1.1 동작에서 사용성으로: 성능 최적화의 진화
10년 전의 웹 페이지는 매우 단순했습니다. 한 페이지가 고작 몇 KB에 불과했고, 로딩 속도는 거의 지연을 느낄 수 없을 정도였죠. 그때는 성능 최적화를 전혀 고려할 필요가 없었습니다 — 문제가 발생하지 않았으니까요.
하지만 지금은 완전히 다릅니다. 현대 웹 페이지의 복잡도는 기하급수적으로 증가했습니다. 이커머스 홈페이지에는 수십 장의 고화질 이미지가, 소셜 플랫폼에는 수천 개의 피드가 동시에 로딩되고, 관리자 대시보드에는 수십 개의 인터랙티브 컴포넌트가 포함될 수 있습니다. 이 모든 "풍부한" 기능 뒤에는 방대한 코드량과 리소스 크기가 숨어 있으며, 제대로 최적화하지 않으면 사용자 경험은 엉망이 됩니다.
👴 10년 전의 웹 페이지
- 단일 페이지 크기는 몇 KB에서 수십 KB
- 텍스트와 소량의 이미지만 존재
- 사용자는 로딩 지연을 거의 느끼지 못함
- 성능 최적화 불필요
🚀 현대의 웹 페이지
- 단일 페이지가 수 MB 이상일 수 있음
- 고화질 이미지, 영상, 인터랙티브 컴포넌트 존재
- 로딩 느림, 스크롤 끊김, 클릭 반응 지연
- 반드시 성능 최적화가 필요함
이것이 바로 "성능 최적화"가 해결하려는 문제입니다: 사용자의 대기 시간을 줄이고, 조작을 더 부드럽게 만드는 것.
1.2 실제 경험담: 왜 성능 최적화를 알아야 하는가
"요즘 네트워크도 빠르고 기기 성능도 좋은데, 성능 최적화가 아직도 필요한가요?"라고 생각할 수 있습니다. 실제 이야기를 하나 들려드리면, 왜 이 지식이 그토록 중요한지 이해하게 될 것입니다.
샤오왕의 성능 최적화 실패기
샤오왕은 신입 프론트엔드 엔지니어로, 회사의 이커머스 홈페이지 개발을 맡았습니다. 최신 Vue 3, 가장 인기 있는 UI 라이브러리를 사용했고, 기능은 매우 완성도 있게 구현했습니다. 회사의 고성능 컴퓨터에서 테스트했을 때는 모든 것이 정상이었습니다.
하지만 출시 다음 날, 고객센터가 난리가 났습니다 — "사이트가 너무 느려요", "이미지가 로딩되지 않아요", "버튼을 눌러도 한참 동안 반응이 없어요"라는 사용자 불만이 쏟아졌습니다. 샤오왕은 개발 머신에서 테스트해보니 전혀 문제가 없었고, 도대체 무엇이 문제인지 이해할 수 없었습니다.
나중에 선배의 도움을 받아 문제를 진단했는데, 선배는 일반 노트북에 일반 4G 네트워크를 연결한 후 웹사이트를 테스트해보라고 했습니다. 샤오왕은 그제야 충격을 받았습니다. 홈페이지 로딩에 10초 이상 걸리고, 리스트를 스크롤할 때 마치 PPT처럼 끊겼으며, 버튼을 클릭한 후 몇 초가 지나야 반응이 왔습니다.
알고 보니 샤오왕의 개발 환경은 최고 사양의 MacBook Pro + 기가비트 광케이블이었지만, 대부분의 사용자는 일반 기기 + 모바일 네트워크를 사용하고 있었습니다. 그가 작성한 코드에는 수십 장의 압축되지 않은 고화질 이미지가 있었고, UI 라이브러리 전체를 가져왔지만 실제로는 몇 개의 컴포넌트만 사용했으며, 렌더링 시 대량의 동기 계산을 수행하고 있었습니다.
해결책은 사실 복잡하지 않았습니다: 이미지 압축, 필요에 따른 컴포넌트 로딩, 계산을 백그라운드 스레드로 이동, 가상 리스트 사용. 이렇게 변경한 후, 홈페이지 로딩 시간은 10초 이상에서 2초로 줄었고, 스크롤도 매우 부드러워졌으며, 사용자 불만은 즉시 사라졌습니다.
샤오왕은 이로부터 한 가지 교훈을 얻었습니다: 성능 최적화를 모르면, 당신이 작성한 코드는 자신의 컴퓨터에서는 빠르게 실행되지만, 사용자 기기에서는 전혀 사용할 수 없을 수도 있다.
💡 핵심 교훈
성능 최적화는 선택 사항이 아니라 필수 기술입니다. 사용자의 관점에서 생각해야 합니다 — 그들은 일반 기기와 일반 네트워크를 사용하고 있습니다. 당신의 코드가 그들의 기기에서 제대로 실행되지 않는다면, 최적화가 필요하다는 뜻입니다.
2. 핵심 개념: 로딩, 렌더링, 인터랙션
🤔 이 개념들이 성능과 무슨 관계가 있나요?
로딩, 렌더링, 인터랙션은 사용자가 웹 페이지를 방문할 때 거치는 세 가지 핵심 단계로, 각 단계가 성능 병목의 원인이 될 수 있습니다.
사용자가 당신의 웹 페이지를 방문할 때 순차적으로 경험하는 것:
- 로딩 → HTML/CSS/JS/이미지를 서버에서 브라우저로 다운로드
- 렌더링 → 다운로드한 콘텐츠를 사용자가 볼 수 있는 페이지로 "그려내기"
- 인터랙션 → 사용자의 클릭, 스크롤 등의 조작에 응답하기
따라서, 성능 최적화는 이 세 단계를 모두 빠르게 만드는 것입니다. 이를 이해해야만 성능 병목이 어디에 있는지, 어떤 방법으로 최적화해야 하는지 알 수 있습니다.
구체적인 최적화 기법을 배우기 전에, 먼저 이 핵심 개념들을 확실히 이해해야 합니다. 더 잘 이해할 수 있도록, 식당 비유를 통해 이들의 관계를 설명해 보겠습니다.
2.1 식당 비유로 세 단계 이해하기
당신이 식당에 식사하러 가는 과정을 상상해 보세요. 이 과정은 웹 페이지 방문과 놀라울 정도로 유사합니다:
| 단계 | 🍽️ 식당 비유 | 실제 역할 | 구체적인 예시 |
|---|---|---|---|
| 로딩 | 식재료를 창고에서 주방으로 운반 | HTML/CSS/JS/이미지를 서버에서 브라우저로 다운로드 | 사용자가 웹 페이지를 열면, 브라우저가 각종 리소스를 다운로드하기 시작 |
| 렌더링 | 요리사가 식재료를 요리로 가공 | 브라우저가 코드를 사용자가 볼 수 있는 페이지로 전환 | 브라우저가 HTML 파싱, 레이아웃 계산, 페이지 그리기 |
| 인터랙션 | 웨이터가 손님의 요구에 응대 | 브라우저가 클릭, 스크롤 등의 조작에 응답 | 사용자가 버튼을 클릭하면 페이지가 피드백을 제공 |
2.2 로딩 (Loading): 식재료 운반
로딩은 웹 페이지에 필요한 각종 리소스(HTML, CSS, JavaScript, 이미지, 폰트 등)를 서버에서 브라우저로 다운로드하는 과정을 말합니다. 이 과정은 마치 식재료를 창고에서 주방으로 운반하는 것과 같아서, 운반이 느리거나 식재료가 너무 많으면 주방은 기다릴 수밖에 없습니다.
왜 로딩이 느릴까요? 주로 세 가지 이유가 있습니다. 첫째, 리소스 크기가 너무 큽니다 — 압축되지 않은 고화질 이미지 한 장이 5MB에 달할 수 있는데, 이는 소설 한 권을 다운로드하는 것과 맞먹습니다. 둘째, 네트워크 지연 — 서버가 해외에 있거나 사용자가 모바일 네트워크를 사용하면 각 요청마다 오래 기다려야 합니다. 마지막으로, 요청이 너무 많습니다 — 브라우저가 동시에 다운로드할 수 있는 리소스 수가 제한되어 있어, 너무 많은 리소스는 줄을 서야 합니다.
🔍 로딩 단계에서 어떤 일이 일어나는지 살펴보기
사용자가 브라우저 주소창에 URL을 입력하고 Enter를 누르면, 순서대로 다음과 같은 일이 발생합니다:
- DNS 조회: 도메인(예:
www.example.com)을 IP 주소(예:192.168.1.1)로 변환. 마치 전화번호부로 식당 주소를 찾는 것과 같습니다 - TCP 연결: 브라우저와 서버가 연결을 수립. 전화를 걸기 전에 먼저 다이얼하는 것과 같습니다
- TLS 핸드셰이크: 보안 연결(HTTPS) 수립. 상대방의 신원을 확인하는 것과 같습니다
- 리소스 요청: 브라우저가 서버에 HTML 파일을 요청
- HTML 파싱: 브라우저가 HTML을 파싱하여 CSS, JS, 이미지 등 리소스가 필요하다는 것을 발견하고 계속 요청
- 리소스 다운로드: 필요한 모든 리소스를 로컬로 다운로드
- 렌더링 시작: 다운로드가 완료되면 페이지 렌더링 시작
앞의 1-4단계를 "첫 바이트 시간"(TTFB)이라고 하고, 뒤의 5-7단계가 실제 리소스 다운로드 시간입니다.
일반적인 로딩 최적화 방법:
- 리소스 압축: 파일을 작게 만들기 (Gzip, Brotli 압축)
- CDN 사용: 파일을 사용자와 더 가까운 서버에 저장
- 지연 로딩: 사용자가 볼 수 있는 콘텐츠만 로딩하고, 나머지는 스크롤할 때 로딩
- 코드 분할: 큰 파일을 작은 파일로 나누어 필요할 때 로딩
2.3 렌더링 (Rendering): 요리사가 요리하기
렌더링은 브라우저가 다운로드한 HTML, CSS, JavaScript를 사용자가 볼 수 있는 페이지로 변환하는 과정입니다. 이 과정은 마치 요리사가 식재료를 요리로 가공하는 것과 같아서, 공정이 복잡하고 단계가 많으면 음식이 늦게 나옵니다.
📖 "렌더링"이란 무엇인가요?
"렌더링"이라는 단어를 들어본 적이 있을 텐데, 정확히 무엇일까요?
간단히 말해, 렌더링은 코드를 화면으로 바꾸는 과정입니다.
브라우저가 해야 할 일은 다음과 같습니다:
- HTML 파싱 → DOM 트리 생성 (페이지의 구조)
- CSS 파싱 → CSSOM 트리 생성 (페이지의 스타일)
- 병합 → 렌더 트리 생성 (구조와 스타일의 결합)
- 레이아웃 → 각 요소의 위치와 크기 계산
- 페인트 → 요소 그리기
- 합성 → 여러 레이어를 최종 화면으로 병합
이 과정은 매우 복잡하며, 어느 한 단계에서 문제가 발생하면 페이지가 끊기게 됩니다.
왜 렌더링이 느릴까요? 주로 두 가지 이유가 있습니다. 첫째, 페이지가 너무 복잡합니다 — 페이지에 수만 개의 DOM 노드가 있으면, 브라우저가 레이아웃 계산과 페인트에 많은 시간을 소모합니다. 둘째, 페이지를 자주 수정합니다 — JavaScript 코드가 DOM을 자주 수정하면, 브라우저가 반복적으로 레이아웃과 페인트를 다시 수행하게 되어 많은 성능을 소모합니다.
📁 렌더링 단계에서 어떤 일이 일어나는지 살펴보기
렌더링의 전체 흐름:
HTML (문자열)
↓
[HTML 파싱] → DOM 트리 생성
↓
DOM 트리 (페이지 구조)
CSS (스타일시트)
↓
[CSS 파싱] → CSSOM 트리 생성
↓
CSSOM 트리 (페이지 스타일)
DOM 트리 + CSSOM 트리
↓
[병합] → 렌더 트리 생성
↓
렌더 트리 (렌더링할 요소)
↓
[레이아웃 Layout] → 각 요소의 위치와 크기 계산
↓
[페인트 Paint] → 색상 채우기, 텍스트 그리기
↓
[합성 Composite] → 여러 레이어 병합
↓
최종 화면핵심 렌더링 경로 (Critical Rendering Path): 브라우저는 가능한 한 빨리 첫 화면 콘텐츠를 렌더링하여 사용자가 "사이트가 빠르다"고 느끼게 해야 합니다. 이를 "핵심 렌더링 경로 최적화"라고 합니다.
👇 직접 확인해보기: 아래 데모는 브라우저가 페이지를 어떻게 렌더링하는지 보여줍니다. "다음"을 클릭하여 렌더링의 각 단계를 관찰해 보세요:
⚠️ Common Bottlenecks
- Large assetsImages and JS bundles are not compressed, so downloads take longer.
- Too many requestsHTTP/1.1 head-of-line blocking makes resources wait in line.
- Network latencyThe server is physically far away, increasing RTT.
🚀 Solutions
- Asset compressionUse Gzip/Brotli and image formats such as WebP.
- Lazy loadingLoad only resources visible in the current viewport.
- CDN accelerationDistribute assets to nodes close to users.
- HTTP cachingUse browser cache to avoid repeated requests.
일반적인 렌더링 최적화 방법:
- 리플로우와 리페인트 줄이기: DOM을 자주 수정하지 않고,
top과width대신transform과opacity사용 - 가상 리스트: 보이는 영역의 콘텐츠만 렌더링하여 대량 데이터에서 성능이 크게 향상
- CSS 애니메이션: JavaScript 애니메이션 대신 CSS 애니메이션을 사용하면 성능이 더 좋음
2.4 인터랙션 (Interaction): 웨이터의 응대
인터랙션은 브라우저가 사용자 조작(클릭, 스크롤, 입력 등)에 응답하는 과정입니다. 이 과정은 마치 웨이터가 손님의 요구에 응대하는 것과 같아서, 웨이터가 너무 바쁘면 손님은 기다려야 합니다.
왜 인터랙션이 끊길까요? 주된 원인은 메인 스레드가 차단되었기 때문입니다. 브라우저의 JavaScript는 싱글 스레드이므로, 코드가 복잡한 계산을 실행 중이면 사용자 조작에 응답할 수 없어 페이지가 끊기게 됩니다.
🤔 "메인 스레드"란 무엇인가요?
브라우저에는 여러 스레드가 있지만, JavaScript 실행, 페이지 렌더링, 사용자 조작 응답을 담당하는 것은 오직 하나 — 메인 스레드뿐입니다.
메인 스레드를 바쁜 웨이터라고 상상해 보세요. 그는 많은 일을 해야 합니다:
- JavaScript 코드 실행 (데이터 계산, API 호출)
- 페이지 렌더링 (레이아웃, 페인트)
- 사용자 조작 응답 (버튼 클릭, 페이지 스크롤)
문제는 바로 이것입니다: 그는 혼자입니다. 만약 그가 복잡한 JavaScript 계산을 실행 중이라면(예: 1만 개의 데이터 처리), 이때 사용자가 버튼을 클릭해도 즉시 응답할 수 없고, 계산이 끝날 때까지 기다려야 합니다. 이것이 바로 끊김 현상의 근원입니다.
해결책:
- 복잡한 계산을 Web Worker(백그라운드 스레드)로 이동
- 타임 슬라이싱을 사용하여 큰 작업을 작은 작업으로 분할
- 동기식 복잡한 작업을 피하고 비동기식으로 전환
👇 직접 해보기: 아래 데모는 동기 계산과 Web Worker의 차이를 비교합니다. "계산 시작"을 클릭하여 페이지가 끊기는지 관찰해 보세요:
일반적인 인터랙션 최적화 방법:
- 디바운스와 스로틀: 이벤트 발생 빈도 제한 (예: 스크롤 이벤트, 입력 이벤트)
- Web Worker: 복잡한 계산을 백그라운드 스레드로 이동하여 메인 스레드를 차단하지 않음
- 타임 슬라이싱: 큰 작업을 작은 작업으로 나누어 브라우저가 사용자 조작에 응답할 기회를 제공
3. 실전: 한 팀의 성능 최적화 진화 여정
지금까지 많은 개념을 설명했으니, 실제 사례를 살펴보겠습니다: 한 스타트업이 "성능을 전혀 고려하지 않은 상태"에서 "체계적인 성능 최적화"로 어떻게 진화했는지. 이 사례를 통해 성능 최적화가 실제로 어떤 문제를 해결하는지 더 직관적으로 이해할 수 있습니다.
3.1 진화의 전체 그림
아래 표는 성능 최적화의 네 단계를 보여줍니다. 최적화 방법, 도구, 지표가 어떻게 단계별로 진화하는지 확인할 수 있습니다:
| 단계 | 최적화 방법 | 모니터링 도구 | 핵심 지표 | 핵심 변화 |
|---|---|---|---|---|
| 1단계: 원시 시대 | 없음 (고려하지 않음) | 없음 (감에 의존) | 없음 | 성능 의식이 전혀 없음, 작동만 하면 됨 |
| 2단계: 수동 최적화 | 이미지 압축, 요청 수 줄이기 | 브라우저 Network 패널 | 페이지 로딩 시간 | 의식이 생기기 시작했으나 방법은 원시적 |
| 3단계: 체계적 최적화 | 코드 분할, 지연 로딩, 가상 리스트 | Lighthouse, Performance 패널 | FCP, LCP, TBT | 전문 도구를 사용하고 명확한 최적화 목표를 가짐 |
| 4단계: 지속적 최적화 | 성능 예산, CI/CD 검사 | RUM, Lighthouse CI | INP, CLS, 전체 링크 모니터링 | 성능을 개발 프로세스에 통합 |
📊 표에서 무엇을 읽을 수 있나요?
행별로 이 표를 해석해 보겠습니다:
1단계 → 2단계: "무의식"에서 "의식"으로. 이것이 중요한 첫걸음입니다 — 개발자가 성능이 문제라는 것을 인식하고 최적화를 시도하기 시작합니다. 하지만 최적화 방법은 비교적 원시적이며, 주로 감과 경험에 의존합니다.
2단계 → 3단계: "수동"에서 "체계적"으로. 이것이 질적 도약입니다 — 전문 도구(Lighthouse, Performance 패널)를 사용하여 성능 문제를 진단하고, 과학적인 방법(코드 분할, 지연 로딩)으로 최적화하며, 감에 의존하지 않습니다.
3단계 → 4단계: "일회성 최적화"에서 "지속적 최적화"로. 성능 최적화가 개발 프로세스의 일부가 되면, 모니터링 체계(RUM, 실제 사용자 모니터링)를 구축하고, 개발 단계에서 성능 예산을 설정하여 성능 저하를 방지해야 합니다.
요약하자면: 성능 최적화의 진화는 단순히 "더 많은 기술을 사용하는 것"이 아니라, 사고방식 전체의 업그레이드입니다 — 수동적 대응에서 능동적 예방으로, 감에 의존하는 것에서 데이터 기반으로, 일회성 최적화에서 지속적 개선으로.
3.2 1단계: 원시 시대 — 전혀 고려하지 않음
왜 "원시 시대"라고 부를까요? 이 단계에서는 성능 문제를 전혀 고려하지 않았기 때문입니다 — 작동만 하면 됐죠. 팀은 3명뿐이고 간단한 기업 웹사이트를 만들고 있었으며, 프로젝트가 작아서 별문제가 없어 보였습니다.
하지만 프로젝트가 커지고 사용자가 늘어나면서 문제가 드러나기 시작했습니다.
개발 방식:
- 최적화 방법: 없음, 바로 개발, 성능 고려 안 함
- 모니터링 도구: 없음, 감으로 빠르고 느림을 판단
- 핵심 지표: 없음
이 단계의 특징:
- ✅ 장점: 개발이 빠르고 추가 학습 비용이 없음
- ❌ 단점: 사용자 경험이 나쁘고, 네트워크가 느리면 전혀 사용할 수 없음
당시의 문제점 보기
발생한 구체적인 문제:
- 이미지가 너무 큼: 제품 관리자가 5MB 크기의 홈페이지 배너 이미지를 업로드하여, 모바일 네트워크 사용자는 웹 페이지를 열기 위해 1분을 기다려야 함
- 압축이 안 됨: CSS와 JS 파일이 전혀 압축되지 않아, 용량이 압축 후의 3배
- 캐시가 없음: 매번 방문할 때마다 모든 리소스를 다시 다운로드해야 하며, 기존 사용자도 기다려야 함
- 동기 로딩: 모든 JS 파일이
<head>에서 동기적으로 로딩되어 페이지 렌더링을 차단
사용자 피드백:
- "당신 사이트 왜 안 열려요?"
- "이미지가 한참 동안 로딩되지 않고 그냥 하얗게 보여요"
- "버튼을 눌러도 반응이 없는데, 사이트가 망가진 건가요?"
당시의 임시 해결책:
<!-- 로딩 마스크로 사용자를 "속이기" -->
<div id="loading">로딩 중...</div>
<script>
// 페이지 로딩이 완료된 후에야 마스크 제거
window.onload = function() {
document.getElementById('loading').style.display = 'none'
}
</script>이것은 완전히 "자기 위안"일 뿐입니다 — 페이지는 여전히 느리지만, 사용자가 보지 못할 뿐이죠.
3.3 2단계: 수동 최적화 — 의식이 생기기 시작
원시 시대의 문제가 어느 정도 쌓이자, 팀은 마침내 성능 최적화를 시작하기로 결정했습니다. 이것은 중요한 전환점입니다 — "전혀 고려하지 않음"에서 "의식적으로 최적화"하는 것으로.
하지만 이 단계의 최적화는 비교적 원시적이며, 주로 이미지 압축, 파일 병합 등의 단순한 방법에 의존합니다.
개발 방식:
- 최적화 방법: 수동으로 이미지 압축, CSS/JS 파일 병합, HTTP 요청 수 줄이기
- 모니터링 도구: 브라우저 Network 패널, 간단한 타이밍 로그
- 핵심 지표: 페이지 로딩 시간 (수동으로 스톱워치로 측정)
이 단계의 특징:
- ✅ 장점: 눈에 띄는 개선이 있고, 사용자 불만이 줄어듦
- ❌ 단점: 최적화가 체계적이지 않고, 쉽게 재발하며, 정량적 지표가 부족함
수동 최적화의 구체적인 방법 보기
수동 최적화 방법:
수동 이미지 압축:
- Photoshop으로 각 이미지를 수동으로 "Web용으로 저장"
- PNG를 JPEG로 변환 (손실 압축이지만 용량이 훨씬 작음)
- 이미지 크기 축소 (예: 2000px 너비의 이미지를 800px로 축소)
수동 파일 병합:
html<!-- 최적화 전: 10개의 JS 파일 = 10개의 요청 --> <script src="utils.js"></script> <script src="api.js"></script> <script src="component-a.js"></script> <script src="component-b.js"></script> ...(6개 더 있음) <!-- 최적화 후: 1개의 병합된 JS 파일 = 1개의 요청 --> <script src="all.js"></script>CSS/JS를 페이지 하단으로 이동:
html<body> <!-- 페이지 콘텐츠 --> <h1>환영합니다</h1> <!-- 최적화: CSS/JS를 마지막에 배치 --> <link rel="stylesheet" href="style.css"> <script src="app.js"></script> </body>
가져온 개선:
- 이미지 용량이 5MB에서 500KB로 감소 (90% 감소)
- HTTP 요청 수가 30개에서 5개로 감소
- 페이지 로딩 시간이 30초에서 8초로 감소
새로운 고민거리:
- 수동 작업량이 큼: 매번 업데이트할 때마다 수동으로 이미지 압축, 파일 병합을 해야 함
- 잊기 쉬움: 신입은 최적화가 필요하다는 것을 모르고 원본 이미지를 직접 업로드
- 정량화 부족: "조금 빨라졌다"는 알지만 구체적으로 얼마나 빨라졌는지 모름
3.4 3단계: 체계적 최적화 — 도구와 데이터로 말하기
2단계의 문제(수동 작업량이 많고 정량화가 부족함)는 팀을 오랫동안 괴롭혔습니다. 이후 팀이 Lighthouse, Performance 패널 등의 전문 도구를 발견하면서 체계적 최적화 시대로 진입했습니다.
이 단계의 핵심은 데이터 기반 최적화입니다 — 먼저 도구로 문제를 진단하고, 성능 병목을 찾은 다음, 표적 최적화를 수행합니다.
개발 방식:
- 최적화 방법: 코드 분할, 지연 로딩, 가상 리스트, 이미지 자동 압축
- 모니터링 도구: Lighthouse, Chrome Performance 패널, WebPageTest
- 핵심 지표: FCP (첫 화면 시간), LCP (최대 콘텐츠 렌더링), TBT (총 차단 시간)
체계적 최적화의 구체적인 방법
Lighthouse로 문제 진단하기:
Lighthouse는 Google이 개발한 자동화된 성능 테스트 도구로, 종합적인 성능 리포트와 최적화 제안을 제공합니다.
# Lighthouse로 웹 페이지 테스트
lighthouse https://www.example.com --viewLighthouse가 제공하는 것:
- 성능 점수 (0-100점)
- 핵심 지표 (FCP, LCP, CLS, TBT, INP)
- 최적화 제안 (예: "텍스트 압축 활성화", "사용되지 않는 JavaScript 제거")
핵심 지표 해석:
| 지표 | 전체 이름 | 의미 | 이상적인 값 |
|---|---|---|---|
| FCP | First Contentful Paint | 첫 콘텐츠 렌더링 시간 (사용자가 첫 번째 콘텐츠를 보는 시간) | <1.8s |
| LCP | Largest Contentful Paint | 최대 콘텐츠 렌더링 시간 (주요 콘텐츠 로딩 완료 시간) | <2.5s |
| TBT | Total Blocking Time | 총 차단 시간 (메인 스레드가 차단된 총 시간) | <200ms |
| CLS | Cumulative Layout Shift | 누적 레이아웃 이동 (페이지 요소가 이리저리 움직이는 정도) | <0.1 |
이 단계의 특징:
- ✅ 장점: 최적화가 표적화되어 있고, 효과가 좋으며, 정량적 지표가 있음
- ❌ 단점: 도구와 지표를 학습해야 하며, 일정한 진입 장벽이 있음
체계적 최적화의 구체적인 기술 보기
1. 코드 분할 (Code Splitting):
큰 파일을 작은 파일로 나누어 필요할 때 로딩합니다. 예를 들어 사용자가 홈페이지를 방문할 때는 홈페이지에 필요한 코드만 로딩하고, "회사 소개"를 클릭할 때 회사 소개 페이지 코드를 로딩합니다.
// 최적화 전: 모든 코드가 하나의 파일에 있고, 한 번에 로딩
import About from './views/About.vue'
import Contact from './views/Contact.vue'
// ... 10개의 페이지가 더 있음
// 최적화 후: 지연 로딩, 방문할 때만 로딩
const About = () => import('./views/About.vue')
const Contact = () => import('./views/Contact.vue')효과: 홈페이지 로딩 코드량이 70% 감소하고, 첫 화면 시간이 5초에서 1.5초로 감소.
2. 이미지 지연 로딩 (Lazy Loading):
사용자가 볼 수 있는 이미지만 로딩하고, 보이는 영역으로 스크롤할 때 다른 이미지를 로딩합니다.
<!-- 최신 브라우저는 네이티브 지연 로딩을 지원 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />효과: 홈페이지에서 로딩되는 이미지 수가 20장에서 3장으로 감소하고, 대역폭 80% 절약.
3. 가상 리스트 (Virtual Scrolling):
10,000개의 데이터를 렌더링해야 한다면, 실제로 10,000개의 DOM 노드를 생성하지 않고, 보이는 영역의 20개만 렌더링하며 스크롤 시 동적으로 교체합니다.
<!-- vue-virtual-scroller 컴포넌트 사용 -->
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>효과: 10,000개의 데이터가 "멈춤"에서 "부드러운 스크롤"로 바뀌고, 메모리 사용량이 95% 감소.
3.5 4단계: 지속적 최적화 — 성능을 개발 프로세스에 통합
도구와 방법이 성숙해지자, 팀은 더 깊은 문제에 주목하기 시작했습니다: 어떻게 성능 저하를 방지할 것인가? 어떻게 성능을 개발 프로세스의 일부로 만들 것인가?
이 단계의 핵심은 성능 모니터링과 예산 체계 구축입니다 — 출시 후에 최적화하는 것이 아니라, 개발 단계에서 성능 문제를 예방합니다.
개발 방식:
- 최적화 방법: 성능 예산 (Performance Budget), Lighthouse CI, 실제 사용자 모니터링 (RUM)
- 모니터링 도구: Lighthouse CI, WebPageTest API, Google Analytics
- 핵심 지표: INP (인터랙션 지연), CLS (레이아웃 이동), 전체 링크 모니터링
지속적 최적화의 구체적인 방법
1. 성능 예산 설정:
번들 설정에서 제한을 설정하고, 초과하면 오류를 발생시켜 "의도치 않게 대용량 파일을 도입하는 것"을 방지합니다.
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
// 단일 파일이 200KB를 초과하지 않도록 제한
chunkFileNames: 'js/[name]-[hash].js',
}
},
// 200KB 초과 시 경고 발생
chunkSizeWarningLimit: 200
}
})2. Lighthouse CI:
코드를 제출할 때마다 자동으로 Lighthouse 테스트를 실행하고, 성능 점수가 하락하면 병합을 차단합니다.
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://staging.example.com
budgetPath: ./budget.json3. 실제 사용자 모니터링 (RUM):
개발 환경에서만 테스트하는 것이 아니라, 실제 사용자 브라우저에서 성능 데이터를 수집합니다.
// 성능 데이터를 서버로 전송
const perfData = performance.getEntriesByType('navigation')[0]
const lcp = performance.getEntriesByType('largest-contentful-paint')[0]
fetch('/api/perf', {
method: 'POST',
body: JSON.stringify({
fcp: perfData.loadEventEnd - perfData.fetchStart,
lcp: lcp.renderTime || lcp.loadTime,
url: window.location.href
})
})효과:
- 성능 저하를 적시에 발견 가능 (예: 특정 커밋으로 인해 LCP가 2초에서 5초로 증가)
- 실제 사용자의 경험을 이해 가능 (개발 환경의 "이상적인 상태"가 아님)
- 가장 느린 10% 사용자에 대해 표적 최적화 가능
이 단계에서 하는 일:
- 성능 예산: 파일 크기, 요청 수를 제한하고 초과 시 알람
- CI/CD 검사: 코드 제출 시마다 자동으로 성능 테스트, 저하 시 병합 차단
- 실제 사용자 모니터링: 실제 사용자의 성능 데이터를 수집하여 지속적 개선
- 정기 성능 리포트: 주간/월간 성능 리포트를 생성하여 트렌드 추적
4. 일반적인 성능 병목과 해결책
지금까지 많은 이론을 설명했으니, 실제 개발에서 가장 흔히 발생하는 성능 문제와 그 해결 방법을 살펴보겠습니다.
4.1 이미지 로딩이 느림
문제 증상: 이미지가 한참 동안 로딩되지 않거나, 로딩 과정에서 페이지가 이리저리 움직입니다.
원인:
- 이미지 용량이 너무 큼 (고화질 원본)
- 이미지 크기가 너무 큼 (2000px 너비의 이미지가 200px로 표시됨)
- 지연 로딩이 적용되지 않음 (모든 이미지를 한 번에 로딩)
해결책:
- 최신 이미지 포맷 사용 (WebP, AVIF):
<!-- 최신: WebP 포맷, 용량 30-70% 감소 -->
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="이미지">
</picture>- 반응형 이미지 (기기 크기에 따라 다른 크기 로딩):
<!-- 작은 기기는 작은 이미지, 큰 기기는 큰 이미지 로딩 -->
<img
src="image-800.jpg"
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
alt="반응형 이미지">- 지연 로딩 (사용자가 스크롤할 때 로딩):
<!-- 최신: 네이티브 지연 로딩 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />👇 직접 해보기: 아래 데모는 지연 로딩과 일반 로딩의 차이를 비교합니다. 네트워크 요청을 관찰해 보세요:
Detailed Comparison
| Format | Size | Quality | Transparency | Animation | Recommendation |
|---|---|---|---|---|---|
| JPEG | Medium | Good | ✗ | ✗ | ★★★★☆ |
| PNG | Large | Perfect | ✓ | ✗ | ★★★★★ |
| WebP | Small | Excellent | ✓ | ✓ | ★★★★★ |
| AVIF | Smallest | Outstanding | ✓ | ✗ | ★★★★★ |
Optimization Tips
- Prefer WebP to reduce size by 30-50%.
- Provide JPEG/PNG fallbacks for older browsers.
- Use the <picture> element for automatic fallback.
- Use JPEG for photos and PNG or SVG for icons.
Recommended Tools
- Squoosh: open-source image compression from Google.
- ImageOptim: image optimization tool for macOS.
- TinyPNG: online smart compression with WebP support.
- Sharp: Node.js image processing library for automation.
4.2 첫 화면 로딩이 느림
문제 증상: 사용자가 웹 페이지를 열면, 하얀 화면이 오래 지속됩니다.
원인:
- 불필요한 코드를 너무 많이 로딩
- 핵심 렌더링 경로가 차단됨
- 코드 분할이 적용되지 않음
해결책:
- 코드 분할 (Code Splitting):
// 라우트 지연 로딩: 방문할 때만 로딩
const routes = [
{
path: '/about',
component: () => import('./views/About.vue') // /about 방문 시에만 로딩
}
]- 핵심 리소스 사전 로딩 (Preload):
<!-- 브라우저에 미리 알리기: 이 리소스는 중요하니 우선 로딩 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">- 핵심 CSS 인라인화:
<!-- 첫 화면에 필요한 CSS를 HTML에 직접 인라인 -->
<style>
/* 첫 화면 핵심 스타일 */
.hero { background: #000; color: #fff; }
</style>4.3 스크롤 끊김
문제 증상: 페이지를 스크롤할 때 끊기고 부드럽지 않습니다.
원인:
- 너무 많은 DOM 노드가 렌더링됨 (예: 10,000개의 데이터)
- 스크롤 이벤트 리스너에 복잡한 계산이 있음
- 레이아웃 계산이 자주 발생함
해결책:
- 가상 리스트 (Virtual Scrolling):
<!-- 보이는 영역의 콘텐츠만 렌더링 -->
<RecycleScroller
:items="10000"
:item-size="50"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>👇 직접 확인해보기: 아래 데모는 일반 리스트와 가상 리스트의 성능 차이를 비교합니다:
- 스크롤 이벤트 스로틀링 (Throttle):
// 스크롤 이벤트 발생 빈도 제한 (최대 100ms마다 한 번)
const throttledScroll = throttle(() => {
updatePosition()
}, 100)
window.addEventListener('scroll', throttledScroll)- CSS
will-change사용:
/* 브라우저에 미리 알리기: 이 요소가 변경될 예정이니 준비하세요 */
.scroll-container {
will-change: transform;
}4.4 클릭 반응이 느림
문제 증상: 버튼을 클릭한 후 몇 초가 지나야 반응이 옵니다.
원인:
- 클릭 이벤트 핸들러에 복잡한 계산이 있음 (메인 스레드 차단)
- 디바운스가 적용되지 않음 (사용자가 빠르게 여러 번 클릭하면 여러 번 계산이 실행됨)
해결책:
- 클릭 이벤트 디바운스 (Debounce):
// 사용자가 클릭을 멈춘 후 300ms 후에 실행
const debouncedClick = debounce(() => {
submitForm()
}, 300)
button.addEventListener('click', debouncedClick)- Web Worker 사용 (계산을 백그라운드 스레드로 이동):
// 메인 스레드
const worker = new Worker('calculator.js')
button.addEventListener('click', () => {
worker.postMessage({ data: largeData })
})
worker.onmessage = (e) => {
// 계산 완료, 결과 표시
showResult(e.data.result)
}
// calculator.js (Worker 스레드)
self.onmessage = (e) => {
const result = heavyCalculation(e.data.data)
self.postMessage({ result })
}5. 성능 모니터링 도구
성능 최적화는 일회성 작업이 아니며, 지속적인 모니터링이 필요합니다. 아래는 자주 사용되는 도구들입니다.
5.1 브라우저 개발자 도구
Chrome DevTools는 가장 일반적으로 사용되는 성능 분석 도구입니다:
- Network 패널: 리소스 로딩 상황 확인
- Performance 패널: 런타임 성능 분석 (FPS, 메인 스레드 활동)
- Lighthouse: 원클릭 성능 리포트 생성
Performance 패널 사용 방법
- Chrome DevTools 열기 (F12)
- Performance 패널로 전환
- "Record" 버튼 클릭
- 웹 페이지 조작 (스크롤, 클릭 등)
- "Stop"을 클릭하여 녹화 중지
- 결과 분석: FPS(프레임 레이트), 메인 스레드 활동, 긴 작업 등 확인
5.2 Lighthouse
Lighthouse는 Google이 개발한 자동화된 성능 테스트 도구입니다:
# 명령줄에서 사용
lighthouse https://www.example.com --view
# 또는 Chrome DevTools에서 사용
# DevTools 열기 → Lighthouse → "Analyze page load" 클릭Lighthouse가 제공하는 것:
- 성능 점수 (0-100점)
- 핵심 지표 (FCP, LCP, CLS, TBT, INP)
- 최적화 제안 (영향도 순으로 정렬)
5.3 WebPageTest
WebPageTest는 온라인 성능 테스트 도구로, 여러 지역, 여러 기기에서 테스트할 수 있습니다:
# https://www.webpagetest.org 방문
# URL 입력, 테스트 지역과 기기 선택, "Start Test" 클릭WebPageTest가 제공하는 것:
- 워터폴 차트 (Waterfall): 각 리소스 로딩 타임라인
- 비디오 비교: 최적화 전후의 로딩 과정 비디오
- 최적화 제안
6. 성능 최적화 체크리스트
아래는 실용적인 성능 최적화 체크리스트입니다. 이 순서대로 웹 페이지를 최적화할 수 있습니다:
6.1 로딩 최적화
- ✅ 이미지 압축: WebP 포맷 사용, 품질 80-85%로 압축
- ✅ 반응형 이미지: 기기 크기에 따라 다른 크기의 이미지 로딩
- ✅ 지연 로딩: 이미지와 컴포넌트 지연 로딩, 보이는 콘텐츠만 로딩
- ✅ 코드 분할: 라우트별로 코드 분할, 필요에 따라 로딩
- ✅ 코드 압축: Gzip/Brotli 압축 활성화
- ✅ CDN 사용: 정적 리소스를 CDN에 배치하여 다운로드 가속
- ✅ 핵심 리소스 사전 로딩:
<link rel="preload">사용
6.2 렌더링 최적화
- ✅ 리플로우와 리페인트 줄이기:
top과width대신transform과opacity사용 - ✅ 가상 리스트: 대량 데이터 시 가상 스크롤 사용
- ✅ CSS 애니메이션: JavaScript 애니메이션보다 CSS 애니메이션을 우선 사용
- ✅ 핵심 렌더링 경로 최적화: 핵심 CSS 인라인화, 비핵심 CSS 지연 로딩
- ✅ @import 피하기:
@import는 렌더링을 차단하므로<link>로 대체
6.3 인터랙션 최적화
- ✅ 디바운스와 스로틀: 스크롤, 입력, resize 이벤트에 디바운스/스로틀 사용
- ✅ Web Worker: 복잡한 계산을 백그라운드 스레드로 이동
- ✅ 타임 슬라이싱: 큰 작업을 작은 작업으로 나누어 긴 작업 방지
- ✅ 동기 레이아웃 피하기: 루프 안에서 레이아웃 속성(예:
offsetHeight)을 읽지 않기
6.4 캐시 최적화
- ✅ HTTP 캐시: Cache-Control과 ETag 설정
- ✅ Service Worker: 정적 리소스를 캐시하여 오프라인 접근 구현
- ✅ LocalStorage: API 데이터를 캐시하여 요청 수 감소
- ✅ 메모리 캐시:
Map/Object를 사용하여 계산 결과 캐시
6.5 모니터링 최적화
- ✅ Lighthouse CI: 코드 제출 시마다 자동으로 성능 테스트
- ✅ 실제 사용자 모니터링: 실제 사용자의 성능 데이터 수집
- ✅ 성능 예산: 파일 크기 제한 설정, 초과 시 알람
- ✅ 정기 성능 리포트: 주간/월간 성능 트렌드 리포트 생성
7. 요약
프론트엔드 성능 최적화의 핵심 개념을 표로 정리해 보겠습니다:
| 개념 | 한 줄 설명 | 해결하는 문제 | 일반적인 방법 |
|---|---|---|---|
| 로딩 최적화 | 리소스 다운로드를 더 빠르게 | 첫 화면 느림, 대기 시간 김 | 이미지 압축, CDN, 코드 분할, 지연 로딩 |
| 렌더링 최적화 | 페이지를 더 빠르게 "그리기" | 스크롤 끊김, 클릭 느림 | 가상 리스트, 리플로우/리페인트 감소, CSS 애니메이션 |
| 인터랙션 최적화 | 응답을 더 빠르게 | 클릭 무반응, 조작 끊김 | 디바운스/스로틀, Web Worker, 타임 슬라이싱 |
| 캐시 최적화 | 중복 다운로드 방지 | 반복 방문 시 느림 | HTTP 캐시, Service Worker, LocalStorage |
| 모니터링 최적화 | 지속적으로 문제 발견 | 성능 저하 | Lighthouse, RUM, 성능 예산 |
마지막으로
성능 최적화는 지속적으로 진화하는 주제입니다. 도구는 변하지만, 핵심 이념은 변하지 않습니다: 사용자의 관점에서 생각하고, 대기 시간을 더 짧게, 조작을 더 부드럽게 만드는 것.
이러한 기본 원리를 이해한다면, 기술이 어떻게 변화하든 빠르게 적응하고 대처할 수 있습니다.
이 글이 프론트엔드 성능 최적화에 대한 전체적인 이해를 구축하는 데 도움이 되길 바랍니다. 실제 프로젝트에서 성능 문제가 발생했을 때, 어디서부터 시작하고, 어떻게 진단하며, 어떻게 해결해야 하는지 알 수 있게 되기를 바랍니다.