웹 성능 최적화 완전정복 - Core Web Vitals부터 번들 최적화까지

December 29, 2025

웹 성능 최적화 완전정복

웹 성능은 사용자 경험과 비즈니스 성과에 직접적인 영향을 미친다. 페이지 로딩 시간이 1초 늦어지면 전환율이 7% 감소한다는 연구 결과도 있다. 이 글은 Core Web Vitals부터 번들 최적화까지 웹 성능 최적화의 모든 것을 실전 예제와 함께 정리한다.

1. 웹 성능 최적화 개요

1-1. 성능이 중요한 이유

사용자 경험:

  • 빠른 로딩 = 더 나은 사용자 경험
  • 느린 사이트 = 이탈률 증가

비즈니스 영향:

  • 전환율: 로딩 시간 1초 감소 시 전환율 7% 증가
  • 검색 순위: Google이 페이지 속도를 랭킹 요소로 사용
  • 사용자 만족도: 빠른 사이트는 사용자 만족도 향상

핵심 메트릭:

  • LCP (Largest Contentful Paint): 주요 콘텐츠 로딩 시간
  • FID (First Input Delay): 첫 상호작용 지연 시간
  • CLS (Cumulative Layout Shift): 레이아웃 안정성

1-2. 성능 측정 도구

Chrome DevTools:

  • Performance 탭: 상세한 성능 분석
  • Lighthouse: 종합 성능 점수
  • Network 탭: 네트워크 요청 분석

온라인 도구:

  • PageSpeed Insights: Google의 성능 분석
  • WebPageTest: 상세한 성능 메트릭
  • GTmetrix: 성능 리포트

실시간 모니터링:

  • Google Analytics: 실제 사용자 메트릭
  • Real User Monitoring (RUM): 실제 사용자 성능 데이터

2. Core Web Vitals 이해

2-1. LCP (Largest Contentful Paint)

LCP는 페이지의 주요 콘텐츠가 로딩되는 시간을 측정한다.

목표 값:

  • 좋음: 2.5초 이하
  • 개선 필요: 2.5초 ~ 4초
  • 나쁨: 4초 초과

최적화 방법:

1. 이미지 최적화

<!-- 나쁜 예: 큰 이미지 -->
<img src="hero.jpg" alt="Hero" />

<!-- 좋은 예: 최적화된 이미지 -->
<img 
  src="hero.webp" 
  srcset="hero-400.webp 400w, hero-800.webp 800w"
  sizes="(max-width: 600px) 400px, 800px"
  alt="Hero"
  loading="lazy"
/>

2. 리소스 우선순위

<!-- 중요한 리소스는 preload -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-image.jpg" as="image" />

<!-- 폰트 preload -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />

3. 서버 응답 시간 최적화

  • CDN 사용
  • 서버 최적화
  • 캐싱 전략

2-2. FID (First Input Delay)

FID는 사용자가 페이지와 처음 상호작용할 때의 지연 시간을 측정한다.

목표 값:

  • 좋음: 100ms 이하
  • 개선 필요: 100ms ~ 300ms
  • 나쁨: 300ms 초과

최적화 방법:

1. JavaScript 실행 시간 최소화

// 나쁜 예: 동기적 무거운 작업
function processData() {
  const data = heavyComputation(); // 블로킹
  return data;
}

// 좋은 예: 비동기 처리
async function processData() {
  // 메인 스레드 블로킹 방지
  const data = await heavyComputationAsync();
  return data;
}

2. 이벤트 핸들러 최적화

// 나쁜 예: 즉시 실행
button.addEventListener('click', () => {
  heavyOperation(); // 블로킹
});

// 좋은 예: requestIdleCallback 사용
button.addEventListener('click', () => {
  requestIdleCallback(() => {
    heavyOperation(); // 유휴 시간에 실행
  });
});

3. 웹 워커 활용

// 메인 스레드에서 무거운 작업 제거
const worker = new Worker('worker.js');

worker.postMessage({ data: largeData });
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

2-3. CLS (Cumulative Layout Shift)

CLS는 페이지 로딩 중 레이아웃이 얼마나 이동하는지 측정한다.

목표 값:

  • 좋음: 0.1 이하
  • 개선 필요: 0.1 ~ 0.25
  • 나쁨: 0.25 초과

최적화 방법:

