development

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

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

2025년 10월 05일
CORS Preflight WebDevelopment Optimization CaseStudies JavaScript APIs
7분 읽기

최종 업데이트: 2026년 6월. Private Network Access(PNA), Access-Control-Max-Age 캐시 상한, 서드파티 쿠키 폐지에 따른 인증 전략, 엣지/서버리스에서의 OPTIONS 처리 등 최신 브라우저·인프라 변화를 반영했습니다. 원문의 스택별 설정 예제는 그대로 유효합니다.

30초 요약 (TL;DR)

  • Preflight는 브라우저가 실제 요청 전에 OPTIONS로 권한을 확인하는 절차입니다. 커스텀 헤더, application/json 본문, PUT/DELETE/PATCH 등 “non-simple” 요청에서 자동 발생합니다.
  • 실패 핵심 원인: 서버가 OPTIONS에 올바른 Access-Control-Allow-Origin/Headers/Methods를 반환하지 않음, Origin 미허용, 자격증명(credentials)과 와일드카드(*) 충돌, 리다이렉트, 프록시/엣지가 OPTIONS를 가로챔.
  • 가장 중요한 규칙: fetch(..., {credentials:'include'})를 쓰면 Access-Control-Allow-Origin*를 쓸 수 없습니다. 정확한 Origin을 반향하고 Access-Control-Allow-Credentials: true를 함께 보내야 합니다.
  • 1차 진단: 브라우저 콘솔의 정확한 CORS 메시지 확인 → Network 탭에서 OPTIONS 응답 헤더 확인 → curl -X OPTIONS -H 'Origin: ...' -H 'Access-Control-Request-Method: ...' 로 재현.
  • 성능: 안전한 범위에서 Access-Control-Max-Age로 프리플라이트를 캐싱하되, 브라우저별 상한(크롬 약 7200초)을 넘기지 마세요.

왜 Preflight가 실패할까? CORS의 본질부터 이해하기

브라우저가 다른 출처(origin)로 네트워크 요청을 보낼 때, 보안을 위해 CORS(Cross-Origin Resource Sharing) 규칙을 따릅니다. 특히 요청이 “간단하지 않은” 경우 브라우저는 본 요청 전에 사전 협상용 OPTIONS 요청, 즉 preflight를 보냅니다. 이 preflight에서 서버가 명확한 허용 의사를 밝히지 않으면 본 요청은 시작조차 하지 못하고 브라우저 콘솔에 CORS 관련 에러가 찍히죠.

핵심은 다음과 같습니다.

  • 브라우저는 서버가 안전하게 특정 출처의 요청을 수용하는지 사전 확인한다.
  • 서버는 명시적인 CORS 응답 헤더로 허용 범위를 알려야 한다.
  • 아주 작은 설정 누락이나 프록시/리다이렉트, CDN 캐시 레이어에서도 손쉽게 실패한다.

이 글에서는 preflight 요청 실패의 흔한 원인과, 각 스택에서 즉시 적용 가능한 해결책, 성능 최적화까지 실전적으로 정리합니다.


Preflight는 언제 발생하나? (Simple vs. Non-simple)

아래 조건을 모두 만족하면 “simple request”로 간주되어 preflight 없이 바로 시도됩니다.

  • HTTP 메서드: GET, HEAD, POST 중 하나
  • 커스텀 요청 헤더 없음 (허용: Accept, Accept-Language, Content-Language, Content-Type 일부 등)
  • Content-Type이 아래 중 하나
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • ReadableStream body 사용하지 않음 등 추가 제약

다음 중 하나라도 해당하면 preflight가 발생합니다.

  • 메서드가 PUT, PATCH, DELETE 등
  • 커스텀 헤더 사용 (예: X-Request-Id, X-Auth-Token)
  • Content-Type: application/json 등 simple 범주 밖
  • Private Network Access 상황(공용 → 사설 네트워크)

브라우저가 보내는 preflight는 대략 이런 형식입니다.

OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, x-auth-token

서버는 허용 여부를 다음과 같이 명시해야 합니다.

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: content-type, x-auth-token
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Vary: Origin

주의:

  • credentials(쿠키/인증 포함) 요청이면 Allow-Origin에 와일드카드(*)를 사용할 수 없습니다.
  • 프리플라이트도 리다이렉트나 4xx, 5xx가 되면 실패합니다.

