본문 바로가기

asset 최적화

이미지·CSS·JavaScript·웹 폰트의 크기를 줄이고 로딩 시점을 조정하며 HTTP 캐시로 반복 전송 비용을 낮춥니다.

이번 장에서는 웹 성능 최적화의 핵심인 에셋 최적화를 다룹니다.

에셋 유형별 파일 크기 축소와 로딩 방식 개선 전략을 정리하고, Core Web Vitals인 LCP(Largest Contentful Paint)와 실험실 지표인 TBT(Total Blocking Time)와의 연관성을 함께 살펴봅니다.

자산을 줄이고 초기 화면의 우선순위를 정합니다

ASSET DELIVERY

자산을 줄이고 초기 화면의 우선순위를 정합니다

이미지·CSS·JavaScript·폰트의 전송 크기와 실행 비용은 다릅니다. 빌드 결과의 URL·캐시 정책과 브라우저의 발견·실행 시점을 함께 설계합니다.

자산의 변환과 전달을 다른 단계로 구별한다3노드2관계는원본변형→최적화해시→전달재사용의논리파이프라인이다. 초기화면의리소스와나중리소스의발견시점은아래보조설명에구별되어있다. 작은번들/압축전송량만으로LCP/INP/CLS개선을확정하지않고HTML의새URL참조도필요하다. mobile같다.
원본·변형표시 크기·품질폰트 부분집합
연결 관계원본·변형 · 표시 크기·품질 · 폰트 부분집합 → 최적화·해시 URL · 압축·미사용 제거 · 필요 단위 분할
최적화·해시 URL압축·미사용 제거필요 단위 분할
연결 관계최적화·해시 URL · 압축·미사용 제거 · 필요 단위 분할 → 전달·재사용 · HTML 참조 갱신 · CDN·캐시 정책
전달·재사용HTML 참조 갱신CDN·캐시 정책
자산 변환과 전달원본의 크기와 포맷/부분집합→압축·분할 및 내용 해시 URL→CDN과 HTTP캐시 전달.1 · 원본과 변형크기 · 포맷 · 부분집합2 · 최적화·해시압축 · 분할 · 새 URL3 · 전달·재사용CDN · HTTP 캐시

원본·변형 · 필요한 품질

실제 표시 크기에 맞는 이미지 후보를 srcset·sizes로 제공합니다. 폰트는 필요한 문자·굵기를 정하고 품질을 확인합니다.

코드·해시 · 배포 식별

미사용 CSS·JS를 제거하고 필요한 단위로 분할합니다. 내용이 바뀐 파일은 새 해시 URL을 만들고 HTML·manifest의 참조도 갱신합니다.

초기 화면 · 빨리 발견해야 할 자원

LCP 이미지와 핵심 CSS를 먼저 찾도록 합니다. LCP 이미지는 lazy로 미루지 않고 preload는 실제 발견 지연을 확인한 중요한 자원에 제한합니다.

나중 화면 · 사용 시점까지 미룰 자원

화면 밖 이미지에 lazy loading을, 비필수 코드에는 필요한 시점의 동적 import를 검토합니다. async·defer는 그 자체로 “사용할 때까지 다운로드하지 않음”을 뜻하지 않습니다.

일반 외부 script · 실행 순서

async는 준비되면 실행하고 순서를 보장하지 않습니다. defer는 파싱 뒤 문서 순서대로 실행합니다. 둘 다 다운로드와 실행 비용을 없애지는 않습니다.

캐시 · 해시 자산과 HTML

변경되지 않는 해시 자산은 긴 max-age·immutable로 재사용할 수 있습니다. HTML은 정책에 맞게 짧은 캐시 또는 재검증으로 새 자산 참조를 전달합니다.

폰트 교체의 레이아웃 이동, 분할된 JS의 실행 비용, 첫 화면 발견 지연을 다시 측정합니다. 파일 크기만 줄었다고 LCP·INP·CLS가 모두 개선되는 것은 아닙니다.


