운영 콘솔 아키텍처 시리즈 8/9
관리자 페이지를 만들다 보면 CORS 에러를 자주 만난다. 로컬에서는 되는데 배포하면 안 되고, GET은 되는데 POST는 안 되고, 쿠키를 보내면 갑자기 막힌다.
CORS는 브라우저가 까다롭게 구는 문제가 아니다. 브라우저 보안 모델이다. 특히 React SPA처럼 정적 파일 도메인과 API 도메인이 분리된 구조에서는 CORS와 쿠키 인증을 처음부터 같이 설계해야 한다.
문제 상황
운영 콘솔은 console.example.com에서 서빙된다. API는 api.example.com에 있다. 두 주소는 host가 다르므로 서로 다른 origin이다.
브라우저의 origin은 세 가지로 결정된다.
scheme + host + port
따라서 다음 두 주소는 다른 origin이다.
https://console.example.com
https://api.example.com
SPA가 API를 호출하려면 API 서버가 "이 origin의 요청을 허용한다"는 CORS 헤더를 응답해야 한다.
선택지
첫 번째 방식은 API와 SPA를 같은 origin으로 맞추는 것이다. 예를 들어 CloudFront나 reverse proxy에서 /api를 같은 도메인 아래로 붙인다. CORS 문제는 줄어들지만 프록시 설정이 필요하다.
두 번째 방식은 origin을 분리하고 CORS를 정확히 설정하는 것이다. API 도메인을 독립적으로 운영할 수 있지만, preflight와 credentials 처리를 이해해야 한다.
운영 콘솔에서는 두 번째 방식을 사용했다. API 경계를 명확히 유지하는 대신 CORS 설정을 표준화했다.
CORS 기본 구조
단순한 GET 요청은 바로 API로 갈 수 있다. 하지만 다음 조건이 있으면 브라우저는 본 요청 전에 OPTIONS preflight를 보낸다.
Content-Type: application/jsonAuthorization헤더PUT,DELETE,PATCH같은 메서드- 커스텀 헤더
관리자 페이지 API는 대부분 JSON을 쓰기 때문에 preflight가 자주 발생한다.

API는 preflight에 다음과 같이 응답해야 한다.
Access-Control-Allow-Origin: https://console.example.com
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Max-Age: 86400
API Gateway를 쓴다면 OPTIONS 요청은 MOCK Integration으로 처리할 수 있다. Lambda나 애플리케이션 서버까지 보내지 않아도 된다.
쿠키 인증이 들어오면 달라지는 점
쿠키를 cross-origin API에 보내려면 조건이 더 엄격하다.
프론트엔드는 credentials를 포함해야 한다.
fetch("https://api.example.com/api/me", {
credentials: "include"
});
Axios라면 다음처럼 설정한다.
axios.create({
baseURL: "https://api.example.com",
withCredentials: true
});
서버는 다음 헤더를 응답해야 한다.
Access-Control-Allow-Origin: https://console.example.com
Access-Control-Allow-Credentials: true
여기서 중요한 점은 Access-Control-Allow-Origin: *를 쓸 수 없다는 것이다. credentials를 허용하는 응답에서는 wildcard origin이 허용되지 않는다. 반드시 구체적인 origin을 반환해야 한다.
쿠키 자체도 cross-site 전송 조건을 만족해야 한다.
Set-Cookie: internal-auth-token=...; Secure; HttpOnly; SameSite=None
실제 속성은 조직의 인증 정책에 따라 달라지지만, cross-origin 쿠키 인증에서는 SameSite, Secure, 도메인 범위를 함께 봐야 한다.
얻은 것
CORS를 에러 대응이 아니라 설계 요소로 다루면 문제가 줄어든다.
API Gateway에서 OPTIONS를 표준화하고, 애플리케이션 응답에서도 CORS 헤더를 일관되게 붙이면 path별 누락을 줄일 수 있다.
쿠키 인증이 필요한 API와 필요 없는 API를 구분하는 것도 중요하다. 공개 API는 credentials 없이 호출되도록 두고, 내부 API만 명시적으로 credentials를 요구하는 편이 안전하다.
자주 하는 실수
첫 번째 실수는 Access-Control-Allow-Origin: *와 Access-Control-Allow-Credentials: true를 같이 쓰는 것이다. 브라우저가 막는다.
두 번째 실수는 POST 응답에는 CORS 헤더를 넣었지만 OPTIONS 응답에는 넣지 않는 것이다. 이 경우 본 요청이 가기 전에 preflight에서 실패한다.
세 번째 실수는 성공 응답에만 CORS 헤더를 넣는 것이다. API가 500을 반환할 때 CORS 헤더가 빠지면 브라우저 콘솔에는 실제 서버 에러 대신 CORS 에러처럼 보일 수 있다.
네 번째 실수는 로컬 개발 환경과 운영 환경의 origin 목록을 다르게 관리하지 않는 것이다. localhost에서는 되는데 배포 후 안 되는 문제의 흔한 원인이다.
다시 설계한다면
CORS 허용 origin을 코드에 흩뿌리지 않고 환경 설정으로 관리하겠다. 운영, 스테이징, 로컬 개발 origin을 명시적으로 나누고, 허용되지 않은 origin은 거절한다.
또한 CORS 테스트를 배포 검증에 포함하겠다. OPTIONS 요청, credentials 요청, 실패 응답의 CORS 헤더까지 자동으로 확인하면 운영 중 장애를 줄일 수 있다.
정리
CORS는 브라우저 보안 모델이다. 우회해야 할 귀찮은 에러가 아니라, SPA와 API가 서로 다른 origin에서 동작할 때 반드시 지켜야 하는 계약이다.
관리자 페이지에서는 CORS와 쿠키 인증이 자주 함께 등장한다. OPTIONS preflight, credentials, concrete origin, cookie attributes를 함께 설계해야 한다. 이 부분을 초기에 정리해두면 배포 후 디버깅 시간을 크게 줄일 수 있다.
함께 볼 GitHub 저장소
'성장과 기술 > 시스템 설계' 카테고리의 다른 글
| API timeout을 피하는 잡 큐와 비동기 워커 패턴 (1) | 2026.07.12 |
|---|---|
| 서버리스만으로 부족했던 운영 콘솔 백엔드 설계 (0) | 2026.07.11 |
| Lambda Function URL 대신 API Gateway를 선택한 이유 (0) | 2026.07.10 |
| 데이터베이스를 쓰지 않기로 한 아키텍처 결정 (0) | 2026.07.09 |
| 관리자 페이지 배포에 서버가 꼭 필요할까? (0) | 2026.07.08 |
글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.