개요

브라우저에서 API 호출이 분명 성공한 것 같은데, 콘솔에는 CORS 에러가 뜨는 상황 자주 마주함 문제의 본질은 서버가 망가진 게 아니라 브라우저의 보안 정책에 의해 응답 사용이 차단된 것임 CORS는 차단을 풀기 위한 협상 절차이자 규칙 세트이며, 정확히 이해하면 재현과 해결이 쉬워짐

핵심 개념

  • Origin 정의

    • Origin = scheme + host + port 조합
    • 세 요소 중 하나라도 다르면 서로 다른 출처로 판단
  • SOP(Same-Origin Policy)

    • 동일 출처 간 상호작용 허용, 교차 출처는 기본 차단하는 브라우저 보안 정책
    • XSS, CSRF 등 교차 사이트 공격 리스크 완화 목적
    • 출처 비교와 차단 판단 주체는 서버가 아니라 브라우저
  • 예외적으로 허용되는 리소스

    • img, link, script 등 일부 태그는 역사적 사유로 cross-origin 허용
    • 단, type=module 스크립트는 CORS 요구됨
    • XHR, Fetch, 웹폰트 등은 SOP 적용 대상이라 CORS 규약을 통과해야 함

CORS 동작 원리

  • 브라우저가 요청 헤더에 Origin 전송
  • 서버가 응답 헤더에 Access-Control-Allow-Origin 등 허용 정보를 포함
  • 브라우저가 Origin과 허용 정보를 대조해 사용 여부 결정
  • 결론적으로 해결 지점은 서버 응답 헤더 구성이며, 클라이언트에서 Origin 위조는 불가
  • 브라우저를 거치지 않는 서버 간 통신에는 SOP/CORS 적용되지 않음

CORS 시나리오 1: 예비 요청 Preflight

  • 조건

    • 대부분의 교차 출처 요청은 본 요청 전에 OPTIONS 메소드로 예비 요청 수행
    • 요청 헤더에 Access-Control-Request-Method, Access-Control-Request-Headers 포함
  • 서버 응답 핵심 헤더

    • Access-Control-Allow-Origin
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Headers
    • Access-Control-Max-Age
  • 캐시 전략

    • Access-Control-Max-Age로 프리플라이트 결과 캐싱
    • 브라우저 벤더마다 상한 존재할 수 있음, Chromium 계열은 통상 수천 초 수준으로 제한
  • 디버깅 포인트

    • DevTools Network에서 OPTIONS 요청과 응답 헤더 확인
    • 일치하지 않거나 누락된 경우 브라우저가 본 요청 응답 사용 차단

CORS 시나리오 2: 단순 요청 Simple Request

  • 프리플라이트 없이 바로 본 요청 전송
  • 조건 충족 시에만 해당
    • 메소드가 GET, HEAD, POST 중 하나
    • CORS safelisted request-headers 범위 내 사용
    • Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나
  • 일반적인 JSON API는 조건을 위반하므로 대부분 프리플라이트 발생

CORS 시나리오 3: 인증된 요청 Credentialed Request

  • 쿠키, Authorization 헤더 등 자격 정보 포함 교차 출처 요청

  • 클라이언트 설정 예

    • fetch credentials:“include”
    • axios withCredentials:true
  • 서버 응답 요건

    • Access-Control-Allow-Credentials:true 필수
    • Access-Control-Allow-Origin에 * 사용 불가, 명시적 Origin 필요
    • Origin을 동적으로 반사하는 경우 Vary: Origin 권장
  • 교차 사이트 쿠키 전송 시 추가 고려

    • Set-Cookie에 SameSite=None; Secure 필요 (현대 브라우저 정책)
  • 위반 시 대표 에러

    • ACAO가 *이거나 ACAC 누락이면 브라우저가 응답 사용 차단

디버깅 체크리스트

  • DevTools Network 탭에서 요청별 Request/Response Headers 대조
  • OPTIONS 응답에 Allow-* 헤더 누락 여부 확인
  • 본 요청 응답의 Access-Control-Allow-Origin 값이 요청 Origin과 정합성 유지하는지 확인
  • 서버 간 호출로 재현 시 CORS가 보이지 않는 점 고려, 브라우저 환경에서 재현해야 의미 있음

해결 전략 총정리

  • 서버에서 허용 헤더 정확히 설정

    • 최소 세트 예시
      • Access-Control-Allow-Origin: https://allowed.example
      • Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
      • Access-Control-Allow-Headers: Content-Type, Authorization
      • Access-Control-Max-Age: 600
    • 인증 포함 시 Access-Control-Allow-Credentials:true 및 명시적 Origin 설정
  • 예비 요청 비용 최적화

    • Max-Age로 프리플라이트 캐시 활용
    • 실제 필요한 메소드/헤더만 허용해 노출 축소
  • 프록시 활용

    • 개발 환경에서 로컬 리버스 프록시 구성으로 동일 출처화
    • 공개 프록시는 남용/제한 많아 테스트 용도 권장, 실 서비스는 자체 프록시 또는 게이트웨이 사용
  • 브라우저 확장/정책 비활성화

    • 로컬 디버깅 한정의 임시 수단
    • 보안상 상용 환경 사용 비권장

서버별 최소 스니펫

  • Fetch로 인증 포함 요청 예
fetch('https://api.example.com/user', {
  method: 'GET',
  credentials: 'include'
})
  • Node.js http 예시
response.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
response.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// 인증 사용 시
// response.setHeader('Access-Control-Allow-Credentials', 'true')
  • Express 예시
const cors = require('cors')
app.use(cors({
  origin: 'https://app.example.com',
  credentials: true
}))
  • Spring WebMvcConfigurer 예시
registry.addMapping("/**")
  .allowedOrigins("https://app.example.com")
  .allowedMethods("GET","POST","PUT","DELETE")
  .allowCredentials(true)
  .maxAge(600)
  • Nginx 예시
location / {
  add_header Access-Control-Allow-Origin https://app.example.com;
  add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
  add_header Access-Control-Allow-Headers Content-Type,Authorization;
}
  • AWS S3 CORS 예시
[
  {
    "AllowedOrigins": ["https://app.example.com"],
    "AllowedMethods": ["GET","HEAD"],
    "AllowedHeaders": ["Authorization","Content-Type"],
    "ExposeHeaders": ["Content-Length"],
    "MaxAgeSeconds": 600
  }
]

주의와 베스트 프랙티스

    • 와일드카드 남용 금지, 특히 Credentials true와 동시 사용 불가
  • Origin 반사 전략 채택 시 Vary: Origin 추가로 캐시 오작동 방지
  • 허용 메소드/헤더 최소화와 짧은 수명 토큰 사용
  • CORS는 인증이나 권한 부여를 대체하지 않음, CSRF 토큰 등 별도 방어 필요
  • 에러가 서버 로그에 보이지 않는 경우가 많음, 브라우저에서 재현하고 헤더를 일치시키는 데 집중

참고 실습/도구

마무리

CORS는 SOP 위반을 예외적으로 허용하기 위한 계약 절차라는 관점에서 보면 단순함 브라우저가 Origin을 제시하고 서버가 허용 범위를 명시하며 브라우저가 검증하는 삼자 합의 구조임 헤더를 정확히 설정하고 프리플라이트와 인증 조건을 충족시키면 대부분의 이슈는 재현과 해결이 가능함 개발 환경에선 프록시나 확장을 활용해 디버깅 속도를 높이고, 운영 환경에선 최소 권한 원칙과 캐시 전략으로 안정성과 성능 균형을 맞추는 것이 핵심

참고자료