이미지 최적화

웹 페이지에서 가장 큰 용량을 차지하는 경우가 많은 것이 바로 이미지입니다.

이미지 최적화는 웹 성능 개선에 가장 큰 효과를 줄 수 있는 영역 중 하나입니다.

  • 적절한 이미지 포맷 선택
    • JPEG: 사진, 복잡한 그래픽 등 풍부한 색상 정보와 디테일이 중요한 이미지에 적합합니다. 손실 압축 방식.
    • PNG: 투명한 배경이 필요하거나 로고, 아이콘 등 선명한 경계가 중요한 이미지에 적합합니다. 무손실 압축 방식.
    • WebP: Google에서 개발한 최신 이미지 포맷으로, JPEG나 PNG보다 더 나은 압축률을 제공하면서도 품질 손실이 적습니다. 투명도도 지원합니다. 모든 최신 브라우저에서 지원되므로 적극적으로 고려해야 합니다.
    • SVG (Scalable Vector Graphics): 로고, 아이콘, 간단한 일러스트 등 벡터 기반 이미지에 적합합니다. 확대/축소에 적합하고 CSS로 스타일링할 수 있습니다. 파일 크기는 기하의 복잡도와 포함 데이터에 따라 커질 수도 있어 실제 결과를 비교합니다.
  • 이미지 압축 (Compression)
    • 손실 압축(Lossy Compression): JPEG에 적용되며, 일부 데이터 손실을 감수하고 파일 크기를 크게 줄입니다.
    • 무손실 압축(Lossless Compression): PNG, GIF, SVG 등에 적용되며, 데이터 손실 없이 파일 크기를 줄입니다.
    • 온라인 도구(TinyPNG, Squoosh)나 빌드 도구 플러그인(Webpack, Gulp 등)을 사용하여 이미지 압축을 자동화합니다.
  • 이미지 크기 조정 (Resizing)
    • 실제로 화면에 표시될 크기에 맞춰 이미지를 최적화합니다. 불필요하게 큰 해상도의 이미지를 사용하지 않습니다.
    • 반응형 이미지 (srcset, sizes 속성): 다양한 화면 크기와 해상도에 맞춰 브라우저가 최적의 이미지를 선택하도록 합니다.
      <img srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
           sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px"
           src="medium.jpg" alt="Description">
  • 지연 로딩 (Lazy Loading)
    • 화면 밖 이미지의 로드를 늦춥니다. native lazy 로딩의 실제 시작 시점은 브라우저가 정하며, 화면에 들어오기 전에 요청을 시작할 수도 있습니다.
    • loading="lazy" 속성을 사용하거나, Intersection Observer API를 사용하여 구현합니다.
      <!-- 초기 LCP 이미지가 아닌, 화면 밖의 갤러리 이미지 예 -->
      <img src="gallery.jpg" alt="갤러리 풍경" width="800" height="600" loading="lazy">
  • CDN (Content Delivery Network) 활용
    • 전 세계 여러 서버에 이미지를 분산 저장하여 사용자에게 가장 가까운 서버에서 이미지를 제공함으로써 로딩 속도를 단축합니다.

CSS 최적화