흔한 실패 원인 10가지와 증상

  1. 허용 출처 미설정 또는 오탈자
  • 에러: “has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header…”
  • 해결: 정확한 Origin을 설정하고, 동적 화이트리스트 사용 시 Vary: Origin 추가.
  1. Allow-Methods/Allow-Headers 누락
  • 에러: “Request header field X-Auth-Token is not allowed…”
  • 해결: Access-Control-Allow-Headers에 요청 헤더(소문자 기준 정규화) 포함. 메서드도 명시.
  1. Credentials와 와일드카드 충돌
  • 에러: “The value of the ‘Access-Control-Allow-Origin’ header in the response must not be ‘*’…”
  • 해결: credentials를 사용할 땐 정확한 Origin 값을 되돌려주고 Access-Control-Allow-Credentials: true.
  1. OPTIONS가 서버/프록시에서 차단
  • 에러: 405 Method Not Allowed, 501 Not Implemented
  • 해결: 서버/로드밸런서/CDN에서 OPTIONS 허용 및 라우팅.
  1. 리다이렉트(30x) 구간에서 CORS 헤더 누락
  • 에러: preflight 단계에서 실패, 네트워크 탭에 302 후 실패 표시
  • 해결: 리다이렉트를 피하거나 모든 단계에서 동일한 CORS 헤더 제공.
  1. CDN/프록시가 CORS 헤더를 제거 또는 캐시 오염
  • 증상: 특정 Origin만 간헐적으로 실패
  • 해결: CORS 헤더를 origin-별로 캐시 분리(Vary: Origin), 헤더 보존 설정.
  1. Access-Control-Max-Age 누락으로 과도한 preflight
  • 증상: 같은 엔드포인트 매 요청 전 preflight
  • 해결: 적절한 Max-Age 설정. 브라우저별 상한이 있어 수 분~수십 분 내 안정적 값 추천.
  1. Private Network Access(PNA) 헤더 누락
  • 에러: “Preflight request failed due to private network access” 등
  • 해결: 공용 → 사설망 요청 시 Access-Control-Allow-Private-Network: true 응답.
  1. Content-Type이 application/json으로 인한 불필요한 preflight
  • 증상: 모든 POST 요청마다 preflight
  • 해결: 가능하다면 simple 타입(form-urlencoded 등) 사용, 혹은 preflight 캐싱 최적화.
  1. 대소문자/스펠링 문제
  • 증상: Allow-Headers에 포함했는데도 불허
  • 해결: 서버에서 소문자로 정규화해 비교. 쉼표, 공백 처리 주의.

디버깅 절차: 단계별 접근

  1. 브라우저 DevTools Network 탭
  • OPTIONS 요청이 실제로 발생했는지 확인
  • 응답 헤더에 Access-Control-Allow-*가 올바른지 검토
  • 리다이렉트 체인 존재 여부 확인
  1. 콘솔 에러 메세지 해석
  • 어떤 헤더가 허용되지 않았는지, 어떤 정책이 충돌했는지 힌트 제공
  1. curl로 재현
curl -i -X OPTIONS 'https://api.example.com/users' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: content-type, x-auth-token'
  • 응답 상태 코드(2xx 여부), 헤더, CORS 값 확인
  1. 서버/프록시/CDN 로그 확인
  • OPTIONS 라우팅 여부, 헤더가 중간에서 제거되는지 확인
  1. 최소 케이스로 축소
  • 커스텀 헤더 제거, 메서드 변경 등으로 문제 축소해 원인 분리

실무 해결 전략: 각 스택별 설정 예제

Node.js (Express)

  • 원칙: cors 미들웨어 사용 시 동적 화이트리스트와 Vary 헤더를 명시
import express from 'express';
import cors from 'cors';

const app = express();

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

const corsOptionsDelegate = (req, callback) => {
  const origin = req.header('Origin');
  const isAllowed = origin && allowlist.has(origin);
  const options = {
    origin: isAllowed, // true면 해당 Origin을 그대로 반영
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
    allowedHeaders: ['content-type', 'x-auth-token', 'authorization'],
    exposedHeaders: ['x-request-id'],
    maxAge: 3600,
  };
  callback(null, options);
};

app.use((req, res, next) => {
  res.setHeader('Vary', 'Origin'); // 캐시 분리
  next();
});
app.use(cors(corsOptionsDelegate));

app.options('*', cors(corsOptionsDelegate)); // Preflight 처리

app.get('/health', (req, res) => res.send('ok'));

app.listen(3000);
  • 주의: credentials 사용 시 Access-Control-Allow-Origin: * 불가. 미들웨어가 이를 자동 처리하지만 whitelist를 제대로 관리해야 합니다.

