본문으로 건너뛰기

⚡ 웹 성능 최적화

📖 정의

웹 성능 최적화는 웹사이트의 로딩 속도와 응답성을 개선하여 사용자 경험을 향상시키는 과정입니다. 빠른 웹사이트는 사용자 만족도, SEO 순위, 전환율을 높입니다. 성능 최적화는 네트워크, 렌더링, JavaScript 실행, 이미지, 캐싱 등 다양한 영역을 포함합니다.

🎯 비유로 이해하기

레스토랑 서빙 속도

웹 성능을 레스토랑에 비유하면:

느린 레스토랑 (최적화 안 된 웹사이트)
├─ 주문받기 느림 (서버 응답 느림)
├─ 주방 혼잡 (비효율적 코드)
├─ 음식 한 번에 안 나옴 (리소스 분산)
└─ 손님 기다리다 떠남 (높은 이탈률)

빠른 레스토랑 (최적화된 웹사이트)
├─ 즉시 주문 접수 (빠른 서버)
├─ 효율적 주방 (최적화 코드)
├─ 코스 순서대로 (우선순위 로딩)
└─ 손님 만족 (낮은 이탈률)

고속도로 vs 시내 도로

최적화 전: 시내 도로
├─ 신호등 많음 (HTTP 요청 많음)
├─ 큰 차량들 (큰 파일 크기)
├─ 정체 (느린 로딩)
└─ 도착 시간 예측 불가

최적화 후: 고속도로
├─ 신호등 없음 (요청 최소화)
├─ 작은 차량들 (압축된 파일)
├─ 빠른 속도 (빠른 로딩)
└─ 정확한 도착 시간

⚙️ 작동 원리

1. 페이지 로딩 과정

1. DNS 조회
└─ example.com → IP 주소

2. TCP 연결
└─ 서버와 연결 수립

3. HTTP 요청
└─ HTML 파일 요청

4. 서버 응답
└─ HTML 파일 전송

5. HTML 파싱
└─ DOM 트리 생성

6. 리소스 로드
├─ CSS (CSSOM 생성)
├─ JavaScript (실행)
├─ 이미지
└─ 폰트

7. 렌더링
└─ 화면에 표시

2. Critical Rendering Path

HTML → DOM Tree
CSS → CSSOM Tree

Render Tree

Layout

Paint

Composite

💡 실제 예시

이미지 최적화

<!-- ❌ 최적화 안 됨 -->
<img src="photo.jpg">
<!-- 문제: 5MB 원본 이미지 로드 -->

<!-- ✅ 최적화됨 -->

<!-- 1. 적절한 크기 -->
<img
src="photo-800w.jpg"
width="800"
height="600"
alt="설명"
>

<!-- 2. 반응형 이미지 -->
<img
srcset="
photo-400w.jpg 400w,
photo-800w.jpg 800w,
photo-1200w.jpg 1200w
"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
src="photo-800w.jpg"
alt="설명"
>

<!-- 3. 차세대 포맷 -->
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="설명">
</picture>

<!-- 4. 지연 로딩 -->
<img
src="photo.jpg"
loading="lazy"
alt="설명"
>

<!-- 5. 블러 플레이스홀더 (Next.js) -->
<Image
src="/photo.jpg"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
alt="설명"
/>

CSS 최적화

<!-- ❌ 렌더링 블로킹 -->
<head>
<link rel="stylesheet" href="styles.css">
<!-- HTML 파싱 중단! -->
</head>

<!-- ✅ Critical CSS 인라인 -->
<head>
<style>
/* 초기 렌더링에 필요한 CSS만 */
body { margin: 0; font-family: sans-serif; }
.hero { height: 100vh; }
</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>
</head>
/* CSS 최적화 */

/* ❌ 비효율적 */
.container .item .link {
color: blue;
}

/* ✅ 효율적 */
.link {
color: blue;
}

/* ❌ 비싼 속성 */
.box {
box-shadow: 0 0 50px rgba(0,0,0,0.5);
filter: blur(10px);
}