CSS 파일은 HTML 렌더링을 차단하므로, 효율적인 CSS 관리가 중요합니다.

  • CSS Minification
    • 공백, 주석, 마지막 세미콜론 등을 제거하여 CSS 파일 크기를 줄입니다. (PostCSS, cssnano, Webpack css-minimizer-webpack-plugin 등)
  • 사용하지 않는 CSS 제거 (PurgeCSS, UnusedCSS)
    • 프로젝트에서 실제로 사용되지 않는 CSS 규칙을 제거합니다. 특히 UI 프레임워크(Bootstrap, Material-UI 등)를 사용할 때 불필요한 CSS가 많을 수 있습니다.
  • Critical CSS (핵심 CSS) 인라인화
    • 초기 화면 렌더링에 필요한 최소한의 CSS(Above-the-fold CSS)를 <style> 태그를 사용하여 HTML 문서 <head> 내에 직접 삽입합니다. 이렇게 하면 외부 CSS 파일이 로드될 때까지 렌더링이 지연되는 것을 방지하여 FCP 및 LCP를 개선합니다.
    • 나머지 CSS는 비동기로 로드합니다.
  • CSS 스프라이트 (CSS Sprites)
    • 여러 개의 작은 아이콘 이미지를 하나의 큰 이미지로 합치고, background-position을 사용하여 필요한 부분만 보이게 합니다. HTTP 요청 수를 줄여 성능을 향상시킵니다. (최근에는 SVG 아이콘 폰트, 인라인 SVG 등으로 대체되기도 함)
  • 미디어 쿼리 최적화
    • @import 대신 <link> 태그의 media 속성을 사용할 때 특정 미디어(예: print)의 적용 조건과 렌더링 차단을 구별합니다. 적용되지 않는 CSS도 미리 다운로드될 수 있으므로 전송 생략을 보장하지 않습니다.

JavaScript 최적화

JavaScript는 파싱, 컴파일, 실행 과정에서 많은 CPU 시간을 소모하고 네트워크 병목을 유발할 수 있습니다.

  • JavaScript Minification & Uglification
    • 코드 압축뿐만 아니라 변수명 단축 등 난독화를 통해 파일 크기를 최소화합니다. (Terser, UglifyJS 등)
  • Tree Shaking (트리 쉐이킹)
    • Webpack, Rollup과 같은 모듈 번들러를 사용하여 실제로 사용되는 코드만 최종 번들에 포함시키고, 사용되지 않는 코드는 제거합니다. 정적 ES 모듈 구조와 패키지의 부작용 정보 등 번들러가 제거의 안전성을 판단할 조건이 필요합니다. 특히 큰 라이브러리(Lodash 등)의 일부 기능만 사용할 때 결과 번들을 비교합니다. Webpack의 트리 쉐이킹 조건을 참고하되 파일을 분할하는 것과 미사용 코드를 제거하는 것을 구별합니다.
  • 코드 스플리팅 (Code Splitting)
    • 애플리케이션의 번들을 여러 작은 조각으로 분리하여, 사용자가 현재 필요한 코드만 다운로드하도록 합니다. React의 React.lazy()와 Suspense, Vue의 비동기 컴포넌트, Webpack의 dynamic import() 등을 활용합니다.
  • 비동기 로딩 (async / defer 속성)
    • 외부 classic <script>의 async·defer는 다운로드를 HTML 파싱과 병행하게 합니다.
      • async: 준비되는 대로 실행하며 실행 중 파싱을 멈출 수 있습니다. 스크립트 사이 실행 순서가 보장되지 않으므로 의존성을 구분합니다.
      • defer: HTML 파싱이 끝난 후 문서 순서대로 실행합니다. 같은 지연 스크립트 사이의 순서 의존성을 표현할 때 사용합니다.
  • 서드 파티 스크립트 최적화
    • Google Analytics, 광고 스크립트, 소셜 미디어 위젯 등 외부 스크립트는 성능에 큰 영향을 미칠 수 있습니다. 필요한 경우에만 로드하거나, 지연 로딩 또는 async/defer 속성을 적용합니다.
  • 불필요한 라이브러리 제거
    • 작은 기능을 위해 큰 라이브러리 전체를 포함하는 대신, 해당 기능만 직접 구현하거나 더 가벼운 대안을 찾습니다.
  • Long Task 방지
    • 메인 스레드를 50ms 넘게 블로킹하는 긴 JavaScript 실행을 피합니다. 복잡한 계산은 Web Workers를 사용하여 백그라운드 스레드에서 처리합니다.

폰트 (Web Font) 최적화

