development

Access-Control-Allow-Origin 설정 최적화: CORS 문제 해결을 위한 실전 사례

CORS 에러에 당황하지 마세요! Access-Control-Allow-Origin 설정을 최적화하여 안전하고 효율적으로 웹 애플리케이션을 보호하는 방법을 알아보세요.

2025년 10월 04일
CORS Access-Control-Allow-Origin 웹 보안 개발 팁 웹 성능 최적화 cross-origin 웹 설정 보안 사례
5분 읽기

CORS를 정확히 이해하기: 문제의 본질부터 짚고 가기

CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 Origin(스킴 + 호스트 + 포트)에서 제공되는 리소스에 접근할 때 적용되는 보안 메커니즘입니다. 서버가 적절한 헤더로 “허용”했음을 명시하지 않으면, 브라우저는 요청을 막거나 응답을 노출하지 않습니다. 이때 가장 핵심적인 응답 헤더가 바로 Access-Control-Allow-Origin(이하 ACAO)입니다.

하지만 많은 프로젝트에서 ACAO를 단순히 “*”로 설정하거나, Origin 헤더를 그대로 반사(reflect)하는 방식으로 문제를 해결하려다 보안 취약점과 유지보수 문제를 야기합니다. 이 글에서는 CORS의 원리부터 ACAO 설정 최적화, 실전 사례, 성능 개선, 보안 체크리스트까지 “현업에서 바로 적용 가능한” 방법을 정리합니다.


CORS의 동작 요약

  • 같은 Origin이란 무엇인가?
  • 간단 요청(Simple Request):
    • GET, HEAD, POST 중 특정 조건을 만족하는 요청은 사전 검사(Preflight) 없이 바로 보냅니다.
    • Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 하고, 커스텀 헤더가 없어야 합니다.
  • 사전 검사(Preflight, OPTIONS):
    • 그 외 대부분의 요청은 브라우저가 먼저 OPTIONS 요청을 보내 서버가 허용하는 메서드/헤더를 확인합니다.
    • 서버는 Access-Control-Allow-Methods, Access-Control-Allow-Headers, 그리고 필요한 경우 Access-Control-Max-Age로 응답합니다.
  • 응답 노출 제한:
    • 브라우저는 서버가 허용한 범위에 따라 응답 접근을 제한합니다. 쿠키/인증정보를 포함한 요청은 추가 요건이 있습니다.

Access-Control-Allow-Origin의 의미와 함정

  • ACAO: 어떤 Origin에서의 접근을 허용할지 서버가 명시합니다.
    • 예: Access-Control-Allow-Origin: https://app.example.com
    • “*”는 모든 Origin 허용을 의미하지만, credentials(쿠키/인증 토큰 포함) 요청과 함께 사용할 수 없습니다.
  • 복수 Origin:
    • ACAO는 하나의 값만 허용합니다. 콤마로 여러 Origin을 나열할 수 없습니다. 필요한 경우 서버가 요청의 Origin을 검사해 “해당 Origin 하나”만 설정해야 합니다.
  • Vary: Origin 반드시 고려:
    • 동적으로 Origin을 반사하는 경우, Vary: Origin 헤더를 추가해 캐시/프록시가 Origin 별로 응답을 구분하도록 해야 캐시 오염을 방지합니다.
  • null Origin 주의:
    • file://, sandbox된 iframe, 일부 데이터 URL 등에서 오는 요청은 Origin 값이 “null”일 수 있습니다. 무분별하게 허용하면 보안 위험이 큽니다.

자주 보는 에러와 근본 원인

  • “No ‘Access-Control-Allow-Origin’ header is present on the requested resource”
    • 서버에서 ACAO가 빠졌거나, 프록시/리다이렉트 구간에서 사라졌습니다.
  • “The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’”
    • 쿠키/인증정보를 보내는 요청인데 ACAO를 “*”로 설정했습니다. 정확한 Origin 값을 넣어야 합니다.
  • “Response to preflight request doesn’t pass access control check”
    • OPTIONS 응답에 Access-Control-Allow-Methods/Headers가 충분히 설정되지 않았거나, 상태 코드/리다이렉트 처리에 문제가 있습니다.
  • 302/301 리다이렉트에서 CORS 실패
    • 리다이렉트 응답에도 CORS 헤더가 필요합니다. 중간 프록시/CDN 단계에서 헤더가 누락되지 않도록 하세요.