Spring Boot

  • WebMvcConfigurer를 통한 글로벌 CORS 정책
@Configuration
public class CorsConfig 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","PATCH","OPTIONS")
      .allowedHeaders("content-type","x-auth-token","authorization")
      .exposedHeaders("x-request-id")
      .allowCredentials(true)
      .maxAge(3600);
  }
}
  • 프록시/리다이렉트 경유 시에도 헤더가 유지되도록 인프라 레벨 설정 필요.

Nginx (리버스 프록시/에지)

  • OPTIONS 허용과 CORS 헤더 설정
server {
  listen 443 ssl;
  server_name api.example.com;

  # 일반 API 라우트
  location /api/ {
    if ($request_method = OPTIONS) {
      add_header 'Access-Control-Allow-Origin' $http_origin;
      add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,PATCH,OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'content-type, x-auth-token, authorization';
      add_header 'Access-Control-Allow-Credentials' 'true';
      add_header 'Access-Control-Max-Age' 3600;
      add_header 'Vary' 'Origin';
      return 204;
    }

    proxy_pass http://backend;
    # 응답에도 CORS 헤더 보장
    add_header 'Access-Control-Allow-Origin' $http_origin always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Vary' 'Origin' always;
  }
}
  • 변수 $http_origin 사용 시 화이트리스트 검증을 별도 구현하거나, 백엔드에서 검증 후 헤더 세팅.

Django

  • django-cors-headers 패키지 사용 권장
# settings.py
INSTALLED_APPS = [
  # ...
  "corsheaders",
]

MIDDLEWARE = [
  "corsheaders.middleware.CorsMiddleware",
  # ...
]

CORS_ALLOWED_ORIGINS = [
  "https://app.example.com",
  "https://admin.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = ["content-type", "x-auth-token", "authorization"]
CORS_EXPOSE_HEADERS = ["x-request-id"]
CORS_PREFLIGHT_MAX_AGE = 3600

S3/CloudFront 등 정적 호스팅

  • S3 CORS 구성 예시
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD", "OPTIONS"],
    "AllowedOrigins": ["https://app.example.com"],
    "ExposeHeaders": ["x-request-id"],
    "MaxAgeSeconds": 3600
  }
]
  • CloudFront 캐시 키에 Origin 포함(캐시 정책의 Origin 헤더 포함, 또는 Vary: Origin 존중)하여 캐시 오염 방지.

성능 최적화: Preflight를 줄이고, 안전하게 캐싱하기

  • Access-Control-Max-Age 설정
    • 수 분~수십 분 사이의 값 권장(브라우저별 상한 차이)
    • 예: 600~3600초
  • 커스텀 헤더 최소화
    • X-Auth-Token 대신 Authorization 표준 헤더 활용
    • 불필요한 X-Trace, X-Client-Time 제거
  • Content-Type 단순화
    • 가능하다면 application/x-www-form-urlencoded 또는 multipart/form-data로 전송
    • 예시: JSON 대신 폼 인코딩 사용 (백엔드 파싱 비용 vs. preflight 감소의 트레이드오프 고려)
  • 호출 병합/배치
    • 여러 작은 요청을 하나의 API로 묶어 preflight 빈도 줄이기
  • 동일 출처화(프록시)
    • 프론트 도메인과 같은 도메인 경로로 API를 프록시하여 CORS 자체를 제거
    • 예: https://app.example.com/api/* → 내부에서 https://api.example.com로 프록시

Vite/Next.js 개발 환경에서의 프록시 예시:

  • Vite
// vite.config.ts
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        secure: true,
      }
    }
  }
}
  • Next.js
// next.config.js
module.exports = {
  async rewrites() {
    return [
      { source: '/api/:path*', destination: 'https://api.example.com/:path*' },
    ];
  }
};

자격 증명(쿠키, 세션)과 CORS의 올바른 결합

  • 클라이언트
    • fetch: credentials: 'include'
    • Axios: withCredentials: true
  • 서버
    • Access-Control-Allow-Origin: https://app.example.com (절대 * 금지)
    • Access-Control-Allow-Credentials: true
    • 세션 쿠키: SameSite=None; Secure 필요(크로스 사이트 전송 시)
  • 응답 헤더 노출
    • 브라우저에서 특정 응답 헤더를 읽으려면 Access-Control-Expose-Headers 사용

예시:

// 클라이언트
fetch('https://api.example.com/me', {
  method: 'GET',
  credentials: 'include'
});