웹 폰트는 디자인 일관성을 제공하지만, 용량이 커서 로딩 성능에 악영향을 미칠 수 있습니다.

  • Web Font 형식 최적화
    • WOFF2 (Web Open Font Format 2.0) 사용: 가장 최신이며 압축률이 뛰어납니다. 대부분의 최신 브라우저에서 지원합니다.
    • WOFF, EOT, TTF 등은 구형 브라우저 지원을 위해 폴백(fallback)으로 제공할 수 있습니다.
  • Subset Font (부분 집합 폰트)
    • 실제로 웹 페이지에서 사용되는 문자(예: 한글 폰트의 경우 필요한 한글 자모 및 숫자, 영문)만 포함하여 폰트 파일 크기를 줄입니다.
  • font-display 속성 사용
    • CSS @font-face 규칙에서 font-display 속성을 사용하여 폰트 로딩 방식을 제어합니다.
      • swap: 폰트가 로드되기 전까지는 시스템 폰트를 사용하고, 로드된 후에는 웹 폰트로 교체합니다. FOUT(Flash Of Unstyled Text)가 발생할 수 있지만, 텍스트가 바로 표시되므로 사용자 경험에 좋습니다.
      • optional: 매우 짧은 block 기간 뒤 swap 기간을 두지 않습니다. 제때 준비되지 않은 폰트는 이 표시에서 대체 폰트를 계속 사용하게 될 수 있습니다. 다운로드를 항상 중단한다는 뜻으로 일반화하지 않습니다. font-display의 표시 시간선을 확인합니다.
  • preload 링크 태그 사용
    • 가장 중요한 폰트 파일에 대해 <link rel="preload" href="your-font.woff2" as="font" type="font/woff2" crossorigin> 태그를 사용하여 브라우저가 가능한 한 빨리 폰트를 가져오도록 지시합니다.

HTTP 캐싱 전략 강화

asset 최적화에서는 위험 신호, 최적화 지점, 사용자 영향도를 확인합니다.

HTTP 캐시 저장·재검증 생명주기 — 상태 추적

저장 body·ETag를 이용한 조건부 GET에서 304와 200은 서로 다른 응답 대안입니다. 304는 검증된 기존 body를 사용하고, 200은 새 응답의 저장 정책을 확인합니다.

캐시는 저장 여부와 재사용 조건을 따로 판단합니다

HTTP CACHE

캐시는 저장 여부와 재사용 조건을 따로 판단합니다

같은 URL을 다시 요청할 때 저장된 응답의 존재·신선도·정책을 확인합니다. 캐시가 오래됐다는 이유만으로 본문을 바로 삭제하지 않습니다.

재검증 요청의 304와 200을 구별한다5노드4관계의기본ETag조건부GET이다. 저장응답이있고재검증이필요하며서버가성공적으로판단하는조건을가정한다. 동일304는본문없이기존본문/메타데이터사용,변경200은새본문과정책확인이다. miss/fresh/no-store/private와다른오류·ServiceWorker·stale예외는아래보조설명/별도범위이고이그림에억지로추가하지않는다. mobile같다.재검증 필요한 응답저장된 ETag 있음조건부 GETIf-None-Match성공한 검증내용 동일?304 · 새 본문 없음기존 본문 재사용200 · 새 본문새 정책 확인·교체동일변경
재검증 요청의 304와 200을 구별한다5노드4관계의기본ETag조건부GET이다. 저장응답이있고재검증이필요하며서버가성공적으로판단하는조건을가정한다. 동일304는본문없이기존본문/메타데이터사용,변경200은새본문과정책확인이다. miss/fresh/no-store/private와다른오류·ServiceWorker·stale예외는아래보조설명/별도범위이고이그림에억지로추가하지않는다. mobile같다.재검증 필요저장된 ETag조건부 GETIf-None-Match성공한 검증내용 동일?304기존 본문메타데이터 갱신200 · 새 본문새 정책으로저장 여부 판단동일변경