/* ✅ transform/opacity 사용 (GPU 가속) */
.box {
transform: translateZ(0); /* GPU 가속 활성화 */
will-change: transform; /* 브라우저 힌트 */
}

JavaScript 최적화

// ❌ 동기 스크립트 (파싱 블로킹)
<script src="app.js"></script>

// ✅ defer (HTML 파싱 후 실행)
<script defer src="app.js"></script>

// ✅ async (다운로드 후 즉시 실행)
<script async src="analytics.js"></script>

// 코드 스플리팅 (React)
import React, { lazy, Suspense } from 'react';

// ❌ 모든 것을 번들에 포함
import HeavyComponent from './HeavyComponent';

// ✅ 필요할 때만 로드
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<HeavyComponent />
</Suspense>
);
}

// Tree Shaking
// ❌ 전체 라이브러리 import
import _ from 'lodash';
const result = _.debounce(func, 300);

// ✅ 필요한 함수만 import
import debounce from 'lodash/debounce';
const result = debounce(func, 300);

// 디바운싱
// ❌ 매번 실행
window.addEventListener('resize', () => {
console.log('Resized!'); // 100번 실행
});

// ✅ 디바운스로 최적화
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}

window.addEventListener('resize', debounce(() => {
console.log('Resized!'); // 1번만 실행
}, 300));

// Web Workers (무거운 작업)
// main.js
const worker = new Worker('heavy-worker.js');
worker.postMessage({ data: bigData });

worker.onmessage = (event) => {
console.log('결과:', event.data);
};

// heavy-worker.js
self.onmessage = (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
};

리소스 힌트

<head>
<!-- 1. DNS Prefetch: DNS 미리 조회 -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">

<!-- 2. Preconnect: DNS + TCP + TLS 미리 연결 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 3. Prefetch: 나중에 필요한 리소스 미리 다운로드 -->
<link rel="prefetch" href="next-page.html">

<!-- 4. Preload: 현재 페이지에 필요한 리소스 우선 다운로드 -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="preload" href="hero.jpg" as="image">

<!-- 5. Prerender: 페이지 전체를 미리 렌더링 -->
<link rel="prerender" href="next-page.html">
</head>

캐싱 전략

// HTTP 캐시 헤더 (서버)

// 1. 정적 리소스 (1년 캐시)
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true
}));

// Response Headers:
// Cache-Control: public, max-age=31536000, immutable
// ETag: "abc123"

// 2. HTML (항상 검증)
app.get('/', (req, res) => {
res.set('Cache-Control', 'no-cache');
res.send(html);
});

// 3. API (5분 캐시)
app.get('/api/data', (req, res) => {
res.set('Cache-Control', 'public, max-age=300');
res.json(data);
});

// Service Worker 캐시
// sw.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles.css',
'/app.js',
'/logo.png'
];

// 설치 시 캐시
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
});

// 요청 시 캐시 우선 전략
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 캐시에 있으면 반환, 없으면 네트워크
return response || fetch(event.request);
})
);
});

// Cache-First 전략
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}

// Network-First 전략
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
return caches.match(request);
}
}

폰트 최적화

/* ❌ 폰트 로딩 중 텍스트 안 보임 (FOIT) */
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
}

/* ✅ 폰트 로딩 중 시스템 폰트 표시 (FOUT) */
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
font-display: swap; /* 즉시 대체 폰트 표시 */
}

/* 폰트 서브셋 (필요한 문자만) */
/* 영문 + 한글 자음/모음만 */
@font-face {
font-family: 'MyFont';
src: url('font-korean-subset.woff2');
unicode-range: U+AC00-D7A3; /* 한글 범위 */
}
<!-- 구글 폰트 최적화 -->
<!-- ❌ 느림 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR" rel="stylesheet">

<!-- ✅ 빠름 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap" rel="stylesheet">

Webpack 최적화

// webpack.config.js

