HTTP 캐싱 전략: ETag와 Cache-Control 적용
HTTP 캐싱 기초부터 ETag와 Cache-Control의 차이점, 조건부 요청 처리, Express에서의 Cache-Control Express 설정과 Node ETag 설정 예제, 최적화 적용법을 포함한 실무 중심의 기술문서
목차
개요
웹 애플리케이션 성능 개선에 캐싱은 필수 요소다. HTTP 레벨 캐시는 응답 재전송을 줄여 지연 시간을 낮추고 대역폭을 절감한다. 이 글에서는 ETag와 Cache-Control의 개념을 명확히 정리하고, Node.js(특히 Express)에 어떻게 적용하는지 단계별 예제를 통해 설명한다.
HTTP 캐싱 기본
캐싱의 목적과 동작
캐싱은 클라이언트나 중간 프록시가 서버 응답을 저장해 동일한 리소스 요청 시 재사용하도록 하는 기법이다. 서버는 응답 헤더로 캐시 정책을 전달하고, 클라이언트는 조건부 요청을 통해 변경 여부를 확인한다.
주요 헤더
- Cache-Control: 캐시 동작을 세부적으로 제어한다.
- ETag: 리소스 버전 식별자(해시 또는 토큰)를 제공한다.
- Last-Modified: 최종 변경 시각을 나타낸다.
- If-None-Match / If-Modified-Since: 조건부 요청에 사용된다.
ETag 상세
개념
ETag는 리소스의 고유 식별자다. 서버가 응답에 ETag를 담아 보내면 클라이언트는 다음 요청 시 If-None-Match에 이 값을 포함한다. 서버는 값이 동일하면 304 Not Modified로 응답해 본문을 전송하지 않는다.
강한 ETag와 약한 ETag
- 강한 ETag: 바이트 단위까지 동일해야 한다. 정확한 동등성을 의미.
- 약한 ETag(weak): W/ 접두어로 표시되며, 의미적으로 동등한 경우에만 같다고 본다.
Cache-Control 상세
주요 디렉티브
- max-age: 응답을 얼마 동안 재사용할지(초 단위).
- public / private: 공유 캐시(프록시)에 보관 가능한지 여부.
- no-cache: 매 요청마다 서버 검증 필요(본문 전송 가능).
- no-store: 절대 저장 금지.
- must-revalidate: 만료 시 재검증 강제.
전략 선택
정적 자원(이미지, JS, CSS)은 긴 max-age와 파일명 해싱을 함께 사용해 캐시 수명을 길게 가져간다. 동적 응답은 ETag나 Last-Modified로 조건부 요청을 처리해 불필요한 본문 전송을 줄인다.
Node.js 적용
Express 기본 동작
Express는 기본적으로 ETag를 자동 생성한다. express.static을 사용하면 Cache-Control도 설정 가능하다. 다만 서비스 요구에 따라 명시적으로 제어하는 것이 안전하다.
Cache-Control Express 설정 예제
정적 파일을 30일 동안 캐시하도록 설정하는 예
const express = require('express')
const app = express()
app.use('/static', express.static('public', {
maxAge: 30 * 24 * 60 * 60 * 1000 // 밀리초 단위
}))
app.listen(3000)
Node ETag 설정 예제: 기본 사용과 비활성화
Express의 ETag 생성 방식을 확인하거나 비활성화하는 방법
const express = require('express')
const app = express()
// 기본 ETag 사용(Express 기본값)
app.get('/resource', (req, res) => {
res.send('Hello')
})
// ETag 비활성화
app.disable('etag')
app.get('/no-etag', (req, res) => {
res.send('No ETag')
})
app.listen(3000)
커스텀 ETag 생성 예제
파일 내용이나 객체를 해시하여 ETag를 직접 제어하면 더 세밀한 캐시 정책 적용이 가능하다.
const express = require('express')
const crypto = require('crypto')
const app = express()
function generateETag(body) {
return crypto.createHash('sha1').update(body).digest('hex')
}
app.get('/custom-etag', (req, res) => {
const body = 'dynamic content ' + Date.now()
const etag = generateETag(body)
res.set('ETag', etag)
if (req.headers['if-none-match'] === etag) {
return res.status(304).end()
}
res.send(body)
})
app.listen(3000)
조건부 요청 흐름 요약
- 클라이언트가 최초 요청을 보냄. 서버는 ETag와 Cache-Control을 응답함.
- 클라이언트가 만료 전이면 로컬 캐시 사용. 만료거나 no-cache면 If-None-Match 포함 재요청.
- 서버가 변경 없으면 304 응답. 변경 있으면 200과 본문 전송.
권장 실무 전략
- 정적 자원은 파일명 해시(예: app.v123.css) + 긴 max-age 사용.
- 동적 응답은 ETag 또는 Last-Modified로 조건부 요청 처리.
- 프라이버시 민감한 응답은 private 또는 no-store 설정.
- 복잡한 리소스는 커스텀 ETag로 정확한 변경 감지 적용.
결론
ETag와 Cache-Control은 서로 보완하는 도구다. Cache-Control로 보관 정책을 정하고, ETag로 변경 여부를 검증하면 네트워크와 서버 리소스를 함께 절약할 수 있다. Node.js 환경에서는 Express 기본 기능을 활용하되, 서비스 특성에 맞춰 명시적 설정과 필요 시 커스텀 ETag 적용을 고려한다.