저장된 응답 없음 · miss

네트워크로 요청합니다. 받은 응답이 저장 가능한 정책이면 본문과 헤더·신선도 정보를 저장합니다.

fresh · 재사용 허용

max-age 등 신선도 기간 안이며 재검증 요구가 없으면 저장된 응답을 사용합니다. 요청·응답의 다른 캐시 조건도 확인합니다.

재검증 필요 · stale / no-cache

stale이거나 no-cache가 재검증을 요구하면 서버에 확인합니다. ETag·Last-Modified가 있으면 조건부 요청을 사용합니다.

조건부 요청 → 서버 응답 → 캐시 처리

If-None-Match: "asset-v1"

304 Not Modified · 기존 본문 재사용

검증된 기존 본문을 유지하고 응답에 따라 메타데이터를 갱신합니다. 304는 새 본문을 보내지 않습니다.

200 OK · 응답 교체

새 본문과 헤더를 받고 정책이 허용하면 저장합니다. 검증자가 없으면 일반 요청으로 새 응답을 받아야 할 수 있습니다.

no-store · 저장 금지

새 응답을 캐시에 저장하지 않습니다. 이미 저장된 같은 URL의 응답을 자동으로 삭제한다는 뜻은 아닙니다.

private · 개인 캐시

브라우저 같은 개인 캐시는 저장할 수 있지만 공유 캐시는 저장하지 않습니다. no-store와 서로 다른 정책입니다.

no-cache · 재사용 전 검증

저장은 가능하지만 재사용 전 서버 검증을 요구합니다. fresh여도 검증을 생략하지 않습니다.

새 내용 해시 URL은 이전 URL의 응답을 지우는 대신 다른 캐시 항목을 요청합니다. 이 그림은 일반 HTTP 요청의 기본 흐름이며 Service Worker·history 복원·stale 허용 정책은 별도입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 저장된 ETag가 있고 재검증이 필요한 정상 GET을 가정합니다. 304와 200은 같은 요청의 연속 응답이 아닌 독립 대안입니다. 200 장면의 캐시 교체는 새 응답 저장을 정책이 허용한 경우만 추적합니다. no-store는 새 응답 저장 금지이며 이전 동일 URL 응답의 자동 삭제를 뜻하지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
전체 관계·조건 설명노드 5개·관계 4개의 기본 ETag 조건부 GET이다. 저장 응답이 있고 재검증이 필요하며 서버가 성공적으로 판단하는 조건을 가정한다. 동일한 내용의 304는 본문 없이 기존 본문/메타데이터 사용, 변경된 내용의 200은 새 본문과 정책 확인이다. miss/fresh/no-store/private와 다른 오류·ServiceWorker·stale 예외는 아래 보조 설명/별도 범위이고 이 그림에 억지로 추가하지 않는다. mobile도 같다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
저장한 응답캐시: 저장 body+ETag / 요청: If-None-Match캐시에 body와 ETag가 있지만 재검증이 필요하다stale 응답은 재사용 가능성 확인이 필요하다
대안 A · 내용 동일 304HTTP: 304 / 캐시 body: 재사용서버가 같은 표현임을 확인한다검증자가 같은 표현을 확인했으므로 새 body 없이 기존 body를 재사용한다
대안 B · 내용 변경 200HTTP: 200 새 body / 캐시: 저장 허용 시 현재 표현으로 갱신새 body와 헤더를 받고 저장 허용 정책을 확인한 뒤 캐시를 교체한다내용이 변경되어 새 body를 받으며, 이 사례는 저장이 허용되어 기존 캐시 항목을 교체한다

HTTP 캐시 저장·재검증 생명주기 — 상태 추적

저장 body·ETag를 이용한 조건부 GET에서 304와 200은 서로 다른 응답 대안입니다. 304는 검증된 기존 body를 사용하고, 200은 새 응답의 저장 정책을 확인합니다.