설계 원칙: 보안과 유지보수의 균형

  1. 최소 허용 원칙
    • 정확한 도메인만 허용하고, 불가피할 때에만 와일드카드를 사용합니다.
  2. 자격 증명(쿠키, 인증 토큰) 처리 시 엄격 모드
    • ACAO는 정확한 Origin으로 설정하고, Access-Control-Allow-Credentials: true를 사용합니다.
    • 쿠키를 쓸 경우 Set-Cookie에 SameSite=None; Secure 설정을 포함합니다.
  3. 동적 반사는 “검증 후 반사”
    • 요청 Origin을 안전한 allowlist로 검증한 뒤, 해당 Origin만 ACAO에 넣습니다. 정규식은 엄격하게 작성하고, “.example.com.evil.com” 같은 우회가 없도록 주의하세요.
  4. 캐시/프록시 고려
    • Vary: Origin을 반드시 포함하고, CDN의 캐시 키에도 Origin을 포함하세요.

실전 예제 1: Node.js(Express)에서 안전한 동적 Origin 허용

간단히 cors 미들웨어를 쓰되, 함수형 origin 옵션으로 allowlist를 검사합니다.

// npm i cors express
const express = require('express');
const cors = require('cors');

const app = express();
const allowlist = new Set([
  'https://app.example.com',
  'https://admin.example.com',
]);

const corsOptionsDelegate = (req, callback) => {
  const requestOrigin = req.header('Origin');
  let corsOptions;

  if (requestOrigin && allowlist.has(requestOrigin)) {
    corsOptions = {
      origin: requestOrigin,
      credentials: true, // 쿠키/인증정보 허용 시
      allowedHeaders: ['Content-Type', 'Authorization'],
      methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
      maxAge: 600, // Preflight 캐시(브라우저 상한 있음)
      exposedHeaders: ['X-Request-Id'],
    };
  } else {
    corsOptions = { origin: false }; // 허용하지 않음
  }
  callback(null, corsOptions);
};

app.use(cors(corsOptionsDelegate));

// OPTIONS 핸들링 (cors 미들웨어가 자동 처리)
app.options('*', cors(corsOptionsDelegate));

app.get('/api/data', (req, res) => {
  res.setHeader('Vary', 'Origin'); // 동적 ACAO시 권장
  res.json({ ok: true });
});

app.listen(3000);

팁:

  • 동적으로 Origin을 설정할 때는 반드시 res.setHeader('Vary', 'Origin') 또는 서버/프록시 레벨에서 Vary 적용.
  • allowlist를 코드에 하드코딩하지 말고 환경변수/DB로 관리하면 운영 편의성이 커집니다.

실전 예제 2: NGINX 리버스 프록시에서 CORS 처리

하나의 서버에서 여러 Origin을 허용하려면 map을 사용해 정교하게 적용합니다.

map $http_origin $cors_origin {
  default "";
  "~^https://app\.example\.com$"   $http_origin;
  "~^https://admin\.example\.com$" $http_origin;
}

server {
  listen 443 ssl;
  server_name api.example.com;

  location / {
    if ($request_method = OPTIONS) {
      add_header Access-Control-Allow-Origin $cors_origin always;
      add_header Access-Control-Allow-Credentials "true" always;
      add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
      add_header Access-Control-Allow-Headers "Content-Type,Authorization" always;
      add_header Access-Control-Max-Age "600" always;
      add_header Vary "Origin" always;
      return 204;
    }

    proxy_pass http://upstream_api;
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Access-Control-Allow-Credentials "true" always;
    add_header Vary "Origin" always;
  }
}

주의:

  • 정규식은 “.”를 이스케이프하고, 스킴까지 포함해야 합니다.
  • $cors_origin이 빈 문자열일 때는 ACAO가 추가되지 않아야 합니다. map에서 default ""로 처리.

실전 예제 3: Spring Boot에서 CORS 구성

전역 설정으로 특정 도메인만 허용합니다.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
      .allowedOrigins("https://app.example.com", "https://admin.example.com")
      .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
      .allowedHeaders("Content-Type","Authorization")
      .exposedHeaders("X-Request-Id")
      .allowCredentials(true)
      .maxAge(600);
  }
}

