2026년 08월 21일

AWS S3 + CloudFront CORS 에러, 헤더 설정으로 해결하는 트러블슈팅 가이드

CloudFront 배포까지 마쳤는데 브라우저 콘솔에 Access to fetch at '...' from origin '...' has been blocked by CORS policy 메시지가 뜨면 당혹스러움에 멘붕이 온 순간이 있다. S3 단독으로 테스트할 때는 멀쩡하던 요청이 CloudFront를 앞단에 붙이고 나서야 에러가 터지는 경우가 많기 때문이다. 이 글에서는 실제로 이 상황을 맞닥뜨렸을 때 원인을 진단하고 헤더를 올바르게 설정해 해결한 과정을 단계별로 정리한다.

CORS 에러가 발생하는 구조적 이유

CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 출처(origin)의 리소스를 요청할 때 서버 측에서 허용 여부를 명시적으로 선언하도록 강제하는 보안 메커니즘이다. 예를 들어 https://example.com에서 실행되는 자바스크립트가 https://cdn.example.com 또는 S3 엔드포인트에 fetch 요청을 보내면, 브라우저는 먼저 Origin 헤더를 포함한 요청을 보내고 응답에 Access-Control-Allow-Origin 헤더가 있는지 확인한다.

S3와 CloudFront를 조합할 때 CORS 문제가 복잡해지는 이유는 두 서비스가 각각 독립적으로 응답 헤더를 처리하기 때문이다. S3에 CORS 규칙을 설정해 두더라도 CloudFront가 그 응답을 캐싱하면서 Access-Control-Allow-Origin 헤더를 누락하거나, 캐시된 헤더가 다른 origin의 요청에 그대로 반환되는 상황이 생긴다.

가장 흔히 겪는 패턴은 다음과 같다.

  • S3에 CORS 설정 자체가 없는 경우
  • S3에 CORS는 설정했지만 CloudFront가 Origin 헤더를 S3로 전달하지 않아 CORS 응답이 생성되지 않는 경우
  • CloudFront가 CORS 관련 헤더를 캐싱하지 않아 첫 번째 요청의 응답 헤더가 이후 다른 origin 요청에도 재사용되는 경우

S3 버킷 CORS 설정

S3 콘솔에서 해당 버킷으로 이동한 뒤 권한(Permissions) 탭 하단의 CORS(Cross-origin 리소스 공유) 섹션을 찾는다. 여기에 JSON 형식으로 규칙을 입력해야 한다.

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedOrigins": ["https://example.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

AllowedOrigins에는 실제로 요청을 보내는 프론트엔드 도메인을 명시한다. 개발 중이거나 다수의 도메인을 허용해야 한다면 "*"를 사용할 수 있지만, 프로덕션 환경에서는 가능한 한 구체적인 도메인을 지정하는 것이 보안상 바람직하다.

AllowedMethods는 실제로 사용하는 HTTP 메서드만 포함한다. 정적 파일을 읽기만 한다면 GET과 HEAD로 충분하다. 업로드 기능이 있다면 PUT이나 POST를 추가한다.

S3 CORS 설정을 저장한 뒤 S3 엔드포인트로 직접 curl 요청을 보내 응답 헤더를 확인해 볼 수 있다.

curl -I -H "Origin: https://example.com" \
  https://your-bucket.s3.ap-northeast-2.amazonaws.com/sample.jpg

응답에 Access-Control-Allow-Origin: https://example.com이 포함되어 있다면 S3 측 설정은 정상이다.

CloudFront에서 Origin 헤더 전달 설정

S3 CORS 설정이 제대로 동작하려면 CloudFront가 클라이언트 요청의 Origin 헤더를 S3로 그대로 전달해야 한다. 그렇지 않으면 S3 입장에서는 CORS 요청인지 판단할 수 없어 CORS 응답 헤더를 반환하지 않는다.

