59장. CloudFront + S3로 정적 콘텐츠 서비스하기
이 장에서 말하고자 하는 것
CloudFront와 S3를 묶어
정적 자원 서비스의 표준 구성 을 만든다.
- SPA (React/Vue/Next) 빌드 결과
- 이미지 · CSS · JS
- 다운로드 파일
- 공개 미디어
부품은 38·39·56장에서 다 봤다.
1. 표준 구성도
[사용자]
↓ HTTPS, DNS (Route 53 ALIAS)
[CloudFront] (us-east-1 ACM, WAF)
↓ OAC
[S3 버킷] (비공개)
- 사용자 응답 빠름 (엣지 캐시)
- S3 노출 없음 (OAC)
- HTTPS 자동 (CloudFront + ACM)
2. SPA 호스팅
SPA의 특징:
/만 진짜 파일 —index.html/login,/orders/123같은 경로도 같은index.html을 받아야 함 (브라우저 라우터)
CloudFront에서 한 줄로 해결한다.
오류 응답 매핑:
S3에서 404 NoSuchKey → CloudFront가 / 의 index.html 200 응답
3. 캐시 무효화 없이 새 버전 배포
invalidation에 의존하지 않는다. 대신 파일명 해시 패턴.
빌드 결과:
index.html
static/app.a3f2c1.js
static/style.b91e8.css
index.html은 캐시 짧게 (1분)static/*.js,*.css는 캐시 길게 (1년) — 새 빌드는 새 파일명
4. 도메인 한 개에 정적 + API
example.com
├─ /api/* → API Gateway → ALB → ECS
└─ /* → S3 (SPA)
CloudFront 한 배포 안에서 두 origin을 가진다.
- CORS 문제 없음
- 사용자가 보기에 한 도메인
5. 우리 서비스에서
[Route 53] example.com → CloudFront
[CloudFront]
├─ default → S3 "static"
│ - Cache 1년
│ - 404 → / 의 index.html (SPA)
├─ /api/* → API Gateway → ALB → ECS
└─ /img/* → S3 "img"
- Cache 7일
6. 배포 흐름 — CI/CD에서
# 1. 빌드
npm run build
# 2. 정적 자원 (긴 캐시)
aws s3 sync ./dist s3://msa-static/ \
--cache-control "public,max-age=31536000,immutable" \
--exclude index.html
# 3. index.html (짧은 캐시)
aws s3 cp ./dist/index.html s3://msa-static/index.html \
--cache-control "public,max-age=60"
# 4. (필요 시) invalidation
aws cloudfront create-invalidation \
--distribution-id E123ABCD \
--paths "/index.html"
invalidation은 index.html 한 줄로 끝. 정적 자원은 파일명이 바뀌어 자동 새 캐시.
7. 직접 확인해보기 — CLI
curl -I https://example.com/static/app.a3f2c1.js
# Cache-Control: public,max-age=31536000,immutable
# x-cache: Hit from cloudfront
curl -I https://example.com/
# Cache-Control: public,max-age=60
8. 코드로는 이렇게 생겼다 — Terraform (요약)
resource "aws_s3_bucket" "static" {
bucket = "msa-prod-static"
}
resource "aws_cloudfront_origin_access_control" "s3" {
name = "s3-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "main" {
enabled = true
aliases = ["example.com"]
origin {
domain_name = aws_s3_bucket.static.bucket_regional_domain_name
origin_id = "s3"
origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
}
default_cache_behavior {
target_origin_id = "s3"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized
}
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.us_east.arn
ssl_support_method = "sni-only"
}
restrictions {
geo_restriction { restriction_type = "none" }
}
}
이게 정적 SPA 호스팅의 최소 운영 구성이다.
9. 이렇게 쓰면 망한다 — 안티패턴
안티패턴 1. S3 정적 호스팅 엔드포인트를 그대로 쓴다
HTTPS · DDoS · 캐시 다 약하다.
항상 CloudFront 앞에
안티패턴 2. SPA 404 매핑을 안 한다
/orders/123 으로 직접 들어오면 흰 화면.
안티패턴 3. 모든 파일을 같은 캐시로 둔다
index.html 도 1년 캐시 → 새 배포가 안 보임.
index.html은 짧게, 해시 파일은 길게
안티패턴 4. 새 빌드마다 /* 전체 invalidation
비용 + 지연. 파일명 해시로 대체.
10. 한 줄로 정리
CloudFront + S3 (OAC) + 파일명 해시 + SPA 404 매핑이
정적 자원 서비스의 사실상 표준이다
11. 이 장의 핵심 정리
- CloudFront + S3 (OAC) 가 정적 콘텐츠의 표준 모양이다.
- SPA는 404 → index.html 200 매핑이 필요하다.
- 파일명 해시 + 차등 캐시로 invalidation 부담을 없앤다.
- 정적 + API를 한 도메인에 두면 CORS 문제도 사라진다.
- S3 정적 웹사이트 엔드포인트는 사실상 쓸 일이 없다.