module.exports = {
mode: 'production',

// 코드 스플리팅
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10
}
}
},
// 런타임 코드 분리
runtimeChunk: 'single'
},

// 압축
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true // console.log 제거
}
}
}),
new CssMinimizerPlugin()
]
},

// Tree Shaking
optimization: {
usedExports: true,
sideEffects: false
},

// Bundle Analyzer
plugins: [
new BundleAnalyzerPlugin()
]
};

성능 측정

// Performance API

// 페이지 로드 시간
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0];

console.log('DNS:', perfData.domainLookupEnd - perfData.domainLookupStart);
console.log('TCP:', perfData.connectEnd - perfData.connectStart);
console.log('TTFB:', perfData.responseStart - perfData.requestStart);
console.log('DOM Load:', perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart);
console.log('Page Load:', perfData.loadEventEnd - perfData.loadEventStart);
});

// Core Web Vitals

// 1. LCP (Largest Contentful Paint)
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

// 2. FID (First Input Delay)
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ type: 'first-input', buffered: true });

// 3. CLS (Cumulative Layout Shift)
let cls = 0;
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
console.log('CLS:', cls);
}).observe({ type: 'layout-shift', buffered: true });

// 커스텀 메트릭
performance.mark('task-start');
// ... 작업 ...
performance.mark('task-end');
performance.measure('task-duration', 'task-start', 'task-end');

const measure = performance.getEntriesByName('task-duration')[0];
console.log('Task took:', measure.duration, 'ms');

🤔 자주 묻는 질문

Q1. 성능 최적화의 우선순위는?

A:

1순위: 사용자가 체감하는 것
├─ 초기 로딩 속도 (FCP, LCP)
├─ 인터랙션 반응성 (FID, INP)
└─ 시각적 안정성 (CLS)

2순위: 리소스 크기
├─ 이미지 최적화 (보통 가장 큰 용량)
├─ JavaScript 번들 크기
└─ CSS 크기

3순위: 네트워크
├─ HTTP 요청 수 줄이기
├─ 압축 (gzip, brotli)
└─ CDN 사용

4순위: 렌더링
├─ Critical CSS
├─ 폰트 최적화
└─ Reflow/Repaint 최소화

측정 → 분석 → 개선 → 반복

Q2. 이미지 포맷은 어떤 것을 사용해야 하나요?

A:

포맷 비교:

AVIF (최신, 가장 작음)
├─ 압축률: ⭐⭐⭐⭐⭐
├─ 품질: ⭐⭐⭐⭐⭐
└─ 지원: Chrome, Firefox

WebP (널리 지원)
├─ 압축률: ⭐⭐⭐⭐
├─ 품질: ⭐⭐⭐⭐
└─ 지원: Chrome, Firefox, Edge, Safari

JPEG (레거시)
├─ 압축률: ⭐⭐⭐
├─ 품질: ⭐⭐⭐
└─ 지원: 모든 브라우저

PNG (투명도)
├─ 압축률: ⭐⭐
├─ 품질: ⭐⭐⭐⭐
└─ 용도: 로고, 아이콘

SVG (벡터)
├─ 크기: 작음
├─ 확대: 무손실
└─ 용도: 아이콘, 로고

권장:
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="...">
</picture>

Q3. Lighthouse 점수를 100점 만들어야 하나요?

A:

❌ 완벽주의 함정

Lighthouse 100점 = 목표 아님
사용자 경험 = 진짜 목표

현실적인 목표:
- 모바일: 80-90점
- 데스크톱: 90-100점

점수보다 중요한 것:
1. 실제 사용자 경험
2. 비즈니스 목표 달성
3. 이탈률, 전환율

과최적화 주의:
- 개발 시간 낭비
- 복잡한 코드
- 유지보수 어려움

80-20 법칙:
20%의 노력으로 80%의 효과

Q4. CDN은 꼭 필요한가요?

A:

// CDN (Content Delivery Network)

// 장점:
1. 빠른 로딩
└─ 사용자와 가까운 서버에서 제공