1. 이미지 크기 지정

<!-- 나쁜 예: 크기 미지정 -->
<img src="image.jpg" alt="Image" />

<!-- 좋은 예: 크기 지정 -->
<img 
  src="image.jpg" 
  alt="Image"
  width="800"
  height="600"
  style="aspect-ratio: 800/600"
/>

2. 동적 콘텐츠 공간 예약

/* 나쁜 예: 높이 미지정 */
.ad-container {
  /* 높이 없음 */
}

/* 좋은 예: 최소 높이 지정 */
.ad-container {
  min-height: 250px;
}

3. 폰트 로딩 최적화

/* FOIT (Flash of Invisible Text) 방지 */
@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 즉시 fallback 폰트 표시 */
}

3. 번들 크기 최적화

3-1. 번들 분석

webpack-bundle-analyzer:

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
    }),
  ],
};

Vite 번들 분석:

npm install --save-dev rollup-plugin-visualizer

# vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
};

3-2. Tree Shaking

Tree Shaking은 사용하지 않는 코드를 제거한다.

ES Modules 사용:

// 나쁜 예: CommonJS
const _ = require('lodash');
const result = _.map([1, 2, 3], x => x * 2);

// 좋은 예: ES Modules
import { map } from 'lodash-es';
const result = map([1, 2, 3], x => x * 2);

package.json 설정:

{
  "sideEffects": false,
  "module": "esm/index.js",
  "main": "cjs/index.js"
}

3-3. 코드 스플리팅

동적 임포트:

// 나쁜 예: 모든 코드를 한 번에 로드
import HeavyComponent from './HeavyComponent';

function App() {
  return <HeavyComponent />;
}

// 좋은 예: 필요할 때만 로드
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

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

라우트 기반 스플리팅:

// React Router
import { lazy } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  );
}

Next.js 자동 스플리팅:

// pages/index.js - 자동으로 코드 스플리팅됨
import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('../components/Heavy'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // 서버 사이드 렌더링 비활성화
});

export default function Home() {
  return <DynamicComponent />;
}

3-4. 라이브러리 최적화

전체 라이브러리 대신 필요한 부분만:

// 나쁜 예: 전체 라이브러리
import _ from 'lodash';
const result = _.debounce(fn, 300);

// 좋은 예: 필요한 함수만
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// 더 좋은 예: 작은 라이브러리 사용
import debounce from 'lodash-es/debounce';

대안 라이브러리 사용:

// 나쁜 예: 큰 라이브러리
import moment from 'moment';

// 좋은 예: 작은 대안
import { format } from 'date-fns';

4. 이미지 최적화

4-1. 이미지 포맷 선택

최신 포맷 사용:

<!-- WebP 지원 브라우저 -->
<picture>
  <source srcset="image.webp" type="image/webp" />
  <source srcset="image.avif" type="image/avif" />
  <img src="image.jpg" alt="Image" />
</picture>

Next.js Image 컴포넌트:

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={800}
  height={600}
  priority // LCP 이미지인 경우
  placeholder="blur" // 블러 플레이스홀더
/>

4-2. 반응형 이미지

srcset 사용:

<img
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  src="image-800.jpg"
  alt="Image"
/>

4-3. Lazy Loading

네이티브 Lazy Loading:

<img 
  src="image.jpg" 
  alt="Image"
  loading="lazy"
  decoding="async"
/>

Intersection Observer:

const images = document.querySelectorAll('img[data-src]');

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      observer.unobserve(img);
    }
  });
});

images.forEach(img => imageObserver.observe(img));

4-4. 이미지 압축

도구:

  • Sharp: Node.js 이미지 처리
  • ImageOptim: 이미지 압축 도구
  • Squoosh: 웹 기반 이미지 압축

자동화:

// webpack 이미지 최적화
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.sharpMinify,
        },
      }),
    ],
  },
};

5. 캐싱 전략

5-1. HTTP 캐싱

Cache-Control 헤더:

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

// HTML: 짧은 캐시
app.get('*.html', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=3600'); // 1시간
  res.sendFile(path.join(__dirname, 'index.html'));
});

ETag 사용:

app.use(express.static('public', {
  etag: true,
  lastModified: true,
}));

5-2. Service Worker 캐싱

Cache First 전략:

// service-worker.js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 캐시에 있으면 캐시 반환
      if (response) {
        return response;
      }
      // 없으면 네트워크 요청
      return fetch(event.request).then((response) => {
        // 캐시에 저장
        const responseToCache = response.clone();
        caches.open('v1').then((cache) => {
          cache.put(event.request, responseToCache);
        });
        return response;
      });
    })
  );
});

Network First 전략:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 네트워크 성공 시 캐시 업데이트
        const responseToCache = response.clone();
        caches.open('v1').then((cache) => {
          cache.put(event.request, responseToCache);
        });
        return response;
      })
      .catch(() => {
        // 네트워크 실패 시 캐시 반환
        return caches.match(event.request);
      })
  );
});

5-3. 브라우저 캐싱

localStorage/SessionStorage:

// API 응답 캐싱
const cacheKey = 'api-data';
const cacheTime = 5 * 60 * 1000; // 5분

function getCachedData() {
  const cached = localStorage.getItem(cacheKey);
  if (cached) {
    const { data, timestamp } = JSON.parse(cached);
    if (Date.now() - timestamp < cacheTime) {
      return data;
    }
  }
  return null;
}

function setCachedData(data) {
  localStorage.setItem(cacheKey, JSON.stringify({
    data,
    timestamp: Date.now(),
  }));
}

6. 리소스 로딩 최적화

6-1. 리소스 우선순위

preload:

<!-- 중요한 리소스 미리 로드 -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-font.woff2" as="font" type="font/woff2" crossorigin />

prefetch:

<!-- 다음에 필요할 리소스 미리 가져오기 -->
<link rel="prefetch" href="next-page.html" />

preconnect:

<!-- 외부 도메인 연결 미리 설정 -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://api.example.com" />

6-2. 폰트 최적화

폰트 디스플레이:

@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 즉시 fallback 표시 */
}

폰트 서브셋:

/* 필요한 문자만 포함 */
@font-face {
  font-family: 'MyFont';
  src: url('font-subset.woff2') format('woff2');
  unicode-range: U+0020-007F; /* ASCII만 */
}

인라인 폰트:

<!-- 작은 폰트는 인라인 -->
<style>
  @font-face {
    font-family: 'IconFont';
    src: url('data:font/woff2;base64,...') format('woff2');
  }
</style>

6-3. CSS 최적화

Critical CSS 인라인:

<!-- 중요한 CSS는 인라인 -->
<style>
  /* Critical CSS */
  body { margin: 0; }
  .header { height: 60px; }
</style>

<!-- 나머지는 비동기 로드 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />

CSS 최소화:

// webpack
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  },
};

7. JavaScript 최적화

7-1. 코드 최소화

Terser 사용:

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // console.log 제거
          },
        },
      }),
    ],
  },
};

7-2. 불필요한 코드 제거

조건부 코드:

// 개발 코드 제거
if (process.env.NODE_ENV === 'production') {
  // 프로덕션 코드만
}

// 또는 webpack DefinePlugin
new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('production'),
}),

7-3. 비동기 로딩

동적 임포트:

// 필요할 때만 로드
const loadModule = async () => {
  const module = await import('./heavy-module.js');
  return module;
};

8. 네트워크 최적화

8-1. HTTP/2 활용

HTTP/2 장점:

  • 멀티플렉싱: 여러 요청 동시 처리
  • 서버 푸시: 서버가 리소스 미리 전송
  • 헤더 압축: 헤더 크기 감소

설정:

// Node.js
const http2 = require('http2');
const server = http2.createServer((req, res) => {
  // HTTP/2 서버
});

8-2. CDN 사용

CDN 장점:

  • 지리적 분산: 사용자와 가까운 서버에서 제공
  • 캐싱: 정적 자산 캐싱
  • DDoS 보호: 트래픽 분산

설정:

  • Cloudflare
  • AWS CloudFront
  • Vercel Edge Network

8-3. 압축

Gzip/Brotli:

// Express
const compression = require('compression');
app.use(compression({
  level: 6,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  },
}));

9. 성능 모니터링

9-1. Web Vitals 측정

web-vitals 라이브러리:

import { getCLS, getFID, getLCP } from 'web-vitals';