// 서버 응답 헤더
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: x-request-id

Private Network Access(PNA) 이슈 대응

공용(예: https://app.example.com)에서 사설망(예: http://192.168.0.10:8080)으로 접근할 때 브라우저가 사전 점검을 강화합니다.

  • Preflight 요청에 대해 서버가 아래 헤더로 허용 의사 명시:
    • Access-Control-Allow-Private-Network: true
  • 여전히 Access-Control-Allow-Origin 등 일반 CORS 헤더도 필요
  • 로컬 개발에서 자주 마주치는 문제로, 인트라넷 API 게이트웨이에 해당 헤더를 추가하세요.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Private-Network: true

리다이렉트와 CORS: 실패를 부르는 미묘한 함정

  • Preflight는 리다이렉트 도중 CORS 헤더가 누락되면 실패
  • OAuth 콜백, www → apex 리다이렉트 등에서 자주 노출
  • 해결책
    • CORS 대상 엔드포인트는 리다이렉트 없이 200으로 즉시 응답
    • 부득이한 리다이렉트라면 모든 단계에서 동일 CORS 헤더 추가
    • CDN/로드밸런서 레벨 규칙도 포함

실전 사례 1: SPA + API에서 JSON POST로 인한 과도한 preflight

  • 상황: React 앱이 POST /api/ordersContent-Type: application/json, X-Request-Id를 포함하여 요청. 매 호출마다 preflight.
  • 문제: 네트워크 혼잡, 초기 로딩 느림
  • 해결:
    1. 커스텀 헤더 제거(서버에서 X-Request-Id 생성 후 Access-Control-Expose-Headers로 노출)
    2. 가능하다면 application/x-www-form-urlencoded로 전환
    3. Access-Control-Max-Age: 1800 적용

Before:

POST /api/orders
Content-Type: application/json
X-Request-Id: abc-123

After:

POST /api/orders
Content-Type: application/x-www-form-urlencoded

서버는 폼 파서로 처리하고, 응답에:

Access-Control-Expose-Headers: x-request-id

결과적으로 preflight 빈도가 크게 줄고 TTFB 체감 개선.


실전 사례 2: CloudFront/ALB에서 OPTIONS 403

  • 상황: CloudFront → ALB → 애플리케이션. OPTIONS가 WAF 규칙에 막혀 403.
  • 해결:
    • WAF에서 OPTIONS 허용 규칙 추가
    • CloudFront Behavior에 Allowed Methods에 OPTIONS 포함
    • 오리진 응답에 CORS 헤더 보존
    • 캐시 정책에서 Origin 헤더 고려 또는 Vary: Origin 적용
  • 체크:
    • CloudFront 로그에서 sc-status, x-edge-result-type 확인
    • ALB Access Logs에서 403 누가 내렸는지 추적

실전 사례 3: 멀티테넌트 SaaS의 동적 Origin 허용

  • 상황: 고객별 커스텀 도메인으로 접근. 허용 리스트가 동적.
  • 요구사항: credentials 사용
  • 구현:
    • 서버에서 요청 Origin을 화이트리스트 검증
    • 일치 시 Access-Control-Allow-Origin: <요청 Origin> 반환
    • Vary: Origin 필수
    • CDN 캐시 키에 Origin을 포함하거나 헤더 기반 캐싱 켜기

Express 예시:

const allowlist = new Set(['https://a.tenant.com', 'https://b.tenant.com']);

app.use((req, res, next) => {
  const origin = req.header('Origin');
  if (origin && allowlist.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  next();
});

개발 환경 vs. 운영 환경: 안정적으로 배포하는 팁

  • 개발
    • Dev 서버 프록시 구성으로 CORS 회피
    • 브라우저 확장 프로그램으로 임시 우회는 금물(운영에서 동작하지 않음)
  • 운영
    • CORS 설정은 IaC(Terraform, Ansible) 등으로 버전 관리
    • 변경 시 스테이징에서 실제 브라우저로 시나리오 테스트
    • CDN/프록시/백엔드 모두 일관성 유지
    • 헬스체크/알림: preflight 실패율 모니터링

보안 모범 사례: “허용 최소화” 원칙

  • 허용 Origin을 구체적으로 제한, 필요 시 동적 화이트리스트 + 로깅
  • Allow-Methods/Allow-Headers는 필요한 값만
  • credentials 사용 시 절대 * 금지, 쿠키에는 HttpOnly/Secure/SameSite=None 적용
  • 민감 정보는 응답 헤더로 불필요하게 노출하지 않기
  • WebSocket은 CORS 대상이 아니나, 서버에서 Origin 검증을 별도로 수행
  • 정기적으로 CORS 정책 및 인프라 레이어 보안 검토

자주 쓰는 코드/설정 스니펫 모음

  • 표준 fetch with credentials
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include',
  headers: { 'Accept': 'application/json' },
});
  • 서버에서 Allow-Headers 계산 시 소문자 정규화
