9장 : HTTP
캐시와 CORS
HTTP의 마지막 주제로, 성능과 보안의 핵심인 캐시(Cache)와 CORS(Cross-Origin Resource Sharing)를 다루겠습니다. 둘 다 실무에서 왜 안 되지?라는 상황을 자주 만들어내는 주제입니다.
HTTP 캐시 전략
HTTP 캐시는 이전에 가져온 리소스를 저장해두고 재사용함으로써, 불필요한 네트워크 요청을 줄이는 메커니즘입니다.
매 페이지 방문마다
style.css (50KB) → 다운로드
app.js (200KB) → 다운로드
logo.png (30KB) → 다운로드
font.woff2 (100KB) → 다운로드
총: 380KB × 매 요청
캐시 활용
첫 방문: 380KB 다운로드 + 캐시 저장
재방문: 0KB! (캐시에서 로드)
→ 로딩 속도 극적 개선Cache-Control 지시자
Cache-Control 헤더가 캐시 정책의 핵심입니다.
저장 관련
public → CDN, 프록시 등 공유 캐시에 저장 가능
private → 브라우저에만 저장 (기본값)
no-store → 캐시에 아예 저장 금지 (은행 거래, 개인정보)
유효성 관련
max-age=3600 → 3600초(1시간) 동안 캐시 유효
s-maxage=3600 → CDN용 max-age (CDN에만 적용)
no-cache → 캐시 저장하되, 매번 서버에 유효성 확인
(이름이 오해의 소지 → "항상 재검증" 의미)
must-revalidate → max-age 경과 후 반드시 서버 재검증
불변
immutable → 리소스 절대 안 변함, 새로고침해도 재검증 안 함| 시나리오 | Cache-Control 설정 | 이유 |
|---|---|---|
| 해시 파일명 JS/CSS | max-age=31536000, immutable | 파일명에 해시 → 변경 시 새 URL |
| API 응답 (공개) | max-age=60, public | 1분 캐시, CDN에서도 캐시 |
| 사용자별 API | max-age=0, private | CDN 캐시 X, 브라우저에만 |
| 은행 거래 내역 | no-store | 민감 정보, 절대 캐시 금지 |
| HTML 문서 | no-cache | 매번 최신 확인, 캐시 저장은 허용 |
강력한 캐시 vs 조건부 캐시
강력한 캐시 (Strong Cache)
Browser
┌──────────┐
│ Cache ✓ │
└──────────┘
→ max-age 내에는 서버 요청 안 보냄
→ 서버 부하 0, 속도 최대
적합: app.a1b2c3.js, style.d4e5f6.css
(파일명에 해시 → 변경 시 URL 자체가 바뀜)
조건부 캐시 (Conditional Cache)
Browser → Server
"If-None-Match: abc"
Server → Browser
"304 Not Modified" (본문 없음)
→ 서버 부하 ↓ (검증만), 대역폭 절약 (본문 생략)
적합: index.html, API 응답ETag (Entity Tag)
서버 응답: ETag: "abc123"
재요청: If-None-Match: "abc123"
서버 판단: 현재 ETag와 비교
같으면 → 304 (본문 생략)
다르면 → 200 + 새 데이터
Last-Modified
서버 응답: Last-Modified: Wed, 15 Nov 2023 10:00:00 GMT
재요청: If-Modified-Since: Wed, 15 Nov 2023 10:00:00 GMT
서버 판단: 마지막 수정 시각 비교
이후 변경 없으면 → 304
변경 있으면 → 200 + 새 데이터
ETag가 더 정확
1초 내 여러 번 변경 시 Last-Modified는 구분 못함
ETag는 내용 기반이라 정밀빌드 시
app.js → app.a1b2c3.js (내용 해시 포함)
style.css → style.d4e5f6.css
index.html
<script src="/app.a1b2c3.js"></script>
<link href="/style.d4e5f6.css">
캐시 설정
*.html → Cache-Control: no-cache (매번 최신 확인)
*.js|*.css → Cache-Control: max-age=31536000, immutable
(1년 캐시, 절대 변경 안 됨)
코드 수정 시
app.js 변경 → app.x7y8z9.js (새 해시 → 새 URL!)
index.html 업데이트 → 새 파일 참조
기존 a1b2c3 파일은 이미 캐시된 사용자들이 사용
새 HTML 받으면 자동으로 새 JS 다운로드CORS 동작 원리
브라우저에서 http://localhost:3000에서 실행 중인 프론트엔드가 http://api.example.com의 API를 호출하면, 브라우저 콘솔에 빨간 에러가 나타납니다.
출처(Origin) = 프로토콜 + 호스트 + 포트
http://localhost:3000 vs http://api.example.com
│ │ │ │ │
프로토콜 호스트 포트 프로토콜 호스트
같음 다름! 다름! → 다른 출처!
같은 출처 예시
http://example.com/page1
http://example.com/page2 ← 같은 출처 ✓ (경로만 다름)
다른 출처 예시
http://example.com vs https://example.com ← 프로토콜 다름
http://example.com vs http://api.example.com ← 호스트 다름
http://example.com vs http://example.com:8080 ← 포트 다름동일 출처 정책은 보안을 위한 것입니다. 악의적인 사이트가 사용자의 은행 사이트에 API 요청을 보내 정보를 탈취하는 것을 방지합니다. CORS는 이 제한을 안전하게 완화하는 메커니즘입니다.
Simple Request vs Preflight Request
조건: GET/HEAD/POST + 기본 헤더만 사용
Browser Server
│ │
│── GET /api/data ─────────────────────→│
│ Origin: http://localhost:3000 │
│ │
│←─ 200 OK ─────────────────────────────│
│ Access-Control-Allow-Origin: * │
│ {"data": "..."} │
│ │
Browser: "Allow-Origin에 내 출처가 포함?" → ✓ 허용조건: 커스텀 헤더(Authorization 등) 또는 PUT/DELETE 사용
Browser Server
│ │
│── OPTIONS /api/users ────────────────→│ ← 프리플라이트
│ Origin: http://localhost:3000 │
│ Access-Control-Request-Method: PUT │
│ Access-Control-Request-Headers: │
│ Authorization, Content-Type │
│ │
│←─ 204 No Content ─────────────────────│
│ Access-Control-Allow-Origin: │
│ http://localhost:3000 │
│ Access-Control-Allow-Methods: │
│ GET, POST, PUT, DELETE │
│ Access-Control-Allow-Headers: │
│ Authorization, Content-Type │
│ Access-Control-Max-Age: 86400 │ ← 24시간 캐시
│ │
│── PUT /api/users/123 ────────────────→│ ← 실제 요청
│ Origin: http://localhost:3000 │
│ Authorization: Bearer eyJ... │
│ │
│←─ 200 OK ─────────────────────────────│
│ Access-Control-Allow-Origin: │
│ http://localhost:3000 │CORS 에러 해결 패턴
CORS 에러를 해결하는 올바른 방법은 서버 측에서 CORS 헤더를 설정하는 것입니다.
필수:
Access-Control-Allow-Origin: http://localhost:3000
(또는 * → 단, 인증 사용 시 * 불가!)
선택:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true ← 쿠키 포함 시
Access-Control-Max-Age: 86400 ← 프리플라이트 캐시
Access-Control-Expose-Headers: X-Total-Count ← 커스텀 헤더 노출| 상황 | 해결 방법 |
|---|---|
| 개발 환경 CORS | 프론트엔드 devServer proxy 설정 |
| 운영 환경 CORS | 서버에서 Allow-Origin 설정 |
| 쿠키 포함 요청 | Allow-Credentials: true + 특정 Origin (not *) |
| 커스텀 헤더 읽기 | Expose-Headers에 헤더명 추가 |
| 프리플라이트 줄이기 | Max-Age 길게 (86400) 설정 |
중요: CORS는 브라우저의 보안 메커니즘!
Postman → Server: CORS 없음 → 성공!
curl → Server: CORS 없음 → 성공!
Server → Server: CORS 없음 → 성공!
Browser → Server: CORS 적용 → 헤더 없으면 차단!
"Postman에서는 되는데 브라우저에서 안 돼요"
→ CORS는 브라우저만 적용하기 때문
개발 중 우회
1. 프론트엔드 프록시 (Next.js rewrites, Vite proxy)
→ 같은 출처에서 요청하므로 CORS 미적용
2. 서버 CORS 설정 (권장)
3. 크롬 확장 프로그램 (비권장, 개발용만)from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class CORSHandler(BaseHTTPRequestHandler):
"""CORS 헤더를 설정하는 간단한 API 서버"""
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-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.send_header("Access-Control-Allow-Credentials", "true")
self.send_header("Access-Control-Max-Age", "86400")
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 성공!", "path": self.path}
self.wfile.write(json.dumps(data).encode())
# HTTPServer(("localhost", 8080), CORSHandler).serve_forever()다음 장에서는 HTTP 통신의 보안을 담당하는 HTTPS와 TLS를 살펴보겠습니다.