캐시는 저장 여부와 재사용 조건을 따로 판단합니다

HTTP CACHE

캐시는 저장 여부와 재사용 조건을 따로 판단합니다

같은 URL을 다시 요청할 때 저장된 응답의 존재·신선도·정책을 확인합니다. 캐시가 오래됐다는 이유만으로 본문을 바로 삭제하지 않습니다.

재검증 요청의 304와 200을 구별한다5노드4관계의기본ETag조건부GET이다. 저장응답이있고재검증이필요하며서버가성공적으로판단하는조건을가정한다. 동일304는본문없이기존본문/메타데이터사용,변경200은새본문과정책확인이다. miss/fresh/no-store/private와다른오류·ServiceWorker·stale예외는아래보조설명/별도범위이고이그림에억지로추가하지않는다. mobile같다.재검증 필요한 응답저장된 ETag 있음조건부 GETIf-None-Match성공한 검증내용 동일?304 · 새 본문 없음기존 본문 재사용200 · 새 본문새 정책 확인·교체동일변경
재검증 요청의 304와 200을 구별한다5노드4관계의기본ETag조건부GET이다. 저장응답이있고재검증이필요하며서버가성공적으로판단하는조건을가정한다. 동일304는본문없이기존본문/메타데이터사용,변경200은새본문과정책확인이다. miss/fresh/no-store/private와다른오류·ServiceWorker·stale예외는아래보조설명/별도범위이고이그림에억지로추가하지않는다. mobile같다.재검증 필요저장된 ETag조건부 GETIf-None-Match성공한 검증내용 동일?304기존 본문메타데이터 갱신200 · 새 본문새 정책으로저장 여부 판단동일변경

저장된 응답 없음 · miss

네트워크로 요청합니다. 받은 응답이 저장 가능한 정책이면 본문과 헤더·신선도 정보를 저장합니다.

fresh · 재사용 허용

max-age 등 신선도 기간 안이며 재검증 요구가 없으면 저장된 응답을 사용합니다. 요청·응답의 다른 캐시 조건도 확인합니다.

재검증 필요 · stale / no-cache

stale이거나 no-cache가 재검증을 요구하면 서버에 확인합니다. ETag·Last-Modified가 있으면 조건부 요청을 사용합니다.

조건부 요청 → 서버 응답 → 캐시 처리

If-None-Match: "asset-v1"

304 Not Modified · 기존 본문 재사용

검증된 기존 본문을 유지하고 응답에 따라 메타데이터를 갱신합니다. 304는 새 본문을 보내지 않습니다.

200 OK · 응답 교체

새 본문과 헤더를 받고 정책이 허용하면 저장합니다. 검증자가 없으면 일반 요청으로 새 응답을 받아야 할 수 있습니다.

no-store · 저장 금지

새 응답을 캐시에 저장하지 않습니다. 이미 저장된 같은 URL의 응답을 자동으로 삭제한다는 뜻은 아닙니다.

private · 개인 캐시

브라우저 같은 개인 캐시는 저장할 수 있지만 공유 캐시는 저장하지 않습니다. no-store와 서로 다른 정책입니다.

no-cache · 재사용 전 검증

저장은 가능하지만 재사용 전 서버 검증을 요구합니다. fresh여도 검증을 생략하지 않습니다.