const reqHeaders = (req.headers['access-control-request-headers'] || '')
  .split(',')
  .map(h => h.trim().toLowerCase())
  .filter(Boolean);
const allowed = ['content-type', 'authorization', 'x-auth-token'];
const ok = reqHeaders.every(h => allowed.includes(h));
  • Apache 예시
<IfModule mod_headers.c>
  Header always set Access-Control-Allow-Origin "https://app.example.com"
  Header always set Access-Control-Allow-Credentials "true"
  Header always set Vary "Origin"
</IfModule>

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{REQUEST_METHOD} OPTIONS
  RewriteRule ^(.*)$ $1 [R=204,L]
</IfModule>

<Location "/api/">
  Header always set Access-Control-Allow-Methods "GET,POST,PUT,DELETE,PATCH,OPTIONS"
  Header always set Access-Control-Allow-Headers "content-type, authorization, x-auth-token"
  Header always set Access-Control-Max-Age "1800"
</Location>

Preflight 문제 방지를 위한 체크리스트

  • 요청 Origin이 정확히 허용되었는가? (오타/스킴/포트 포함)
  • credentials를 쓰면 Allow-Origin은 특정값, Allow-Credentials는 true인가?
  • Allow-Methods/Allow-Headers에 필요한 값이 모두 포함되어 있는가?
  • OPTIONS 메서드가 전체 경로를 통해 허용/라우팅되는가?
  • 리다이렉트 체인 중간에도 CORS 헤더가 유지되는가?
  • CDN/프록시가 CORS 헤더를 제거/캐시 오염하지 않는가? (Vary: Origin)
  • Access-Control-Max-Age로 preflight 캐싱을 활성화했는가?
  • PNA 상황에서 Allow-Private-Network 헤더가 필요한가?
  • 커스텀 헤더/JSON 전송 등 불필요한 preflight 유발 요소를 줄였는가?
  • 모니터링과 로깅으로 장애 상황을 빨리 파악할 수 있는가?

2026 업데이트: 최신 브라우저·인프라에서의 CORS

Private Network Access(PNA) — 새로운 프리플라이트

크롬은 공개 사이트(public)가 사용자의 사설망/로컬(localhost·내부 IP) 로 요청할 때 추가 프리플라이트를 요구합니다.

  • 브라우저가 Access-Control-Request-Private-Network: true 를 보내면, 서버는 Access-Control-Allow-Private-Network: true 로 응답해야 합니다.
  • 영향: 로컬 개발 서버 호출, IoT/프린터/내부 장비 제어 웹앱에서 “예전엔 되던” 요청이 갑자기 실패. 해당 응답 헤더를 추가해야 합니다.

Access-Control-Max-Age 캐시 상한

프리플라이트 결과 캐시는 무한정이 아닙니다. 브라우저별 상한이 있어(크롬 약 2시간=7200초) 그 이상 값을 줘도 잘립니다. 너무 큰 값에 의존하지 말고, 애초에 프리플라이트를 유발하지 않도록 요청을 단순화하는 편이 좋습니다.

서드파티 쿠키 폐지 → 인증 전략 전환

크로스 사이트 쿠키 제약이 강해지면서 쿠키 기반 크로스도메인 인증이 점점 어려워집니다.

  • 크로스 사이트 쿠키를 쓰려면 SameSite=None; Secure 가 필수이고, 그래도 브라우저 정책에 따라 차단될 수 있습니다.
  • 권장: 가능하면 Authorization: Bearer 토큰(헤더 기반)으로 전환하거나, 프론트와 API를 동일 사이트(서브도메인 + 적절한 쿠키 도메인) 로 배치해 크로스 오리진 자체를 줄이세요.

엣지/서버리스에서 OPTIONS 일원화

CloudFront Functions·Lambda@Edge·Cloudflare Workers·Vercel Edge에서 OPTIONS를 엣지에서 직접 처리하면, 오리진까지 가지 않아 빠르고 일관됩니다.

  • 주의: ALB/CloudFront가 OPTIONS를 캐시하거나 WAF가 차단해 403/404 OPTIONS가 나는 사례가 흔합니다. 메서드 허용·캐시 키·WAF 규칙을 점검하세요.

