안동민 개발노트 아이콘

안동민 개발노트

9장 : HTTP

캐시와 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는 신선한 시간, privatepublic은 저장 위치, no-cacheno-store는 저장과 재검증의 차이를 나타냅니다.

시나리오Cache-Control 설정이유
해시 파일명 JS/CSSpublic, max-age=31536000, immutable파일 내용이 바뀌면 URL도 바뀜
HTML 문서no-cache저장은 가능하지만 사용 전 재검증
사용자별 APIprivate, no-cache브라우저 캐시는 가능, 공유 캐시는 제한
민감 정보no-store저장 자체를 피해야 함
CDN 전용 TTLs-maxage=300, stale-while-revalidate=30공유 캐시에 별도 TTL과 stale 정책 제공

no-cache는 “캐시하지 말라”가 아니라 “재사용 전에 원본 서버에 검증하라”에 가깝습니다. 이때 ETagLast-Modified 같은 검증자가 없으면 보통 304 대신 200 전체 응답을 다시 받게 됩니다. HTTP 캐시에 저장하지 않아야 하는 응답에는 no-store를 써야 합니다.


Fresh Cache와 Conditional Request

캐시된 응답이 아직 fresh라면 브라우저나 CDN은 원본 서버에 가지 않고 응답을 재사용할 수 있습니다. stale 상태가 되면 검증자가 있을 때 조건부 요청을 보내고, 서버는 내용이 바뀌지 않았으면 304 Not Modified로 본문 없이 응답할 수 있습니다.

검증자요청 헤더서버 판단 기준특징
ETagIf-None-Match리소스 버전 식별자 비교strong/weak validator가 있고 우선순위가 높음
Last-ModifiedIf-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를 허용할지 명확히 응답해야 해결됩니다.

상황확인할 것
개발 환경 CORSdev server proxy로 같은 출처처럼 중계할 수 있음
운영 환경 CORS서버가 정확한 Access-Control-Allow-Origin 반환
쿠키 포함 요청Allow-Credentials: true와 특정 Origin 필요
와일드카드 Origincredentials 요청에는 * 사용 불가
커스텀 응답 헤더 읽기Access-Control-Expose-Headers에 이름 추가
preflight 반복Access-Control-Max-Age를 쓰되 브라우저 한계 고려
Origin 반사허용 목록 검증과 Vary: Origin 함께 고려
cors_server.py
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를 살펴보겠습니다.