Skip to content

필사 모드: 웹 성능 최적화 완전 가이드 2025: Core Web Vitals, 로딩 전략, 렌더링 패턴 총정리

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

웹 성능은 사용자 경험과 비즈니스 성과에 직접적인 영향을 미칩니다. Google의 연구에 따르면, 페이지 로딩 시간이 1초에서 3초로 늘어나면 이탈률이 32% 증가하고, 5초가 되면 90% 증가합니다. Amazon은 페이지 로드 시간이 100ms 증가할 때마다 매출이 1% 감소한다고 보고했습니다.

2025년, Google은 Core Web Vitals를 검색 순위 요소로 강화했으며, INP(Interaction to Next Paint)가 FID를 대체하면서 상호작용 성능의 중요성이 더욱 커졌습니다. 이 글에서는 Core Web Vitals 최적화부터 이미지, JavaScript, CSS, 폰트, 렌더링 패턴, 캐싱, 네트워크, 모니터링, 그리고 Next.js 특화 최적화까지 웹 성능의 모든 것을 다룹니다.

1. 왜 성능이 중요한가

1.1 비즈니스 임팩트

성능과 비즈니스 지표:

├── 로딩 3초 초과 → 53% 이탈 (Google)

├── 100ms 지연 → 매출 1% 감소 (Amazon)

├── 500ms 지연 → 트래픽 20% 감소 (Google)

├── 1초 개선 → 전환율 7% 향상 (Walmart)

└── 2초 개선 → 바운스율 50% 감소 (COOK)

1.2 SEO와 Core Web Vitals

2025년 Google은 Core Web Vitals를 검색 순위의 핵심 요소로 확정했습니다.

| 지표 | 좋음 (Good) | 개선 필요 (Needs Improvement) | 나쁨 (Poor) |

|------|------------|-------------------------------|------------|

| LCP | 2.5초 이내 | 2.5~4.0초 | 4.0초 초과 |

| INP | 200ms 이내 | 200~500ms | 500ms 초과 |

| CLS | 0.1 이하 | 0.1~0.25 | 0.25 초과 |

1.3 성능 예산 (Performance Budget)

성능 예산 예시:

├── 초기 로드 JS: 200KB 이하 (gzip)

├── 초기 로드 CSS: 50KB 이하 (gzip)

├── 전체 페이지 크기: 1.5MB 이하

├── LCP: 2.5초 이내

├── INP: 200ms 이내

├── CLS: 0.1 이하

├── Time to First Byte: 600ms 이내

└── 요청 수: 50개 이하

2. Core Web Vitals 심층 분석

2.1 LCP (Largest Contentful Paint)

LCP는 뷰포트 내에서 가장 큰 콘텐츠 요소가 렌더링되는 시간입니다.

LCP 대상 요소:

├── img 요소

├── video 요소 (포스터 이미지)

├── CSS background-image가 있는 요소

├── 텍스트 노드를 포함하는 블록 레벨 요소

└── svg 내의 image 요소

**LCP 최적화 전략:**

<!-- 1. 히어로 이미지 프리로드 -->

<!-- 2. LCP 이미지에 fetchpriority 설정 -->

<!-- 3. 서버 응답 시간 최적화 -->

<!-- TTFB 목표: 200ms 이내 -->

/* 4. 폰트 로딩 최적화 */

@font-face {

font-family: 'MyFont';

src: url('/fonts/myfont.woff2') format('woff2');

font-display: swap;

}

2.2 INP (Interaction to Next Paint)

INP는 사용자 상호작용(클릭, 탭, 키보드)에서 다음 페인트까지의 지연 시간을 측정합니다.

INP 최적화 전략:

1. 긴 태스크 분할

- 50ms 이상의 태스크를 더 작은 단위로 분할

- requestIdleCallback, scheduler.yield() 활용

2. 메인 스레드 해방

- Web Worker로 무거운 계산 이동

- 불필요한 동기 JS 제거

3. 이벤트 핸들러 최적화

- debounce / throttle 적용

- passive 이벤트 리스너 사용

// 긴 태스크 분할 예시

async function processLargeList(items: Item[]) {

const CHUNK_SIZE = 50;

for (let i = 0; i < items.length; i += CHUNK_SIZE) {

const chunk = items.slice(i, i + CHUNK_SIZE);

processChunk(chunk);

// 브라우저에게 렌더링 기회 제공

await scheduler.yield();

}

}

// scheduler.yield 폴리필

if (!('scheduler' in globalThis)) {

(globalThis as any).scheduler = {

yield: () => new Promise(resolve => setTimeout(resolve, 0))

};

}

2.3 CLS (Cumulative Layout Shift)