새 내용 해시 URL은 이전 URL의 응답을 지우는 대신 다른 캐시 항목을 요청합니다. 이 그림은 일반 HTTP 요청의 기본 흐름이며 Service Worker·history 복원·stale 허용 정책은 별도입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 저장된 ETag가 있고 재검증이 필요한 정상 GET을 가정합니다. 304와 200은 같은 요청의 연속 응답이 아닌 독립 대안입니다. 200 장면의 캐시 교체는 새 응답 저장을 정책이 허용한 경우만 추적합니다. no-store는 새 응답 저장 금지이며 이전 동일 URL 응답의 자동 삭제를 뜻하지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
전체 관계·조건 설명노드 5개·관계 4개의 기본 ETag 조건부 GET이다. 저장 응답이 있고 재검증이 필요하며 서버가 성공적으로 판단하는 조건을 가정한다. 동일한 내용의 304는 본문 없이 기존 본문/메타데이터 사용, 변경된 내용의 200은 새 본문과 정책 확인이다. miss/fresh/no-store/private와 다른 오류·ServiceWorker·stale 예외는 아래 보조 설명/별도 범위이고 이 그림에 억지로 추가하지 않는다. mobile도 같다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
저장한 응답캐시: 저장 body+ETag / 요청: If-None-Match캐시에 body와 ETag가 있지만 재검증이 필요하다stale 응답은 재사용 가능성 확인이 필요하다
대안 A · 내용 동일 304HTTP: 304 / 캐시 body: 재사용서버가 같은 표현임을 확인한다검증자가 같은 표현을 확인했으므로 새 body 없이 기존 body를 재사용한다
대안 B · 내용 변경 200HTTP: 200 새 body / 캐시: 저장 허용 시 현재 표현으로 갱신새 body와 헤더를 받고 저장 허용 정책을 확인한 뒤 캐시를 교체한다내용이 변경되어 새 body를 받으며, 이 사례는 저장이 허용되어 기존 캐시 항목을 교체한다

모든 에셋에 적용될 수 있는 중요한 최적화 기법입니다.

  • Cache-Control 헤더
    • 서버 응답 시 Cache-Control HTTP 헤더를 사용하여 브라우저 캐싱 정책을 명확하게 지시합니다.
    • public, max-age=31536000, immutable: 정적 에셋에 대해 긴 캐싱 기간을 설정하여 재방문 시 즉시 로드되도록 합니다. immutable은 해당 응답의 freshness 수명 동안 재검증을 생략할 수 있음을 뜻합니다. 파일명에 content hash를 넣고 변경 시 새 URL을 배포합니다.
    • no-cache: 서버에 항상 재검증 요청을 보내지만, 유효한 캐시가 있으면 사용합니다.
    • no-store: 캐시를 저장하지 않습니다. (매우 민감한 정보에 사용)
  • ETag / Last-Modified
    • 서버에서 파일이 변경되었는지 확인하는 데 사용되는 유효성 검사 토큰입니다. 브라우저가 캐시된 에셋을 다시 요청할 때 If-None-Match 또는 If-Modified-Since 헤더를 포함하여 서버에 전송하고, 서버는 파일이 변경되지 않았으면 304 Not Modified 응답을 보냅니다. 이는 파일 자체를 재전송하는 것보다 훨씬 효율적입니다.
  • 버전 관리 (Cache Busting)
    • 정적 에셋 파일명에 해시 값이나 버전 번호를 포함시켜 파일 내용이 변경될 때마다 새로운 파일명으로 배포합니다.
    • (예: bundle.1a2b3c4d.js, style.v1.2.3.css)
    • 새 파일명은 새 캐시 키가 됩니다. 사용자가 새 파일을 받으려면 HTML이나 manifest도 그 새 URL을 참조해야 하며, 오래된 HTML이 구 URL을 계속 가리키는 경우까지 자동으로 해결하지는 않습니다.

에셋 최적화는 모든 파일을 똑같이 줄이는 작업이 아니라, 초기 화면과 지연 로드 자원을 나눠 우선순위를 정하는 작업입니다.


예시

전송량만 계산하는 모델을 정합니다. 첫 화면의 이미지 200kB, 로고 20kB, 폰트 80kB, JS 400kB와 화면 밖 갤러리 300kB를 모두 처음 요청하면 합계 1000kB입니다. 갤러리만 필요한 시점으로 미루면 초기 대상은 200 + 20 + 80 + 400 = 700kB로 30% 줄지만, 이후 갤러리를 보면 다시 300kB를 받습니다. 이 모델은 캐시·압축 차이를 제외하며 전송량 감소가 LCP 30% 단축을 보장하지 않습니다. 초기 LCP 이미지를 함께 lazy로 미루는 것은 반대 결과를 낼 수 있습니다.