만약 테넌트별 커스텀 도메인이 동적으로 늘어나는 SaaS라면, Filter에서 Origin을 검사하고 응답 헤더를 직접 설정하는 전략을 사용하세요. 이때 반드시 Vary: Origin을 추가합니다.


실전 예제 4: S3/CloudFront에서 파일 업로드 CORS

직접 브라우저에서 S3로 업로드(PUT)하거나, pre-signed URL을 사용할 때는 S3 버킷 CORS 규칙이 필요합니다.

S3 버킷 CORS 예시(XML):

<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>https://app.example.com</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <MaxAgeSeconds>600</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>

CloudFront를 경유한다면:

  • 캐시 키에 Origin 헤더를 포함하거나, “Cache policy”에서 Origin을 포함.
  • Origin 요청 정책과 응답 헤더 정책을 적절히 설정.
  • 리다이렉트/에러 페이지에도 CORS 헤더가 유지되도록 확인.

Credentials(쿠키/토큰) 요청을 안전하게 처리하는 요령

  • 서버:
    • Access-Control-Allow-Credentials: true
    • Access-Control-Allow-Origin: 정확히 요청 Origin
    • Vary: Origin
  • 클라이언트(fetch):
    • fetch(url, { credentials: 'include' })
    • XMLHttpRequest에서는 xhr.withCredentials = true
  • 쿠키 설정:
    • Set-Cookie: name=value; Path=/; Domain=.example.com; Secure; HttpOnly; SameSite=None
    • Secure는 HTTPS에서만 동작. SameSite=None이 없으면 크로스 사이트 쿠키 전송이 차단됩니다.

주의:

  • ACAO에 “*”와 Credentials를 함께 사용할 수 없습니다.
  • Authorization 헤더를 쓴다면 Preflight가 발생합니다. API를 단순화하거나 토큰을 쿠키 기반으로 전환하는 전략도 고려하세요.

Preflight(OPTIONS) 최적화: 성능과 안정성

  • Access-Control-Max-Age로 Preflight 캐시
    • 서버는 600~7200초 범위로 설정하는 경우가 많습니다.
    • 브라우저마다 상한이 다릅니다(사파리는 상대적으로 보수적, 수분 단위 제한이 흔함). 목표 브라우저에서 실제 동작을 검증하세요.
  • Preflight 자체를 줄이기
    • 가능하면 Simple Request 요건을 맞추세요.
    • Content-Type을 application/json 대신 text/plain + 서버 변환으로 설계해도 됩니다. 다만 유지보수성을 고려해 트레이드오프를 평가해야 합니다.
    • 커스텀 헤더 최소화. 필요 없는 X- 요청 헤더 제거.
  • CDN/프록시 캐싱
    • Preflight 응답도 캐시될 수 있습니다. Vary: Origin을 유지하고, CDN에서 OPTIONS 응답 캐시 정책을 명확히 하세요.
  • 상태 코드
    • Preflight 응답은 204/200 등 성공 코드로, 본문 없이도 충분합니다.

실전 사례로 보는 문제 해결

사례 1: SPA + API 서브도메인, 쿠키 인증

상황:

해결:

  • API 서버
    • ACAO: 요청 Origin이 app.example.com일 때만 설정
    • Access-Control-Allow-Credentials: true
    • Vary: Origin
  • 쿠키
    • Set-Cookie: session=...; Domain=.example.com; Path=/; Secure; HttpOnly; SameSite=None
  • 클라이언트
    • fetch('/me', { credentials: 'include' })

검증:

  • DevTools Network에서 Request Headers의 Origin, Response Headers의 ACAO/ACAC(Allow-Credentials) 확인
  • OPTIONS 요청에서 Allow-Methods/Headers/Max-Age 확인

성능:

  • Access-Control-Max-Age 600 설정
  • 커스텀 헤더 제거로 Preflight 빈도 추가 감소

보안:

  • allowlist에 app.example.com만 포함
  • admin 도메인 분리 시 별도 정책

사례 2: 멀티테넌트 SaaS, 고객 커스텀 도메인 지원

상황:

해결:

  • DB/설정 저장소에 테넌트별 허용 Origin 목록 관리
  • 서버에서 요청 Origin을 조회해 허용 시 ACAO에 그대로 반영
  • 부정확한 정규식 금지, 정확한 일치 또는 엄격한 앵커 사용
  • Vary: Origin 필수
  • 관측
    • 미허용 Origin 요청 로깅 및 경고 대시보드 구성
    • OPTIONS 비율 모니터링으로 Preflight 튜닝