CLS는 페이지 로드 중 예기치 않은 레이아웃 이동의 총합입니다.

<!-- CLS 방지: 이미지에 크기 명시 -->

<!-- CLS 방지: 동적 콘텐츠에 min-height 설정 -->

<!-- 광고 또는 동적 콘텐츠 -->

/* CLS 방지: 폰트 로딩 시 레이아웃 시프트 최소화 */

@font-face {

font-family: 'MyFont';

src: url('/fonts/myfont.woff2') format('woff2');

font-display: optional; /* 또는 swap */

/* size-adjust로 대체 폰트와 크기 맞춤 */

size-adjust: 100.5%;

ascent-override: 95%;

descent-override: 22%;

line-gap-override: 0%;

}

**CLS 주요 원인과 해결:**

| 원인 | 해결 방법 |

|------|----------|

| 크기 미지정 이미지 | width/height 또는 aspect-ratio 속성 사용 |

| 동적 삽입 콘텐츠 | min-height로 공간 예약 |

| 웹 폰트 FOUT/FOIT | font-display: optional + preload |

| 동적 광고 | 고정 크기 컨테이너 사용 |

| 늦게 로드되는 CSS | Critical CSS 인라인 |

3. 이미지 최적화

3.1 차세대 포맷

이미지 포맷 비교 (같은 품질 기준):

├── JPEG: 100KB (기준)

├── WebP: 70KB (-30%)

├── AVIF: 50KB (-50%)

└── JXL (JPEG XL): 55KB (-45%)

브라우저 지원 (2025):

├── WebP: 97%+ (IE 미지원)

├── AVIF: 92%+ (Safari 16.4+)

└── JXL: Chrome에서 제거, Safari/Firefox 지원

<!-- picture 요소로 포맷 폴백 -->

3.2 반응형 이미지

<!-- srcset과 sizes로 반응형 이미지 -->

srcset="

/hero-400w.webp 400w,

/hero-800w.webp 800w,

/hero-1200w.webp 1200w,

/hero-1600w.webp 1600w

"

sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"

src="/hero-800w.webp"

alt="Hero image"

width="1200"

height="600"

loading="lazy"

decoding="async"

/>

3.3 지연 로딩과 블러 플레이스홀더

<!-- 네이티브 lazy loading -->

<!-- Intersection Observer로 커스텀 lazy loading -->

// 블러 플레이스홀더 구현

function BlurImage({ src, alt, width, height, blurDataURL }: ImageProps) {

return (

{/* 블러 플레이스홀더 */}

src={blurDataURL}

alt=""

style={{

position: 'absolute',

inset: 0,

filter: 'blur(20px)',

transform: 'scale(1.1)',

}}

/>

{/* 실제 이미지 */}

src={src}

alt={alt}

width={width}

height={height}

loading="lazy"

decoding="async"

onLoad={(e) => {

// 로드 완료 시 블러 제거

e.currentTarget.style.opacity = '1';

}}

style={{ position: 'relative', opacity: 0, transition: 'opacity 0.3s' }}

/>

);

}

4. JavaScript 최적화

4.1 Code Splitting

// React lazy + Suspense (Route-based splitting)

const Dashboard = lazy(() => import('./pages/Dashboard'));

const Settings = lazy(() => import('./pages/Settings'));

function App() {

return (

);

}

// 조건부 동적 import

async function handleExport() {

// xlsx 라이브러리를 필요할 때만 로드

const XLSX = await import('xlsx');

const workbook = XLSX.utils.book_new();

// ...

}

4.2 Tree Shaking

// 나쁜 예: 전체 라이브러리 import

_.debounce(fn, 300);

// 좋은 예: 필요한 함수만 import

debounce(fn, 300);

// 더 좋은 예: 네이티브 또는 경량 대안 사용

// lodash.debounce: 1.4KB vs lodash: 72KB

// webpack-bundle-analyzer로 번들 분석

// npm install --save-dev webpack-bundle-analyzer

// next.config.js

const withBundleAnalyzer = require('@next/bundle-analyzer')({

enabled: process.env.ANALYZE === 'true',

});

module.exports = withBundleAnalyzer({

// Next.js config

});

4.3 번들 최적화

// webpack splitChunks 설정

module.exports = {

optimization: {

splitChunks: {

chunks: 'all',

cacheGroups: {

vendor: {

test: /[\\/]node_modules[\\/]/,

name: 'vendors',

chunks: 'all',

priority: 10,

},

common: {

minChunks: 2,

priority: -10,

reuseExistingChunk: true,

},

},

},

},

};

4.4 스크립트 로딩 전략

<!-- 일반: 파싱 차단 -->

<!-- async: 비동기 다운로드, 즉시 실행 (파싱 차단 가능) -->

