AWS 실전 운영 시리즈 5/12
브라우저에서 API를 호출할 때 CORS 오류를 만나면 당황스럽다.
서버는 멀쩡한 것 같은데 브라우저가 요청을 막는다. Postman에서는 되는데 화면에서는 안 된다.
운영 콘솔처럼 React SPA와 API 도메인이 분리된 구조에서는 CORS가 설계 요소다.
특히 JSON 요청이 많다면 preflight OPTIONS 요청을 어떻게 처리할지 정해야 한다.
CORS preflight란 무엇인가
브라우저는 다른 origin으로 특정 요청을 보내기 전에 OPTIONS 요청을 먼저 보낼 수 있다.
이 요청을 preflight라고 한다.
예를 들어 다음 조건에서는 preflight가 발생한다.
Content-Type: application/jsonAuthorization헤더 사용PUT,DELETE,PATCH같은 method 사용- credentials 포함 요청
운영 콘솔의 API는 대부분 JSON을 주고받는다.
따라서 preflight는 예외가 아니라 일반적인 흐름이다.
Lambda까지 보낼 필요가 없는 요청
OPTIONS 요청의 응답은 대체로 정적이다.
서버는 이런 정보를 알려주면 된다.
- 허용 origin
- 허용 method
- 허용 header
- credentials 허용 여부
- preflight 캐시 시간
이 응답을 만들기 위해 Lambda를 실행할 필요는 없다.
API Gateway REST API의 MOCK Integration을 쓰면 Gateway가 직접 응답할 수 있다.

OPTIONS는 Lambda를 깨우지 않는다.
본 요청만 Lambda로 간다.
path가 많아지면 이 구조가 모든 resource에 같은 방식으로 적용되어야 한다.

이 그림의 핵심은 CORS를 Lambda 코드의 예외 처리로 보지 않는 것이다. API Gateway 설정의 표준화 대상으로 봐야 누락을 줄일 수 있다.
MOCK Integration의 장점
MOCK Integration을 쓰면 몇 가지 장점이 있다.
첫째, Lambda 비용과 지연이 줄어든다.
preflight마다 Lambda를 실행하지 않아도 된다.
둘째, Lambda 코드가 단순해진다.
모든 handler에서 OPTIONS 분기를 처리할 필요가 줄어든다.
셋째, CORS 정책이 Gateway 계층에 모인다.
브라우저와 API 사이의 정책을 API Gateway에서 관리할 수 있다.
넷째, path별로 정책을 다르게 둘 수 있다.
운영자 API와 공개 API의 허용 method나 header가 다르다면 분리할 수 있다.
설정 누락이 자주 생긴다
문제는 path가 많아질수록 OPTIONS 설정 누락이 생기기 쉽다는 것이다.
예를 들어 /api/dashboard에는 OPTIONS가 있지만 /api/reports에는 없을 수 있다.
그러면 어떤 API는 되고 어떤 API는 브라우저에서만 실패한다.
이런 상황은 디버깅이 번거롭다.
서버 코드가 아니라 API Gateway 설정 문제이기 때문이다.
모든 path에 동일한 기본 설정을 자동화하기
CORS 설정은 수동으로 하나씩 하면 실수하기 쉽다.
따라서 스크립트로 표준화하는 것이 좋다.
해야 할 작업은 대략 다음과 같다.
- 모든 resource 목록 확인
- OPTIONS method가 있는지 확인
- 없으면 OPTIONS method 추가
- MOCK Integration 연결
- Method Response에 CORS header 선언
- Integration Response에 CORS header 값 설정
- API Gateway stage 배포
핵심은 "모든 path에 같은 기본 CORS 응답이 있어야 한다"는 것이다.
Lambda 응답에도 fallback 헤더를 둔다
OPTIONS는 Gateway에서 처리하더라도 실제 API 응답에는 Lambda가 CORS 헤더를 포함해야 한다.
브라우저는 본 요청 응답에서도 CORS 헤더를 확인하기 때문이다.
따라서 Lambda 응답에도 다음과 같은 헤더가 필요하다.
def cors_headers():
return {
"Access-Control-Allow-Origin": "https://console.example.com",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
}
credentials를 쓰는 경우에는 Access-Control-Allow-Origin: *를 쓰면 안 된다.
구체적인 origin을 반환해야 한다.
테스트 방법
CORS는 브라우저에서 확인하는 것이 가장 정확하다.
하지만 preflight 응답 자체는 curl로도 확인할 수 있다.
curl -i -X OPTIONS https://api.example.com/api/reports \
-H "Origin: https://console.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
확인할 것은 다음이다.
- status code가 200 계열인가
Access-Control-Allow-Origin이 맞는가Access-Control-Allow-Methods에 본 요청 method가 있는가Access-Control-Allow-Headers에 필요한 header가 있는가- credentials 요청이면 allow credentials가 있는가
CORS 문서화가 필요한 이유
CORS 설정은 한 번 맞춰도 나중에 새 API를 추가하면서 다시 깨질 수 있다.
그래서 문서화가 필요하다.
운영 문서에는 다음을 남긴다.
- 어떤 origin을 허용하는가
- credentials를 쓰는가
- OPTIONS는 Gateway MOCK으로 처리하는가
- Lambda 응답에도 CORS 헤더가 있는가
- 새 path를 추가할 때 CORS 설정도 추가해야 하는가
- 테스트 명령어는 무엇인가
이 문서가 없으면 CORS는 매번 새로운 문제처럼 보인다.
정리
CORS preflight는 브라우저가 자동으로 보내는 실제 요청이다.
운영 콘솔처럼 SPA와 API 도메인이 분리되어 있고 JSON 요청이 많다면 preflight는 자주 발생한다.
OPTIONS 응답은 대부분 정적이므로 Lambda까지 보낼 필요가 없다.
API Gateway MOCK Integration으로 처리하면 비용과 지연을 줄이고, CORS 정책을 Gateway 계층에 둘 수 있다.
다만 path별 설정 누락이 생기기 쉬우므로 자동화와 문서화가 필요하다.
브라우저에서 CORS가 터졌다면 서버 코드만 보지 말고, Gateway OPTIONS 설정부터 확인해야 한다.
함께 볼 GitHub 저장소
'성장과 기술 > 시스템 설계' 카테고리의 다른 글
| API Gateway REST API와 HTTP API 선택 기준 (0) | 2026.06.19 |
|---|---|
| Lambda 배포 파일을 가볍게 만든 이유 (0) | 2026.06.18 |
| CloudFront 캐시는 성능 기능이 아니라 운영 기능이다 (0) | 2026.06.17 |
| React SPA를 S3와 CloudFront에 올릴 때 먼저 정해야 할 것들 (0) | 2026.06.16 |
| 리포트 시스템을 다시 설계한다면 볼 것들 (0) | 2026.06.07 |
글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.