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 관련 글

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

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

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

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

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

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

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

크로스 브라우저 호환성 이슈 해결: 웹 개발자를 위한 실전 사례...

웹 개발자를 위한 크로스 브라우저 호환성 문제 해결의 실전 사례와 효과적인 polyfil...

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

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

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

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

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

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