HTTP 캐시에서는 이미 저장된 응답의 현재 age가 600초이고 max-age=3600이며 다른 재검증 요구가 없다고 가정하면 freshness가 3000초 남습니다. age가 3700초가 되면 만료되지만 본문이 곧바로 사라진다는 뜻은 아닙니다. 저장된 ETag를 조건부 GET에 보내 304를 받으면 기존 본문을 사용하고 메타데이터를 갱신합니다. 네트워크 왕복과 헤더 전송은 여전히 있습니다.

독립 확인

① 같은 저장 응답이 age 60초이지만 응답 정책에 인자 없는 no-cache가 있다면 서버 확인을 생략할 수 있나요? ② 검증 후 200과 새 본문이 왔을 때 기존 본문으로 계속 표시해야 하나요? ③ 새 해시 파일을 배포했지만 사용자의 오래된 HTML이 구 파일명을 참조하면 새 파일이 자동으로 선택되나요? ④ 위 전송량 모델에서 JS를 400→250kB로 줄이고 갤러리를 미루면 초기 합계와 최초 1000kB 대비 감소율은 얼마인가요?

정책과 수량 확인

① fresh여도 재사용 전에 성공적인 검증이 필요합니다. ② 이 성공 응답의 새 본문을 사용하며 저장 가능 여부는 새 정책으로 판단합니다. ③ 구 URL은 별도의 캐시 항목이므로 HTML·manifest 참조도 새 URL로 갱신해야 합니다. ④ 200 + 20 + 80 + 250 = 550kB, 감소분 450kB이므로 45%입니다. 실제 배포의 압축 크기나 실행 시간 측정값으로 확대하지 않습니다. HTTP 캐시의 재사용·검증 조건은 이 기본 GET 모델보다 넓으므로 Service Worker나 stale 허용 정책은 따로 구별합니다.

아키텍처 적용 검증 체크리스트

개념 점검
  • 이미지/CSS/JS/폰트 각각에서 가장 영향이 큰 최적화 포인트를 설명할 수 있는가?
  • 캐시 전략(max-age, immutable, ETag)의 차이를 구분할 수 있는가?
구현 점검
  • 정적 에셋에 해시 기반 파일명 전략(Cache Busting)을 적용했는가?
  • 초기 화면에 필요한 리소스와 지연 로드 리소스를 분리했는가?
  • 성능 개선 후 수치 검증은 13장 3절의 성능 감사 흐름으로 재확인했는가?

배포 체크 요약

이번 장에서는 웹 성능 최적화의 핵심적인 부분인 에셋 최적화에 대해 심층적으로 학습했습니다.

이번 절에서는 이미지, CSS, JavaScript, 폰트의 파일 크기를 줄이고 로딩 방식을 개선하는 전략을 정리했습니다.

포맷 선택, 압축, 반응형 이미지, 지연 로딩, 미니피케이션, 트리 쉐이킹, 코드 스플리팅, 비동기 로딩, 웹 폰트 최적화, HTTP 캐싱을 에셋별로 검토합니다.

에셋 최적화는 단순히 파일 크기를 줄이는 것을 넘어, 브라우저의 렌더링 과정을 이해하고 사용자에게 필요한 리소스만을 필요한 시점에 제공하는 복합적인 노력입니다.

필요한 리소스를 적절한 시점에 제공하면 페이지 로딩과 상호작용 성능을 개선할 수 있습니다.

에셋 최적화의 마지막 판단은 초기 화면, 지연 로드, 캐시 정책을 하나의 배포 루틴으로 묶어 확인하는 것입니다.