본문 바로가기
Q&A해결2026년 7월 25일2분 읽기

브라우저에서 S3로 직접 presigned URL 업로드하는데 CORS preflight에서 막혀요

s
s3_dahye
조회 744댓글 2

프론트(리액트)에서 파일을 서버 안 거치고 S3로 바로 올리려고 백엔드에서 presigned PUT URL 받아서 fetch로 PUT 하는 구조입니다. 로컬에서 curl로 그 URL에 PUT 하면 잘 올라가는데, 브라우저에서 하면 OPTIONS 요청부터 막히고 콘솔에 CORS 에러가 뜹니다.

Access to fetch at 'https://my-bucket.s3.ap-northeast-2.amazonaws.com/...'
from origin 'https://app.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

버킷 정책이랑 IAM은 서버가 URL 잘 만들어주는 거 보면 문제 없는 것 같은데, 브라우저에서만 이러니 어디를 봐야 할지 모르겠어요.

댓글 2

김김영삼3개월 전
CORS는 IAM이나 버킷 정책이랑 완전히 별개 설정이에요. 그 둘은 '누가 이 객체에 접근할 권리가 있냐'를 보는 거고, CORS는 '브라우저가 이 origin에서 온 요청에 응답을 읽게 허용하냐'를 보는 거라서요. curl은 CORS 검사를 안 하니까 잘 되고, 브라우저만 preflight(OPTIONS)를 먼저 쏘는데 버킷에 CORS 규칙이 없으면 그 OPTIONS에 Access-Control-Allow-Origin 헤더가 안 붙어서 막히는 겁니다.

버킷 > 권한 > CORS 에 이런 규칙 넣으세요.

[{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}]

AllowedHeaders에 *를 넣어야 브라우저가 보내는 Content-Type이나 커스텀 헤더가 preflight를 통과해요. 그리고 하나 함정이 있는데, presigned URL 만들 때 Content-Type을 서명에 포함했으면 브라우저 PUT의 Content-Type이 그거랑 정확히 일치해야 합니다. 안 그러면 CORS 통과해도 403 SignatureDoesNotMatch 나요. ExposeHeaders에 ETag 넣은 건 나중에 멀티파트 업로드 하면 ETag를 읽어야 해서 미리 넣어둔 거예요.
ss3_dahye(guest)3개월 전
CORS 규칙 넣으니 바로 올라가네요. Content-Type 일치 얘기까지 딱 걸릴 뻔한 거 미리 알려주셔서 감사합니다 🙏
Ctrl+Enter로 등록