보안:

  • “null” Origin 기본 거부
  • 와일드카드 서브도메인 허용 불가(ACAO는 와일드카드 서브도메인 패턴을 지원하지 않음). 반드시 명시적 Origin만 허용.

사례 3: S3에 대한 대용량 파일 업로드

상황:

  • 브라우저에서 S3로 직접 PUT 업로드
  • x-amz-* 헤더들로 인해 Preflight 발생

해결:

  • S3 CORS에 AllowedOrigin을 앱 도메인으로 제한
  • AllowedMethod에 PUT 포함, AllowedHeader에 필요한 x-amz-* 포함
  • CloudFront를 사용한다면 OPTIONS 캐시 최적화, Origin 포함한 캐시 키 설정
  • 성능을 위해 MaxAgeSeconds 증가(브라우저 상한 고려)

안티패턴과 보안 이슈

  • Origin 무차별 반사
    • 요청 Origin을 무조건 ACAO로 되돌려주는 패턴은 공격자가 악성 도메인에서 민감 데이터를 읽도록 악용할 수 있습니다. 반드시 allowlist로 검증하세요.
  • ACAO “*” + Credentials
    • 브라우저가 거부합니다. 보안상도 위험합니다.
  • “null” Origin 허용
    • file://, sandbox iframe 등에서 오는 요청까지 허용하면 데이터 유출 리스크가 큽니다.
  • 리다이렉트에서 CORS 헤더 누락
    • 301/302 응답에도 CORS 헤더를 넣거나, 리다이렉트를 회피하는 구조로 바꾸세요.
  • 과도하게 넓은 Allowed-Headers/Methods
    • 필요한 최소 범위로 제한하세요. 특히 Authorization 외에 불필요한 커스텀 헤더는 제거를 권장합니다.

테스트와 디버깅 방법

  • curl로 빠르게 재현
  • 브라우저 DevTools
    • Network 탭에서 Request/Response Headers 확인
    • 콘솔 에러 메시지로 누락된 헤더 식별
  • 서버/프록시 로그
    • 요청 Origin을 로그에 남기고 거부 사유를 모니터링
  • 자동화 테스트
    • 통합 테스트에 OPTIONS 시나리오 포함
    • 주요 브라우저(Chrome, Safari, Firefox)에서 실제 동작 검증

CDN/프록시와 CORS: 놓치기 쉬운 포인트

  • Vary: Origin
    • 동적 ACAO 응답은 CDN이 Origin별로 캐싱해야 합니다. 그렇지 않으면 잘못된 Origin의 헤더가 재사용됩니다.
  • 캐시 키 구성
    • CloudFront/Nginx/Envoy 등에서 Origin을 캐시 키에 포함시키는 정책을 적용합니다.
  • OPTIONS 캐시
    • Preflight 응답을 일정 시간 캐시해 부하를 줄일 수 있습니다.
  • 헤더 유지
    • 프록시/리다이렉트 단계에서 CORS 헤더가 사라지지 않도록 “always” 플래그 또는 헤더 전파 정책을 설정합니다.

운영 체크리스트

  • 정책
    • 허용 Origin 명시 목록(환경/테넌트별)
    • Credentials 사용 여부와 이유
    • Allowed-Methods/Allowed-Headers 최소화
  • 구현
    • 동적 ACAO 시 Vary: Origin 적용
    • OPTIONS 응답: 200/204, Allow-Methods/Headers/Max-Age 포함
    • 리다이렉트/에러 응답에도 CORS 헤더 확인
  • 보안
    • null Origin 차단
    • 정규식 우회 방지(정확한 앵커, 스킴 포함)
    • 로그/알림으로 비정상 Origin 탐지
  • 성능
    • Access-Control-Max-Age 적정 설정(목표 브라우저 검증)
    • Preflight 빈도 모니터링 및 헤더 단순화
    • CDN 캐시 정책과 일관성

