캐시와 CORS
HTTP의 마지막 주제로, 성능을 책임지는 캐시(Cache)와 브라우저 보안을 책임지는 CORS(Cross-Origin Resource Sharing)를 다룹니다. 둘 다 헤더 몇 줄처럼 보이지만, 실제로는 브라우저, CDN, 프록시, 서버가 함께 해석하는 규칙입니다.
HTTP 캐시가 하는 일
HTTP 캐시는 이전 응답을 저장해두었다가 같은 요청에 재사용합니다. 캐시가 잘 동작하면 지연 시간과 네트워크 사용량이 줄고, 원본 서버 부하도 낮아집니다. 단, 캐시는 “아무 응답이나 저장하는 저장소”가 아니라 저장 가능 여부, 신선도(freshness), 검증(validation), 캐시 키를 기준으로 판단합니다.
캐시는 크게 브라우저 같은 private cache와 CDN·프록시 같은 shared cache로 나눌 수 있습니다. 같은 URL이라도 Vary 헤더가 있으면 요청 헤더 일부가 캐시 키에 포함됩니다. 예를 들어 Vary: Accept-Encoding은 gzip/br 응답을 구분하고, 동적으로 CORS Origin을 반사하는 응답은 보통 Vary: Origin을 함께 고려해야 합니다.
Cache-Control 지시자
Cache-Control은 캐시 정책의 핵심 헤더입니다. max-age는 신선한 시간, private과 public은 저장 위치, no-cache와 no-store는 저장과 재검증의 차이를 나타냅니다.
| 시나리오 | Cache-Control 설정 | 이유 |
|---|---|---|
| 해시 파일명 JS/CSS | public, max-age=31536000, immutable | 파일 내용이 바뀌면 URL도 바뀜 |
| HTML 문서 | no-cache | 저장은 가능하지만 사용 전 재검증 |
| 사용자별 API | private, no-cache | 브라우저 캐시는 가능, 공유 캐시는 제한 |
| 민감 정보 | no-store | 저장 자체를 피해야 함 |
| CDN 전용 TTL | s-maxage=300, stale-while-revalidate=30 | 공유 캐시에 별도 TTL과 stale 정책 제공 |
no-cache는 “캐시하지 말라”가 아니라 “재사용 전에 원본 서버에 검증하라”에 가깝습니다. 이때 ETag나 Last-Modified 같은 검증자가 없으면 보통 304 대신 200 전체 응답을 다시 받게 됩니다. HTTP 캐시에 저장하지 않아야 하는 응답에는 no-store를 써야 합니다.
Fresh Cache와 Conditional Request
캐시된 응답이 아직 fresh라면 브라우저나 CDN은 원본 서버에 가지 않고 응답을 재사용할 수 있습니다. stale 상태가 되면 검증자가 있을 때 조건부 요청을 보내고, 서버는 내용이 바뀌지 않았으면 304 Not Modified로 본문 없이 응답할 수 있습니다.
| 검증자 | 요청 헤더 | 서버 판단 기준 | 특징 |
|---|---|---|---|
ETag | If-None-Match | 리소스 버전 식별자 비교 | strong/weak validator가 있고 우선순위가 높음 |
Last-Modified | If-Modified-Since | 마지막 수정 시각 비교 | 구현이 쉽지만 초 단위·시계 오차 영향 |
| 둘 다 있음 | 둘 다 전송될 수 있음 | 일반적으로 ETag 검증이 우선 | 서버 구현과 HTTP 규칙을 함께 확인 |
실무 캐시 전략
정적 자산은 파일명에 콘텐츠 해시를 넣고 긴 TTL을 주는 전략이 가장 안정적입니다. 반대로 HTML은 최신 자산 URL을 알려주는 진입점이므로 짧게 캐시하거나 매번 재검증하도록 두는 편이 안전합니다.
이 전략의 핵심은 “변경 가능성”을 URL에 반영하는 것입니다. /app.7f3a9c.js처럼 내용이 바뀌면 URL이 바뀌는 파일은 오래 캐시해도 되고, /index.html처럼 같은 URL에서 내용이 바뀌는 문서는 검증 중심으로 둡니다.
CORS 동작 원리
브라우저의 동일 출처 정책(Same-Origin Policy)은 한 출처의 스크립트가 다른 출처의 응답을 마음대로 읽지 못하게 막습니다. 여기서 출처(origin)는 scheme, host, port 조합입니다.
CORS는 서버가 Access-Control-* 응답 헤더로 “이 출처에는 응답을 공유해도 된다”고 알려주는 프로토콜입니다. 중요한 점은 CORS가 서버 간 요청을 막는 기능이 아니라, 브라우저가 JavaScript에 응답을 노출할지 결정하는 규칙이라는 점입니다. 요청 자체가 전송될 수 있으므로 CSRF 방어는 별도 정책으로 설계해야 합니다.
Simple Request와 Preflight Request
브라우저는 CORS-safelisted 조건에 들어오는 요청은 바로 보내고, 그 조건을 벗어난 요청은 먼저 OPTIONS preflight로 서버의 허용 여부를 확인합니다.
예를 들어 GET 요청이라도 Authorization 헤더를 붙이면 preflight 대상이 될 수 있고, POST라도 Content-Type: application/json이면 safelisted content type이 아니므로 preflight가 필요합니다.
CORS 에러 해결 패턴
CORS 에러는 프론트엔드 코드에서 우회하는 문제가 아니라, 서버가 어떤 출처와 어떤 메서드·헤더·credentials를 허용할지 명확히 응답해야 해결됩니다.
| 상황 | 확인할 것 |
|---|---|
| 개발 환경 CORS | dev server proxy로 같은 출처처럼 중계할 수 있음 |
| 운영 환경 CORS | 서버가 정확한 Access-Control-Allow-Origin 반환 |
| 쿠키 포함 요청 | Allow-Credentials: true와 특정 Origin 필요 |
| 와일드카드 Origin | credentials 요청에는 * 사용 불가 |
| 커스텀 응답 헤더 읽기 | Access-Control-Expose-Headers에 이름 추가 |
| preflight 반복 | Access-Control-Max-Age를 쓰되 브라우저 한계 고려 |
| Origin 반사 | 허용 목록 검증과 Vary: Origin 함께 고려 |
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class CORSHandler(BaseHTTPRequestHandler):
"""허용 목록 기반 CORS 응답 예시"""
ALLOWED_ORIGINS = {"http://localhost:3000", "http://localhost:5173"}
def set_cors_headers(self):
origin = self.headers.get("Origin")
if origin in self.ALLOWED_ORIGINS:
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Access-Control-Allow-Credentials", "true")
self.send_header("Access-Control-Allow-Methods", "GET, POST")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.send_header("Access-Control-Expose-Headers", "X-Request-Id")
self.send_header("Access-Control-Max-Age", "86400")
self.send_header("Vary", "Origin")
def do_OPTIONS(self):
self.send_response(204)
self.set_cors_headers()
self.end_headers()
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.set_cors_headers()
self.end_headers()
data = {"message": "CORS success", "path": self.path}
self.wfile.write(json.dumps(data).encode())
# HTTPServer(("localhost", 8080), CORSHandler).serve_forever()다음 장에서는 HTTP 통신의 보안을 담당하는 HTTPS와 TLS를 살펴보겠습니다.