<!-- defer: 비동기 다운로드, DOM 파싱 후 실행 (순서 보장) -->

<!-- module: defer와 동일한 동작 -->

스크립트 로딩 타임라인:

일반: [HTML 파싱...] [다운로드] [실행] [HTML 파싱...]

async: [HTML 파싱......다운로드......] [실행] [HTML 파싱...]

defer: [HTML 파싱......다운로드.............] [실행]

5. CSS 최적화

5.1 Critical CSS

<!-- Critical CSS 인라인 -->

/* 첫 화면(Above the fold)에 필요한 최소 CSS만 인라인 */

body { margin: 0; font-family: system-ui; }

.header { height: 60px; background: #fff; }

.hero { height: 400px; display: flex; align-items: center; }

<!-- 나머지 CSS는 비동기 로드 -->

onload="this.onload=null;this.rel='stylesheet'" />

5.2 CSS Containment

/* contain으로 렌더링 범위 제한 */

.card {

contain: layout style paint;

/* 또는 content-visibility로 오프스크린 요소 최적화 */

content-visibility: auto;

contain-intrinsic-size: 0 300px;

}

/* will-change로 GPU 가속 힌트 */

.animated-element {

will-change: transform;

/* 주의: 남용 시 메모리 낭비 */

}

5.3 불필요한 CSS 제거

// PurgeCSS 설정 (postcss.config.js)

module.exports = {

plugins: [

require('@fullhuman/postcss-purgecss')({

content: [

'./src/**/*.{js,jsx,ts,tsx}',

'./public/index.html'

],

defaultExtractor: content =>

content.match(/[\w-/:]+(?<!:)/g) || [],

safelist: ['html', 'body', /^data-/]

})

]

};

6. 폰트 최적화

6.1 font-display 전략

/* swap: 대체 폰트 → 웹폰트 (FOUT 발생, 텍스트 즉시 표시) */

@font-face {

font-family: 'MyFont';

font-display: swap;

src: url('/fonts/myfont.woff2') format('woff2');

}

/* optional: 빠르면 웹폰트, 느리면 대체 폰트 유지 (CLS 최소) */

@font-face {

font-family: 'MyFont';

font-display: optional;

src: url('/fonts/myfont.woff2') format('woff2');

}

6.2 폰트 프리로드

<!-- 핵심 폰트 프리로드 -->

rel="preload"

href="/fonts/inter-var.woff2"

as="font"

type="font/woff2"

crossorigin

/>

6.3 Variable Fonts

/* 기존: 각 굵기별 별도 파일 (400, 500, 600, 700 = 4파일) */

/* Variable Font: 1파일로 모든 굵기 */

@font-face {

font-family: 'Inter';

src: url('/fonts/inter-variable.woff2') format('woff2-variations');

font-weight: 100 900;

font-display: swap;

}

/* 사용 */

.light { font-weight: 300; }

.regular { font-weight: 400; }

.bold { font-weight: 700; }

6.4 폰트 서브셋

pyftsubset으로 한글 폰트 서브셋 생성

pip install fonttools brotli

pyftsubset NotoSansKR-Regular.otf \

--text-file=korean-chars.txt \

--output-file=NotoSansKR-subset.woff2 \

--flavor=woff2

결과: 4.5MB → 300KB (한글 2,350자 기준)

7. 렌더링 패턴

7.1 패턴 비교표

| 패턴 | TTFB | FCP | LCP | TTI | SEO | 사용 사례 |

|------|------|-----|-----|-----|-----|----------|

| CSR | 빠름 | 느림 | 느림 | 느림 | 나쁨 | SPA, 대시보드 |

| SSR | 느림 | 빠름 | 빠름 | 느림 | 좋음 | 동적 콘텐츠, SEO 필요 |

| SSG | 매우 빠름 | 매우 빠름 | 매우 빠름 | 빠름 | 매우 좋음 | 블로그, 문서, 마케팅 |

| ISR | 매우 빠름 | 매우 빠름 | 매우 빠름 | 빠름 | 매우 좋음 | 이커머스, 뉴스 |

| Streaming SSR | 빠름 | 매우 빠름 | 빠름 | 빠름 | 좋음 | 복잡한 동적 페이지 |

7.2 CSR (Client-Side Rendering)

CSR 플로우:

Browser Server

│─ HTML 요청 ──→│

│←─ 빈 HTML ────│

│─ JS 요청 ────→│

│←─ JS 번들 ────│

│ [JS 파싱/실행] │

│─ API 요청 ───→│

│←─ 데이터 ─────│

│ [렌더링] │

▼ 화면 표시 ▼

7.3 SSR (Server-Side Rendering)

SSR 플로우:

Browser Server

│─ HTML 요청 ──→│

│ │ [데이터 패칭]

│ │ [HTML 렌더링]

│←─ 완성 HTML ──│

│ [화면 표시] │

│─ JS 요청 ────→│

│←─ JS 번들 ────│

│ [Hydration] │

▼ 인터랙티브 ▼

7.4 Streaming SSR

// Next.js App Router Streaming SSR

async function SlowComponent() {

const data = await fetchSlowData(); // 3초 소요

return <div>{/* data 렌더링 */}</div>;

}

export default function Page() {

return (

{/* 즉시 렌더링 */}

{/* 스트리밍: 준비되면 점진적 전송 */}

{/* 즉시 렌더링 */}

);

}

7.5 ISR (Incremental Static Regeneration)

// Next.js ISR

// app/products/[id]/page.tsx

export const revalidate = 3600; // 1시간마다 재생성

async function ProductPage({ params }: { params: { id: string } }) {

const product = await getProduct(params.id);

return (

);

}

export default ProductPage;

// 정적 경로 생성

export async function generateStaticParams() {

const products = await getTopProducts(100);

return products.map(p => ({ id: p.id }));

}

8. 캐싱 전략

8.1 HTTP 캐시

HTTP 캐시 전략:

├── 정적 자산 (JS/CSS/이미지)

│ Cache-Control: public, max-age=31536000, immutable

│ (파일명에 해시 포함: app.abc123.js)

├── HTML

│ Cache-Control: public, max-age=0, must-revalidate

│ (항상 서버 확인)

├── API 응답

│ Cache-Control: private, max-age=60, stale-while-revalidate=300

│ (60초 캐시, 5분간 stale 허용)

└── 사용자별 데이터

Cache-Control: private, no-cache

(매번 서버 검증)

8.2 stale-while-revalidate

stale-while-revalidate 동작:

요청 1: [캐시 미스] → 서버 → 응답 + 캐시 저장

요청 2: [캐시 히트, fresh] → 즉시 응답

요청 3: [캐시 히트, stale] → 즉시 응답(stale) + 백그라운드 서버 갱신

요청 4: [캐시 히트, fresh] → 즉시 응답(갱신된 데이터)

// SWR 라이브러리 (React)

function Profile() {

const { data, error, isLoading } = useSWR('/api/user', fetcher, {

revalidateOnFocus: true,

revalidateOnReconnect: true,

refreshInterval: 30000, // 30초마다 갱신

});

if (isLoading) return <Skeleton />;

if (error) return <Error />;

return <UserCard user={data} />;

}

8.3 Service Worker 캐시

// Service Worker 캐싱 전략

// sw.js

const CACHE_NAME = 'app-v1';

const STATIC_ASSETS = [

'/',

'/styles.css',

'/app.js',

'/offline.html'

];

// 설치: 정적 자산 사전 캐싱

self.addEventListener('install', (event) => {

event.waitUntil(

caches.open(CACHE_NAME).then(cache =>

cache.addAll(STATIC_ASSETS)

)

);

});

// 요청: Cache First + Network Fallback

self.addEventListener('fetch', (event) => {

event.respondWith(

caches.match(event.request).then(cached => {

if (cached) return cached;

return fetch(event.request).then(response => {

// 성공 응답을 캐시에 저장

const clone = response.clone();

caches.open(CACHE_NAME).then(cache =>

cache.put(event.request, clone)

);

return response;

}).catch(() => {

// 오프라인 폴백

return caches.match('/offline.html');

});

})

);

});

8.4 CDN 캐시

CDN 캐시 계층:

사용자 → [브라우저 캐시] → [CDN Edge] → [CDN Origin Shield] → [서버]

CDN 헤더 예시:

정적 자산:

Cache-Control: public, max-age=31536000, immutable

CDN-Cache-Control: public, max-age=31536000

동적 콘텐츠:

Cache-Control: public, max-age=0, must-revalidate

CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300

Surrogate-Control: max-age=3600

9. 네트워크 최적화

9.1 리소스 힌트

<!-- DNS Prefetch: 외부 도메인 DNS 미리 해석 -->

<!-- Preconnect: DNS + TCP + TLS 미리 연결 -->

<!-- Prefetch: 다음 페이지 리소스 미리 로드 -->

<!-- Preload: 현재 페이지 핵심 리소스 우선 로드 -->

9.2 Priority Hints

<!-- fetchpriority로 리소스 우선순위 지정 -->

<!-- 히어로 이미지: 높은 우선순위 -->

<!-- 오프스크린 이미지: 낮은 우선순위 -->

<!-- 핵심 스크립트: 높은 우선순위 -->

9.3 HTTP/2와 HTTP/3

HTTP/1.1 vs HTTP/2 vs HTTP/3:

┌─────────────┬──────────────┬──────────────┬──────────────┐

│ 기능 │ HTTP/1.1 │ HTTP/2 │ HTTP/3 │

├─────────────┼──────────────┼──────────────┼──────────────┤

│ 멀티플렉싱 │ 불가 │ 지원 │ 지원 │

│ 헤더 압축 │ 없음 │ HPACK │ QPACK │

│ 서버 푸시 │ 없음 │ 지원 │ 제거됨 │

│ HOL 블로킹 │ TCP 레벨 │ TCP 레벨 │ 해결(QUIC) │

│ 전송 프로토콜│ TCP │ TCP │ QUIC(UDP) │

│ 연결 설정 │ TCP+TLS │ TCP+TLS │ 0-RTT 가능 │

└─────────────┴──────────────┴──────────────┴──────────────┘

10. 성능 모니터링

10.1 Lighthouse CI

.github/workflows/lighthouse.yml

name: Lighthouse CI

on: [pull_request]

jobs:

lighthouse:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: 20

- run: npm ci && npm run build

- name: Run Lighthouse CI

uses: treosh/lighthouse-ci-action@v11

with:

configPath: './lighthouserc.json'

uploadArtifacts: true

{

"ci": {

"assert": {

"assertions": {

"categories:performance": ["error", { "minScore": 0.9 }],

"categories:accessibility": ["error", { "minScore": 0.9 }],

"categories:best-practices": ["warn", { "minScore": 0.9 }],

"categories:seo": ["error", { "minScore": 0.9 }],

"first-contentful-paint": ["error", { "maxNumericValue": 2000 }],

"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],

"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],

"interactive": ["error", { "maxNumericValue": 5000 }]

}

}

}

}

10.2 Real User Monitoring (RUM)

// Web Vitals 측정 및 보고

function sendToAnalytics(metric: any) {

const body = JSON.stringify({

name: metric.name,

value: metric.value,

rating: metric.rating,

delta: metric.delta,

id: metric.id,

navigationType: metric.navigationType,

});

// Beacon API로 비동기 전송

if (navigator.sendBeacon) {

navigator.sendBeacon('/api/vitals', body);

} else {

fetch('/api/vitals', { body, method: 'POST', keepalive: true });

}

}

onLCP(sendToAnalytics);

onINP(sendToAnalytics);

onCLS(sendToAnalytics);

onFCP(sendToAnalytics);

onTTFB(sendToAnalytics);

10.3 성능 대시보드

성능 모니터링 도구:

├── 합성 모니터링 (Lab Data)

│ ├── Lighthouse CI (자동화)

│ ├── WebPageTest (상세 분석)

│ └── PageSpeed Insights (Google)

├── 실사용자 모니터링 (Field Data)

│ ├── Chrome UX Report (CrUX)

│ ├── web-vitals 라이브러리

│ └── 상용: Datadog RUM, New Relic

└── 번들 분석

├── webpack-bundle-analyzer

├── source-map-explorer

└── bundlephobia.com

11. Next.js 특화 최적화

11.1 App Router와 Server Components

// Server Component (기본값 - JS 번들에 포함되지 않음)

// app/products/page.tsx

async function ProductsPage() {

// 서버에서 직접 데이터 패칭 (API 라우트 불필요)

const products = await db.product.findMany();

return (

{products.map(p => (

))}

);

}

// Client Component (인터랙션이 필요한 부분만)

// components/AddToCart.tsx

'use client';

export function AddToCart({ productId }: { productId: string }) {

const [count, setCount] = useState(0);

return (

Add to Cart ({count})

);

}

11.2 Next.js Image 최적화

// 자동 WebP/AVIF 변환, 반응형, lazy loading

function HeroSection() {

return (

src="/hero.jpg"

alt="Hero"

width={1200}

height={600}

priority // LCP 이미지는 priority 설정

sizes="(max-width: 768px) 100vw, 1200px"

quality={85}

placeholder="blur"

blurDataURL="data:image/jpeg;base64,..."

/>

);

}

// 원격 이미지

function Avatar({ user }: { user: User }) {

return (

src={user.avatarUrl}

alt={user.name}

width={48}

height={48}

loading="lazy"

/>

);

}

11.3 Next.js Font 최적화

// next/font로 자동 최적화 (자체 호스팅, CLS 제거)

const inter = Inter({

subsets: ['latin'],

display: 'swap',

variable: '--font-inter',

});

const notoSansKR = Noto_Sans_KR({

subsets: ['latin'],

weight: ['400', '500', '700'],

display: 'swap',

variable: '--font-noto-sans-kr',

});

export default function RootLayout({ children }: { children: React.ReactNode }) {

return (

);

}

11.4 Route Segment Config

// app/blog/[slug]/page.tsx

// 정적 생성 + ISR (60초)

export const revalidate = 60;

// 또는 완전 정적

export const dynamic = 'force-static';

// 또는 항상 동적

export const dynamic = 'force-dynamic';

// 런타임 선택

export const runtime = 'edge'; // Edge Runtime (빠른 TTFB)

// export const runtime = 'nodejs'; // Node.js Runtime (기본)

12. 면접 질문 모음

기본 개념

1. **LCP (Largest Contentful Paint)**: 뷰포트에서 가장 큰 콘텐츠 요소가 렌더링되는 시간. 목표: 2.5초 이내. 로딩 성능을 측정.

2. **INP (Interaction to Next Paint)**: 사용자 상호작용(클릭, 키보드 등)에서 다음 페인트까지의 지연. 목표: 200ms 이내. 응답성을 측정.

3. **CLS (Cumulative Layout Shift)**: 페이지 로드 중 예기치 않은 레이아웃 이동의 총합. 목표: 0.1 이하. 시각적 안정성을 측정.

**CSR (Client-Side Rendering)**: 브라우저에서 JS로 렌더링. 빈 HTML 전송 후 클라이언트에서 렌더링.

- 장점: 서버 부하 낮음, SPA 적합

- 단점: 느린 초기 로딩, SEO 불리

**SSR (Server-Side Rendering)**: 서버에서 HTML을 매 요청마다 생성.

- 장점: 빠른 FCP, SEO 유리

- 단점: 서버 부하, 느린 TTFB

**SSG (Static Site Generation)**: 빌드 시 HTML 미리 생성.

- 장점: 최고 성능, CDN 캐싱

- 단점: 빌드 시간, 동적 콘텐츠 제한

**ISR (Incremental Static Regeneration)**: SSG + 백그라운드 재생성.

- 장점: SSG 성능 + 데이터 갱신

- 단점: 구현 복잡성

**Code Splitting**: JS 번들을 여러 청크로 분할하여 필요한 것만 로드하는 기법. 라우트 기반(dynamic import) 또는 컴포넌트 기반으로 분할. 초기 로딩 시간 단축.

**Tree Shaking**: 빌드 시 사용되지 않는(dead) 코드를 제거하는 최적화. ES Module의 정적 구조를 분석하여 import되지 않은 export를 제거. 최종 번들 크기 감소.

차이: Code Splitting은 "언제 로드할지" (When), Tree Shaking은 "무엇을 제거할지" (What).

1. **서버 응답 최적화**: TTFB 200ms 이내, CDN 활용, 캐싱

2. **리소스 프리로드**: LCP 이미지에 preload + fetchpriority="high"

3. **이미지 최적화**: WebP/AVIF, 적절한 크기, 압축

4. **렌더링 차단 제거**: Critical CSS 인라인, JS defer

5. **폰트 최적화**: font-display: swap, preload

6. **SSR/SSG 활용**: 서버에서 완성된 HTML 전송

7. **Third-party 최적화**: 외부 스크립트 지연 로드

**주요 원인:**

1. 크기 미지정 이미지/비디오

2. 동적 삽입 콘텐츠(광고, 배너)

3. 웹 폰트 로딩 시 레이아웃 시프트

4. 늦게 로드되는 CSS

5. DOM을 조작하는 JavaScript

**해결 방법:**

1. img/video에 width, height 속성 또는 aspect-ratio CSS 명시

2. 동적 콘텐츠 영역에 min-height로 공간 예약

3. font-display: optional, 폰트 프리로드

4. Critical CSS 인라인

5. transform 애니메이션 사용(top/left 대신)

심화 질문

1. **Cache First**: 캐시 확인 후 없으면 네트워크. 정적 자산에 적합.

2. **Network First**: 네트워크 시도 후 실패 시 캐시. API 응답에 적합.

3. **Stale While Revalidate**: 캐시된 응답 즉시 반환 + 백그라운드 갱신. 빈번히 변경되는 자산에 적합.

4. **Cache Only**: 캐시만 사용. 오프라인 자산에 적합.

5. **Network Only**: 네트워크만 사용. 실시간 데이터에 적합.

선택 기준: 데이터의 신선도 요구사항과 오프라인 지원 필요성에 따라 결정.

- **Cache-Control**: 캐시 정책의 핵심 헤더

- max-age: 캐시 유효 시간(초)

- no-cache: 매번 서버 검증 필요

- no-store: 절대 캐시하지 않음

- public/private: CDN 캐시 가능 여부

- immutable: 변하지 않는 리소스

- stale-while-revalidate: stale 응답 허용 시간

- **ETag**: 리소스 버전 식별자. 조건부 요청(If-None-Match)에 사용.

- **Last-Modified**: 마지막 수정 시간. If-Modified-Since와 함께 사용.

추천 전략:

- 정적 자산(해시 포함): max-age=31536000, immutable

- HTML: no-cache 또는 max-age=0, must-revalidate

1. **포맷**: WebP/AVIF 사용, picture 요소로 폴백

2. **크기**: 반응형 이미지(srcset + sizes), 실제 표시 크기에 맞게

3. **압축**: 품질 75~85, 용도에 따라 조정

4. **로딩**: LCP 이미지는 eager + fetchpriority="high", 나머지는 lazy

5. **프레임워크**: Next.js Image 등 자동 최적화 도구 활용

6. **CDN**: 이미지 CDN(Cloudinary, imgix) 활용

7. **플레이스홀더**: 블러 또는 LQIP로 사용자 경험 향상

1. **긴 태스크 분할**: 50ms 이상의 작업을 scheduler.yield()나 requestIdleCallback으로 분할

2. **메인 스레드 해방**: Web Worker로 무거운 계산 이동

3. **이벤트 핸들러 최적화**: debounce/throttle, passive 이벤트 리스너

4. **JS 번들 최소화**: 코드 스플리팅, 트리 셰이킹

5. **가상화**: 긴 리스트는 react-virtuoso 등으로 가상화

6. **startTransition**: 긴급하지 않은 업데이트를 전환으로 표시

7. **React 최적화**: useMemo, useCallback, React.memo 적절히 사용

**동작 원리:**

1. 전 세계에 분산된 Edge 서버(PoP)에 콘텐츠 캐시

2. 사용자의 DNS 요청을 가장 가까운 Edge로 라우팅

3. Edge에 캐시가 있으면 즉시 응답, 없으면 Origin에서 가져와 캐시

**성능 이점:**

- 물리적 거리 단축: RTT(Round Trip Time) 감소

- Origin 서버 부하 분산

- DDoS 방어

- TLS 최적화 (Edge에서 TLS 종료)

- 자동 압축 (Brotli, gzip)

- HTTP/2, HTTP/3 지원

webpack의 splitChunks 플러그인으로 코드 분할:

1. **Entry Points**: 여러 진입점으로 수동 분할

2. **Dynamic Imports**: import()로 동적 분할

3. **splitChunks**: 자동 분할 규칙 설정

주요 설정:

- chunks: 'all'로 동기/비동기 모두 분할

- cacheGroups: vendor(node_modules)와 common(공유 모듈) 분리

- minSize: 최소 청크 크기 (기본 20KB)

- maxSize: 최대 청크 크기 (자동 분할)

효과: 초기 로딩 시간 단축, 캐시 효율 향상, 필요한 코드만 로드

**장점:**

- TTFB 단축: HTML을 점진적으로 전송

- FCP 향상: 준비된 부분부터 표시

- 느린 데이터 소스 격리: Suspense로 감싸서 독립적 로딩

- 사용자 경험 향상: 스켈레톤/로딩 상태 즉시 표시

**구현 (Next.js App Router):**

- Server Component를 기본으로 사용

- 느린 컴포넌트를 Suspense로 감싸기

- fallback에 스켈레톤 UI 제공

- 데이터가 준비되면 자동으로 스트리밍

핵심: 전체 페이지가 준비될 때까지 기다리지 않고, 준비된 부분부터 점진적으로 전송.

1. **제로 JS 번들**: Server Component는 클라이언트 JS 번들에 포함되지 않음

2. **직접 데이터 접근**: 서버에서 직접 DB/API 접근 (API 라우트 불필요)

3. **자동 코드 분할**: Client Component만 번들에 포함

4. **스트리밍**: Suspense와 결합하여 점진적 렌더링

5. **캐싱**: 서버 측 캐싱으로 반복 요청 최적화

6. **보안**: 민감한 로직/키가 클라이언트에 노출되지 않음

사용 원칙: 인터랙션이 없는 컴포넌트는 Server, useState/useEffect가 필요한 부분만 Client로.

**설정:**

1. 경쟁사 분석: 주요 경쟁사의 성능 지표 측정

2. 사용자 기기 분석: 대상 사용자의 평균 기기/네트워크 파악

3. 비즈니스 목표 반영: 전환율, 이탈률 목표에 맞게 설정

4. 구체적 수치: JS 200KB, CSS 50KB, LCP 2.5s, INP 200ms 등

**적용:**

1. Lighthouse CI로 PR마다 자동 검사

2. 예산 초과 시 빌드 실패 또는 경고

3. bundlesize 또는 size-limit으로 번들 크기 제한

4. 팀 대시보드에서 트렌드 모니터링

5. 정기적으로 예산 검토 및 조정

1. **Variable Font**: 하나의 파일로 모든 굵기/스타일 (파일 수 감소)

2. **서브셋**: 필요한 글자만 포함 (한글: 4MB → 300KB)

3. **WOFF2 포맷**: 최고 압축률의 웹 폰트 포맷

4. **font-display**: swap(텍스트 즉시 표시) 또는 optional(CLS 최소)

5. **preload**: 핵심 폰트를 link preload로 조기 다운로드

6. **자체 호스팅**: Google Fonts 대신 직접 호스팅 (DNS/연결 비용 절감)

7. **size-adjust**: 대체 폰트와 웹 폰트의 크기를 맞추어 CLS 방지

13. 퀴즈

**정답: 2.5초 이내**

LCP(Largest Contentful Paint)의 기준:

- Good: 2.5초 이내

- Needs Improvement: 2.5~4.0초

- Poor: 4.0초 초과

LCP는 뷰포트에서 가장 큰 콘텐츠 요소가 렌더링되는 시간을 측정합니다.

**정답: INP (Interaction to Next Paint)**

2024년 3월부터 FID(First Input Delay)가 INP로 대체되었습니다. FID는 첫 번째 상호작용만 측정했지만, INP는 페이지 전체 수명 동안의 모든 상호작용을 측정하여 더 포괄적인 응답성 지표를 제공합니다.

**정답: 리소스가 절대 변경되지 않음을 나타내어 재검증 요청을 방지**

Cache-Control: immutable은 리소스가 변경되지 않을 것임을 브라우저에게 알려줍니다. 이로 인해 브라우저가 max-age 내에서 재검증(304) 요청을 보내지 않습니다. 파일명에 해시가 포함된 정적 자산(app.abc123.js)에 적합합니다.

**정답: ES Module (import/export) 사용**

Tree Shaking은 ES Module의 정적 구조를 분석하여 사용되지 않는 export를 제거합니다. CommonJS(require/module.exports)는 동적이므로 Tree Shaking이 불가능합니다. package.json의 "sideEffects": false 설정도 중요합니다.

**정답: 아니요, Server Components는 클라이언트 JS 번들에 포함되지 않습니다**

Server Components는 서버에서만 실행되고 결과 HTML만 클라이언트에 전송됩니다. 따라서 JS 번들 크기를 크게 줄일 수 있습니다. 'use client' 디렉티브가 선언된 Client Components만 번들에 포함됩니다.

14. 참고 자료

공식 문서

- [web.dev - Core Web Vitals](https://web.dev/vitals/)

- [web.dev - LCP 최적화](https://web.dev/optimize-lcp/)

- [web.dev - INP 최적화](https://web.dev/optimize-inp/)

- [web.dev - CLS 최적화](https://web.dev/optimize-cls/)

- [Next.js 성능 최적화](https://nextjs.org/docs/app/building-your-application/optimizing)

측정 도구

- [PageSpeed Insights](https://pagespeed.web.dev/)

- [WebPageTest](https://www.webpagetest.org/)

- [Chrome UX Report](https://developer.chrome.com/docs/crux/)

- [Lighthouse](https://developer.chrome.com/docs/lighthouse/)

- [web-vitals 라이브러리](https://github.com/GoogleChrome/web-vitals)

이미지 최적화

- [Squoosh (이미지 압축)](https://squoosh.app/)

- [AVIF 지원 현황](https://caniuse.com/avif)

- [Cloudinary 이미지 CDN](https://cloudinary.com/)

성능 참고

- [HTTP Archive Web Almanac](https://almanac.httparchive.org/)

- [Smashing Magazine Performance](https://www.smashingmagazine.com/category/performance/)

- [Addy Osmani - Image Optimization](https://www.smashingmagazine.com/2021/04/image-optimization-pre-release/)

- [Philip Walton - web-vitals](https://github.com/GoogleChrome/web-vitals)

프레임워크

- [Next.js Image Component](https://nextjs.org/docs/app/api-reference/components/image)

- [Next.js Font Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/fonts)

- [Vite 빌드 최적화](https://vitejs.dev/guide/build.html)

현재 단락 (1/724)

웹 성능은 사용자 경험과 비즈니스 성과에 직접적인 영향을 미칩니다. Google의 연구에 따르면, 페이지 로딩 시간이 1초에서 3초로 늘어나면 이탈률이 32% 증가하고, 5초가 되...

작성 글자: 0원문 글자: 19,352작성 단락: 0/724