2. 서버 부하 감소
└─ 정적 파일을 CDN이 처리

3. 글로벌 가용성
└─ 전 세계 어디서나 빠름

4. DDoS 방어
└─ 보안 강화

// 무료 CDN:
- Cloudflare (무료 플랜)
- jsDelivr (오픈소스)
- unpkg (npm 패키지)

// 사용:
<!-- ❌ 느림: 자체 서버 -->
<script src="/jquery.js"></script>

<!-- ✅ 빠름: CDN -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>

// 고려사항:
- 비용 (대용량 트래픽)
- 의존성 (CDN 장애)
- 캐시 제어

// 결론: 대부분의 사이트에 추천

Q5. SSR vs SSG vs CSR 성능은?

A:

// CSR (Client-Side Rendering)
// - React 기본 방식

장점:
✅ 풍부한 인터랙션
✅ 서버 부하 낮음

단점:
❌ 초기 로딩 느림 (JavaScript 다운로드)
SEO 어려움
FCP/LCP 느림

적합:
- 대시보드
- 관리자 페이지
- 인증 필요한 앱

// SSR (Server-Side Rendering)
// - Next.js getServerSideProps

장점:
✅ 빠른 FCP
SEO 좋음
✅ 최신 데이터

단점:
❌ 서버 부하
TTFB 느릴 수 있음

적합:
- 뉴스 사이트
- 전자상거래
- 개인화 콘텐츠

// SSG (Static Site Generation)
// - Next.js getStaticProps

장점:
✅ 가장 빠름
SEO 좋음
✅ 서버 부하 없음 (CDN)

단점:
❌ 빌드 시간
❌ 실시간 데이터 어려움

적합:
- 블로그
- 문서 사이트
- 마케팅 페이지

// 결론: 하이브리드 접근
- 정적 페이지: SSG
- 동적 페이지: SSR
- 인터랙티브: CSR

🎓 다음 단계

웹 성능 최적화를 이해했다면, 다음을 학습해보세요:

  1. 브라우저 작동 원리 - 렌더링 이해
  2. React란? (문서 작성 예정) - React 성능 최적화
  3. SEO 기초 (문서 작성 예정) - 성능과 SEO

성능 측정 도구

// 온라인 도구
Google PageSpeed Insights // 전체 분석
Google Lighthouse // 세부 분석
WebPageTest // 상세 분석
GTmetrix // 종합 분석

// 브라우저 도구
Chrome DevTools
- Performance
- Network
- Coverage
- Lighthouse

// 모니터링
Google Analytics // 사용자 분석
Sentry // 에러 추적
New Relic // APM
Datadog // 모니터링

// 번들 분석
webpack-bundle-analyzer
source-map-explorer

체크리스트

## 이미지
[ ] WebP/AVIF 포맷
[ ] 적절한 크기
[ ] Lazy Loading
[ ] CDN 사용

## JavaScript
[ ] 코드 스플리팅
[ ] Tree Shaking
[ ] defer/async
[ ] 압축 (Minify)

## CSS
[ ] Critical CSS 인라인
[ ] 사용 안 하는 CSS 제거
[ ] CSS Minify

## 폰트
[ ] font-display: swap
[ ] 서브셋
[ ] Preload

## 캐싱
[ ] HTTP 캐시 헤더
[ ] Service Worker
[ ] CDN

## 네트워크
[ ] HTTP/2
[ ] Gzip/Brotli 압축
[ ] DNS Prefetch

## Core Web Vitals
[ ] LCP < 2.5s
[ ] FID < 100ms
[ ] CLS < 0.1

🎬 마무리

웹 성능 최적화는 사용자 경험의 핵심입니다:

  • 측정: 데이터 기반 접근
  • 우선순위: 영향력 큰 것부터
  • 점진적: 한 번에 모든 것 말고 단계적으로
  • 지속적: 정기적인 모니터링과 개선

1초가 빠르면 전환율이 7% 증가합니다! ⚡✨