CloudFront 콘솔에서 해당 배포를 선택하고 동작(Behaviors) 탭으로 이동한다. 편집할 동작을 선택한 뒤 캐시 키 및 원본 요청(Cache key and origin requests) 섹션에서 **캐시 정책(Cache policy)**과 **원본 요청 정책(Origin request policy)**을 확인한다.

캐시 정책 설정

AWS에서 제공하는 관리형 캐시 정책 중 CachingOptimized는 기본적으로 Origin 헤더를 캐시 키에 포함하지 않는다. CORS를 올바르게 처리하려면 Origin 헤더를 캐시 키에 포함시켜야 요청 출처별로 다른 응답을 캐싱할 수 있다.

CachingOptimizedForUncompressedObjects 정책을 사용하거나, 커스텀 캐시 정책을 만들어 헤더 항목에 Origin을 추가한다.

원본 요청 정책 설정

원본 요청 정책은 CloudFront가 S3(원본)로 요청을 보낼 때 어떤 헤더를 포함할지 결정한다. Origin 헤더를 S3로 전달하려면 원본 요청 정책에 Origin을 포함해야 한다.

AWS 관리형 정책 중 CORS-S3Origin을 사용하면 Origin, Access-Control-Request-Headers, Access-Control-Request-Method 헤더가 S3로 전달된다. 이 정책을 선택하는 것이 가장 간단한 방법이다.

설정을 저장하고 배포 변경이 완료되기까지 보통 수 분에서 수십 분이 걸릴 수 있다.

CloudFront 응답 헤더 정책으로 CORS 헤더 직접 추가

앞선 방식이 S3의 CORS 응답을 CloudFront가 그대로 전달하는 구조라면, 응답 헤더 정책(Response Headers Policy)을 사용하면 CloudFront 레벨에서 직접 Access-Control-Allow-Origin 같은 헤더를 응답에 추가할 수 있다. 이 방법은 S3 CORS 설정과 무관하게 동작하기 때문에, 원본 설정을 변경하기 어려운 상황이나 헤더를 일관되게 제어하고 싶을 때 유용하다.

CloudFront 콘솔 좌측 메뉴에서 정책(Policies) → **응답 헤더(Response headers)**로 이동해 새 정책을 생성한다.

CORS 구성 섹션에서 다음 항목을 설정한다.

  • Access-Control-Allow-Origin: 허용할 도메인 또는 *
  • Access-Control-Allow-Headers: * 또는 필요한 헤더 목록
  • Access-Control-Allow-Methods: GET, HEAD 등 허용할 메서드
  • Access-Control-Max-Age: Preflight 요청 캐싱 시간(초)
  • Access-Control-Allow-Credentials: 쿠키나 인증 정보를 포함하는 요청이라면 true, 그렇지 않으면 설정하지 않음

정책을 저장한 뒤 CloudFront 배포의 동작 편집 화면으로 돌아가 응답 헤더 정책 항목에 방금 만든 정책을 연결한다.

주의할 점은, Access-Control-Allow-Credentials를 true로 설정한 경우 Access-Control-Allow-Origin에 *를 사용할 수 없다. 이때는 요청 Origin 헤더 값을 동적으로 반영하는 것이 필요하므로, 응답 헤더 정책 단독으로는 해결하기 어렵고 앞서 설명한 캐시 키 및 원본 요청 정책 방식과 병행해야 한다.

캐시 무효화(Invalidation) 처리

설정을 변경했는데도 에러가 계속된다면 기존에 캐싱된 응답이 남아 있을 가능성이 높다. CloudFront는 TTL이 만료되기 전까지 이전 응답을 그대로 반환하기 때문이다.

