Part 1. 언어와 웹의 본질

HTTP 캐싱 완벽 가이드: Cache-Control, ETag로 네트워크 비용 줄이기

HTTP 캐싱 완벽 가이드: Cache-Control, ETag로 네트워크 비용 줄이기

크리스가 운영하는 웹 서비스가 대박이 났다. 트래픽이 폭주하자 서버 비용 명세서도 함께 폭발했다.

분석해보니 사용자가 새로고침을 할 때마다 5MB짜리 배경 이미지와 무거운 자바스크립트 파일을 매번 다시 다운로드하고 있었다.

"어차피 똑같은 파일인데, 브라우저가 좀 알아서 저장해두면 안 되나?"

이런 비효율을 막기 위해 존재하는 기술이 바로 HTTP 캐싱(Caching)이다. 브라우저에게 "이 파일은 내 컴퓨터에 저장해두고 재사용해"라고 알려주는 규칙, 즉 HTTP 헤더를 제대로 설정하는 것만으로도 서버 비용을 획기적으로 줄이고 로딩 속도를 높일 수 있다.

1. 캐시의 유효기간: Cache-Control

가장 기본이 되는 헤더는 Cache-Control이다. 브라우저에게 이 자원을 얼마나 오래 기억할지 명령한다.

// server-response.ts

// 1. "1년 동안 나를 찾지 마. 저장된 거 써." (변하지 않는 정적 파일)
res.setHeader('Cache-Control', 'max-age=31536000');

// 2. "저장은 하되, 쓸 때마다 나한테 물어봐." (HTML 등 자주 바뀌는 파일)
res.setHeader('Cache-Control', 'no-cache');

// 3. "절대 저장하지 마. 보안 문서야." (개인정보 등)
res.setHeader('Cache-Control', 'no-store');

여기서 가장 많이 오해하는 것이 no-cache다.

이름만 보면 "캐시 하지 마" 같지만, 실제 뜻은 "캐시는 하되, 사용할 때마다 서버에 유효한지 확인(Validation)해라"이다. 아예 저장조차 하지 말아야 한다면 no-store를 써야 한다.

2. 유효성 검사: ETag와 304 Not Modified

max-age가 만료되었다고 해서 무조건 파일을 다시 다운로드하는 것은 낭비다. 파일 내용이 여전히 똑같을 수도 있기 때문이다.

이때 사용하는 것이 ETag다.

ETag: 파일의 지문(Fingerprint)

서버는 파일을 보낼 때 내용물을 해싱한 고유 값을 ETag 헤더에 붙여서 보낸다.

  • 브라우저: "이 파일 max-age 끝났네. 근데 서버야, 내가 가진 파일 지문이 "abc-123"인데 이거 아직 유효해?" (If-None-Match: "abc-123")
  • 서버: (파일을 확인해 보니 지문이 여전히 "abc-123"이다)
  • 서버: "어, 그거 안 바뀌었어. 그냥 그거 써." (304 Not Modified)
  • 이때 서버는 파일 본문(Body)을 보내지 않고 헤더만 보낸다. 데이터 전송량이 확 줄어드는 마법 같은 순간이다.

    3. 실전 캐싱 전략 (Best Practice)

    그렇다면 리액트 프로젝트에서는 어떤 전략을 써야 할까? 크리스의 팀이 채택한 전략은 다음과 같다.

    전략 1: HTML은 no-cache

    HTML 파일은 진입점이다. 여기서 JS, CSS 파일의 경로를 불러온다. HTML이 옛날 버전으로 캐시 되어 있으면, 사용자는 영원히 최신 배포를 못 볼 수도 있다.

    // nginx.conf or server.ts
    // index.html 요청 시
    res.setHeader('Cache-Control', 'no-cache'); 
    // "항상 서버에 물어보고 가져가!"

    전략 2: JS/CSS/Image는 max-age 길게 + 파일명 해싱

    빌드된 JS 파일(main.a1b2c.js)은 내용이 바뀌면 파일명(해시)도 바뀐다. 즉, 파일 이름이 같다면 내용은 100% 같다는 뜻이다.

    그러므로 브라우저에게 "이건 절대 안 바뀌니 1년 동안 저장해"라고 해도 안전하다.

    // static-files.ts
    // .js, .css, .png 요청 시
    res.setHeader('Cache-Control', 'max-age=31536000, immutable');

    만약 배포를 새로 하면?

    HTML 파일(no-cache)이 갱신되어 새로운 파일명(main.d4e5f.js)을 가리킬 것이고, 브라우저는 새로운 파일을 다운로드한다. 이것이 완벽한 캐시 무효화 전략이다.

    4. 캐싱 동작 확인하기

    개발자 도구(Network 탭)를 열어보면 캐시가 잘 동작하는지 확인할 수 있다.

  • 200 OK (from memory cache): 브라우저가 서버에 요청도 안 하고 메모리에서 꺼내 씀. (가장 빠름)
  • 200 OK (from disk cache): 브라우저를 껐다 켜도 파일에 저장된 것을 꺼내 씀.
  • 304 Not Modified: 서버에 물어봤는데(no-cache), 안 바뀌었다고 해서 기존 거 씀. (헤더 통신 비용만 발생)
  • 핵심 정리

  • Cache-Control: no-store: 절대 저장 금지.
  • Cache-Control: no-cache: 저장은 하되, 쓸 때마다 서버에 확인(304).
  • Cache-Control: max-age=...: 해당 시간 동안은 서버에 안 물어보고 그냥 씀.
  • 전략: HTML은 no-cache, 해싱 된 정적 파일(JS, CSS)은 max-age를 길게 잡는다.
  • 이것으로 Part 1. 언어와 웹의 본질 파트가 끝났다.

    기초 체력을 다졌으니, 이제 본격적으로 리액트의 심장을 열어볼 차례다.

    다음 글 예고:

    리액트 컴포넌트는 어떻게 태어나고, 변하고, 사라질까?

    "리액트 렌더링의 2단계: Render Phase와 Commit Phase의 차이점" 편에서 Part 2가 시작된다.


    🔗 참고 링크

  • Cache-Control
  • ETag
  • HTTP Caching