Skip to Content
Journal · Frontend

로컬에선 됐는데 배포하니 CORS — preflight 디버깅기

로컬에서 멀쩡하던 API 호출이 배포 환경에서 CORS 에러로 막혔다. 브라우저의 동일 출처 정책과 preflight 요청을 이해하지 못하면 끝없이 헤맬 수 있는 문제다. 원인과 해결을 기록한다.
실무 기록 목록으로
1

증상: 콘솔의 빨간 줄

프론트엔드(https://app.example.com)에서 API (https://api.example.com)를 호출하자 브라우저 콘솔에 익숙한 에러가 떴다. 정작 백엔드 로그에는 에러가 없었다.

브라우저 콘솔
Access to fetch at 'https://api.example.com/orders'
from origin 'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

핵심 오해 풀기

CORS는 서버가 요청을 거부한 게 아니다. 서버는 응답을 정상적으로 보냈지만, 출처가 다르고 허용 헤더가 없어서 브라우저가 응답을 가로막은것이다. 그래서 백엔드 로그엔 흔적이 없다.

2

preflight(OPTIONS)의 정체

네트워크 탭을 보니 실제 요청 전에 OPTIONS 요청이 먼저 나가고 있었다. 이것이 preflight다. 브라우저가 ‘이 교차 출처 요청을 보내도 되는지’를 서버에 미리 묻는 단계다.

preflight 요청/응답
# 브라우저가 먼저 보내는 예비 요청
OPTIONS /orders
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

# 서버가 허용을 응답해야 본 요청이 진행된다
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
  • 단순 요청(GET, 단순 헤더)은 preflight 없이 바로 간다.
  • Content-Type: application/json이거나 Authorization 헤더가 붙으면 preflight가 발생한다 — 대부분의 실제 API가 여기 해당한다.
3

원인: 서버가 허용을 안 함

로컬에선 프론트와 API가 같은 출처(또는 dev 프록시)였기에 CORS가 발동하지 않았다. 배포 후 출처가 갈리면서, 서버가 OPTIONS에 허용 헤더를 주지 않아 본 요청이 시작도 못 한 것이다.

“로컬에선 됐는데”의 정체

개발 서버의 프록시(예: Next의 rewrites, Vite proxy)가 같은 출처처럼 보이게 만들어 CORS를 숨기고 있었다. 환경이 바뀌면 드러나는 전형적인 함정이다.

4

해결: 서버 CORS 설정

CORS는 서버에서 허용해야 풀린다. 프론트에서 우회할 수 없다(보안상 당연하다). 백엔드에 허용 출처·메서드·헤더를 명시했다.

Spring Boot 예시
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("https://app.example.com") // 구체적 출처
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}
FastAPI 예시
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_methods=["*"],
    allow_headers=["*"],
    allow_credentials=True,
)
5

함정과 교훈

빠지기 쉬운 함정

  • credentials + 와일드카드 불가: 쿠키·인증을 보내려면 allow_credentials=true인데, 이때 Allow-Origin: *는 브라우저가 거부한다. 출처를 구체적으로 명시해야 한다.
  • OPTIONS도 라우팅돼야: 인증 필터가 OPTIONS를 먼저 가로채 401을 주면 preflight가 실패한다. preflight는 인증 예외로 둔다.
  • 프론트 우회는 불가: CORS는 브라우저 보안이라 클라이언트 코드로 못 푼다. 정답은 항상 서버 설정(또는 같은 출처로 묶는 게이트웨이).

HTTP 헤더와 메서드의 기초는 HTTP / REST API 설계 글에서 다룬다.

CORS 에러는 서버의 거부가 아니라 브라우저의 보호다.
preflight의 흐름을 이해하면, 빨간 줄은 더 이상 미스터리가 아니다.