CloudFront 콘솔에서 해당 배포를 선택하고 무효화(Invalidations) 탭으로 이동한다. 무효화 생성을 클릭하고 경로에 /*를 입력하면 전체 캐시를 초기화할 수 있다. 특정 파일이나 경로만 초기화하려면 /images/* 등 구체적인 경로를 입력한다.

AWS CLI를 사용하는 경우 다음 명령으로 처리할 수 있다.

aws cloudfront create-invalidation \
  --distribution-id EXXXXXXXXXXXXXX \
  --paths "/*"

무효화가 완료된 이후 브라우저 캐시도 함께 지워야 정확한 테스트가 가능하다. 브라우저의 개발자 도구 네트워크 탭에서 캐시 비활성화(Disable cache) 옵션을 체크하고 요청을 재시도하는 것이 편하다.

Preflight 요청(OPTIONS) 대응

GET이나 HEAD가 아닌 POST, PUT, DELETE 요청이나 커스텀 헤더가 포함된 요청은 브라우저가 본 요청 전에 OPTIONS 메서드로 Preflight 요청을 먼저 보낸다. S3는 기본적으로 OPTIONS 메서드를 지원하지만, CloudFront 동작 설정에서 OPTIONS 메서드가 허용되어 있어야 한다.

CloudFront 동작 편집 화면의 허용된 HTTP 메서드 항목에서 GET, HEAD, OPTIONS를 선택하거나, 필요에 따라 모든 메서드를 허용하는 옵션을 선택한다. OPTIONS가 차단되면 Preflight 요청 자체가 실패해 본 요청이 진행되지 않는다.

Preflight 요청에 대한 S3 응답에는 Access-Control-Allow-Methods와 Access-Control-Allow-Headers가 포함되어야 한다. 응답 헤더 정책을 사용하는 경우 해당 항목이 정책에 포함되어 있는지 재확인한다.

설정 이후 동작 검증 방법

변경 사항이 적용됐는지 확인할 때는 curl로 직접 헤더를 확인하는 방법이 가장 빠르다.

curl -I -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: GET" \
  -X OPTIONS \
  https://your-cloudfront-domain.cloudfront.net/sample.jpg

응답에 다음 헤더들이 포함되어 있어야 정상이다.

  • Access-Control-Allow-Origin: https://example.com (또는 *)
  • Access-Control-Allow-Methods: GET, HEAD (설정에 따라 다름)
  • Vary: Origin — 이 헤더는 CloudFront가 Origin별로 다른 응답을 캐싱하고 있음을 나타낸다

Vary: Origin이 응답에 없다면 캐시 키 설정이 제대로 적용되지 않았을 가능성이 있다. 이 경우 캐시 정책에서 Origin 헤더가 포함되어 있는지 다시 확인한다.

브라우저에서 직접 테스트할 때는 개발자 도구 콘솔에서 간단한 fetch 코드를 실행해 응답 상태와 헤더를 확인할 수 있다.

fetch('https://your-cloudfront-domain.cloudfront.net/sample.jpg', {
  method: 'GET',
  mode: 'cors'
})
.then(res => console.log([...res.headers.entries()]))
.catch(err => console.error(err));

설정 체크리스트

지금까지의 내용을 기준으로 CORS 설정이 누락되거나 잘못 적용된 부분이 없는지 점검할 수 있는 항목을 정리한다.

  • S3 버킷 CORS 설정에 올바른 AllowedOrigins와 AllowedMethods가 포함되어 있는가
  • CloudFront 동작의 원본 요청 정책에 Origin 헤더가 포함되어 있는가 (또는 CORS-S3Origin 정책 사용)
  • CloudFront 캐시 정책에 Origin 헤더가 캐시 키로 포함되어 있는가
  • CloudFront 동작에서 OPTIONS 메서드가 허용되어 있는가
  • 응답 헤더 정책을 사용하는 경우 CORS 관련 헤더가 모두 설정되어 있는가
  • 설정 변경 후 CloudFront 캐시 무효화를 실행했는가
  • 브라우저 캐시를 비운 뒤 재테스트했는가

이 항목들을 하나씩 확인하면 대부분의 CORS 문제는 원인을 특정할 수 있다. S3와 CloudFront 조합에서 발생하는 CORS 에러는 설정 한 곳만의 문제가 아니라 두 서비스 간의 헤더 전달 흐름 전체를 이해해야 해결이 가능하기 때문에, 처음 접하면 당황스럽지만 원리를 파악하면 비교적 명확하게 대응할 수 있다.