개발 환경은 프록시로 CORS 회피

Vite/Next.js 등의 개발 서버 프록시(rewrites/proxy)로 프론트와 API를 같은 오리진처럼 보이게 하면 개발 중 CORS 문제를 근본적으로 없앨 수 있습니다. 운영에서는 리버스 프록시로 같은 도메인 경로(/api)에 API를 매핑하는 것도 좋은 전략입니다.

명세는 “정확히”: 흔한 2026년 실수

  • 와일드카드 Access-Control-Allow-Headers: *자격증명 요청에서 무효입니다. 실제 헤더를 명시하세요.
  • Vary: Origin 을 빠뜨리면 CDN이 한 Origin의 CORS 응답을 다른 Origin에 잘못 캐싱합니다. 동적 Origin 반향 시 반드시 추가하세요.

자주 묻는 질문 (FAQ)

Q. 왜 OPTIONS 요청이 추가로 발생하나요? 브라우저가 “이 크로스 오리진 요청을 보내도 되는지”를 실제 요청 전에 확인하는 프리플라이트입니다. 커스텀 헤더(Authorization 등), application/json 본문, PUT/DELETE/PATCH 메서드를 쓰면 자동으로 발생합니다.

Q. Access-Control-Allow-Origin: * 인데 왜 쿠키가 안 가나요? 자격증명을 포함한 요청(credentials:'include')은 와일드카드와 함께 쓸 수 없습니다. 서버가 요청의 정확한 Origin 을 반향하고 Access-Control-Allow-Credentials: true 를 보내야 합니다.

Q. 개발에서는 되는데 운영에서만 CORS 에러가 납니다. 대개 운영의 프록시/CDN/WAF가 OPTIONS를 가로채거나 헤더를 제거하기 때문입니다. 엣지 계층의 OPTIONS 허용·헤더 전달·캐시 키, 그리고 운영 Origin이 허용 목록에 있는지 확인하세요.

Q. 프리플라이트를 줄이려면? 요청을 “simple”하게 유지(불필요한 커스텀 헤더 제거, 가능한 경우 표준 콘텐츠 타입 사용)하고, Access-Control-Max-Age로 결과를 캐싱하세요. 같은 오리진으로 배치하는 것이 가장 확실합니다.

Q. OPTIONS 요청에 403/404가 옵니다. 서버/프레임워크 라우트가 OPTIONS 메서드를 처리하지 않거나, 앞단의 WAF/프록시가 차단·리다이렉트하는 경우입니다. 라우팅에서 OPTIONS를 허용하고, 프리플라이트는 리다이렉트 없이 200/204로 즉시 응답해야 합니다.

Q. localhost로 보내는 요청이 갑자기 막혔습니다. 크롬의 Private Network Access 정책 때문일 수 있습니다. 서버 응답에 Access-Control-Allow-Private-Network: true 를 추가하세요.

마무리: 빠르고 안전한 CORS는 “정확한 명세 + 인프라 일관성”

preflight 실패의 대부분은 작은 설정 누락이나 인프라 레이어의 불일치에서 발생합니다. 브라우저가 무엇을 확인하려는지(Allow-Origin/Methods/Headers/Credentials/Max-Age), 그리고 요청 경로 전체(애플리케이션, 리버스 프록시, CDN, WAF, 리다이렉트)에 걸쳐 같은 규칙이 유지되는지 점검하세요.

추가로, 다음을 습관화하면 장기적으로 안정적입니다.

  • 정책을 코드/설정으로 버전 관리(IaC)
  • 변경 시 실제 브라우저로 e2e 검증
  • 최소 허용 원칙과 preflight 캐싱 최적화의 균형

CORS는 결국 신뢰 경계를 어떻게 명확히 표현할지의 문제입니다. 원칙을 이해하고 스택별로 일관되게 적용하면 “preflight 요청 실패”는 드문 이벤트가 됩니다. 이제 여러분의 서비스에서도 빠르고 안전한 크로스 오리진 통신을 구현해 보세요.

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

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

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

development 관련 글

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

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

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

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

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

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

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

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

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

Access-Control-Allow-Origin 설정 최적화: CORS 문제 해결을 위...

CORS 에러에 당황하지 마세요! Access-Control-Allow-Origin 설정을 최적화하여 안전...

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

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

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

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