function sendToAnalytics(metric) {
  // Google Analytics로 전송
  gtag('event', metric.name, {
    value: Math.round(metric.value),
    event_label: metric.id,
    non_interaction: true,
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

9-2. Performance API

성능 측정:

// 페이지 로딩 시간
window.addEventListener('load', () => {
  const perfData = performance.timing;
  const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
  console.log('Page Load Time:', pageLoadTime);
});

// 리소스 로딩 시간
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
  console.log(resource.name, resource.duration);
});

9-3. 실시간 모니터링

Google Analytics:

// Web Vitals 리포트
import { onCLS, onFID, onLCP } from 'web-vitals';

onCLS(console.log);
onFID(console.log);
onLCP(console.log);

10. 실전 체크리스트

성능 최적화 체크리스트

이미지:

  • WebP/AVIF 포맷 사용
  • 적절한 크기로 리사이즈
  • Lazy loading 적용
  • srcset으로 반응형 이미지

JavaScript:

  • 코드 스플리팅 적용
  • Tree shaking 활성화
  • 불필요한 코드 제거
  • 동적 임포트 사용

CSS:

  • Critical CSS 인라인
  • 사용하지 않는 CSS 제거
  • CSS 최소화

캐싱:

  • HTTP 캐싱 설정
  • Service Worker 구현
  • CDN 사용

네트워크:

  • HTTP/2 사용
  • Gzip/Brotli 압축
  • 리소스 우선순위 설정

FAQ

Q: 성능 최적화를 어디서부터 시작해야 하나요?
A: Lighthouse로 측정하여 가장 점수가 낮은 항목부터 개선하는 것이 좋다. 일반적으로 이미지 최적화와 번들 크기 감소가 큰 효과를 낸다.

Q: 모든 최적화를 한 번에 적용해야 하나요?
A: 아니요, 점진적으로 적용하고 각 변경사항의 효과를 측정하는 것이 좋다. 한 번에 너무 많이 변경하면 문제 원인 파악이 어렵다.

Q: 성능과 기능 사이의 균형은 어떻게 맞추나요?
A: 사용자 경험에 가장 중요한 기능에 우선순위를 두고, 나머지는 점진적으로 최적화한다. 완벽보다는 실용적인 개선이 중요하다.

Q: 모바일과 데스크톱 성능을 다르게 최적화해야 하나요?
A: 네, 모바일은 네트워크와 처리 능력이 제한적이므로 더 공격적인 최적화가 필요하다. 반응형 이미지와 조건부 로딩을 활용한다.

Q: 성능 최적화 후 어떻게 검증하나요?
A: Lighthouse, PageSpeed Insights, WebPageTest 등으로 측정하고, 실제 사용자 메트릭(RUM)도 모니터링한다.

Q: 성능 최적화가 SEO에 영향을 주나요?
A: 네, Google은 페이지 속도를 랭킹 요소로 사용하므로 성능 최적화는 SEO에도 긍정적인 영향을 준다.

결론: 지속적인 성능 최적화

웹 성능 최적화는 한 번의 작업이 아니라 지속적인 프로세스다. 새로운 기능 추가, 라이브러리 업데이트, 콘텐츠 변경 등이 성능에 영향을 줄 수 있으므로 정기적으로 측정하고 개선해야 한다.

Core Web Vitals를 목표로 하되, 사용자 경험을 최우선으로 고려한다. 기술적 최적화도 중요하지만, 실제 사용자가 느끼는 성능이 가장 중요하다.

성능 최적화는 투자 대비 효과가 큰 작업이다. 작은 개선이라도 사용자 경험과 비즈니스 성과에 큰 영향을 미칠 수 있다. 지속적으로 측정하고 개선하는 문화를 만들어가자.

성능 최적화 후 프로젝트 기회

웹 성능 최적화를 마스터한 후, 실제 프로젝트에 적용하고 싶다면 블루버튼 같은 프로젝트 매칭 플랫폼을 활용할 수 있다. 성능 최적화가 중요한 프로젝트, Core Web Vitals 개선이 필요한 프로젝트, 빠른 로딩 속도가 필수인 프로젝트 등에서 실전 경험을 쌓을 수 있다. 특히 사용자 경험 개선이나 SEO 최적화가 중요한 프로젝트에서 성능 최적화의 강점을 발휘할 수 있다.


Written by Jeon Byung Hun 개발을 즐기는 bottlehs - Engineer, MS, AI, FE, BE, OS, IOT, Blockchain, 설계, 테스트