빠른 레시피 모음

  • Express + cors
    • credentials 필요하면 origin: true 대신 allowlist 검증 함수 사용
    • res.setHeader('Vary','Origin') 추가
  • Nginx
    • map + 정규식으로 안전한 Origin 매핑
    • OPTIONS 별도 처리 + always 플래그
  • Spring Boot
    • WebMvcConfigurer로 전역 규칙
    • 동적 필요 시 Filter로 Origin 검증 + 직접 헤더 설정
  • Kubernetes Ingress-NGINX
    • nginx.ingress.kubernetes.io/configuration-snippet으로 헤더 추가
    • 또는 서버 애플리케이션에서 처리
  • CloudFront
    • Cache policy에 Origin 포함
    • Response headers policy로 CORS 일괄 적용
  • S3
    • 버킷 CORS에 AllowedOrigin/Methods/Headers 정확히 설정
    • Pre-signed URL이면 동일 규칙 필요

문제 해결 가이드: 상황별 처방전

  • 쿠키가 안 간다
    • 클라이언트: credentials: 'include'
    • 서버: ACAC: true, ACAO: 정확한 Origin
    • 쿠키: SameSite=None; Secure
  • “* not allowed with credentials”
    • ACAO를 요청 Origin으로 설정(allowlist 검증 후)
  • Preflight 과다
    • 커스텀 헤더/메서드 줄이기
    • Max-Age 늘리기(브라우저 상한 확인)
    • OPTIONS 캐시 도입
  • S3 업로드 실패
    • AllowedHeader에 필요한 x-amz-* 포함
    • CloudFront 캐시 정책 점검
  • Redirect에서 실패
    • 중간 30x 응답에도 CORS 헤더가 유지되도록 설정하거나, 리다이렉트 없는 경로 사용

마무리: “동적, 최소, 검증”이 핵심

CORS는 복잡해 보이지만 원리는 단순합니다. 서버가 어떤 Origin을 허용하는지 명시하고, 필요한 경우에만 최소한으로 문을 열면 됩니다. 실전에서는 다음 3가지를 기억하세요.

  1. 동적: 여러 Origin을 지원해야 한다면 요청 Origin을 검증 후 반사합니다.
  2. 최소: Methods/Headers/Origins를 최소화하고, Credentials는 꼭 필요한 경우에만 사용합니다.
  3. 검증: Vary: Origin, CDN 캐시 키, 리다이렉트/에러 경로, 브라우저별 Preflight 상한 등 운영 환경 전체를 검증합니다.

이 글의 예제와 체크리스트를 적용하면, CORS 에러에 당황하지 않고 안정적이면서도 성능 좋은 ACAO 설정을 구축할 수 있을 것입니다.

개발 파트너가 필요하신가요?

DevTeam은 MVP·웹·앱·AI 개발을 설계부터 배포·운영까지 한 팀이 책임집니다.

이 글 공유하기
Twitter LinkedIn
최종 수정: 2026년 06월 06일

development 관련 글

더 많은 스타트업 노하우와 비즈니스 인사이트를 확인해보세요

서버 리소스 고갈 방지: CPU 과부하 및 메모리 부족(OOM) 모니터...

효율적인 서버 관리를 위해 CPU 과부하 및 메모리 부족을 모니터링하는 방법을 알아보...

Preflight 요청 실패 시 해결 방법: CORS 정책 최적화 및 실전...

CORS 요청 실패? 최적화 기술과 사례로 문제를 해결하세요.

장애 조기 감지를 위한 로그 분석 및 알림 시스템 구축: Sentry...

실시간 장애 조기 감지를 위해 Sentry를 활용한 로그 분석과 알림 시스템을 설계하고...

504 Gateway Timeout: 웹 개발자를 위한 원인 분석 및 복구 절차

웹 개발자를 위한 504 게이트웨이 타임아웃의 원인 분석과 효과적인 복구 방법에 대한...

파일 경로 오류 및 의존성 충돌 문제 해결: 환경 설정 최적화 가...

효과적인 개발 환경을 유지하기 위해 파일 경로 오류 및 의존성 충돌의 원인을 진단하...

IE/Edge 레거시 지원을 위한 브라우저 호환성 모범 사례

브라우저 호환성 문제를 해결하고 Safari 특정 버그를 해결하기 위한 전문가 가이드

전문가 도움이 필요하신가요?

스타트업과 비즈니스 성장을 위한 전문 컨설팅을 받아보세요.
확장 가능하고 비즈니스 성과로 이어지는 솔루션을 구축할 수 있도록 도와드립니다.