CORS (Cross-Origin Resource Sharing)

한눈에 보기

CORS는 브라우저의 동일 출처 정책을 안전하게 완화하는 HTTP 헤더 기반 메커니즘이다. 단순 요청과 프리플라이트, 자격 증명, 서버 설정에서 자주 하는 실수까지 정리했다.

최종 업데이트 2026.09.17

교차 출처 리소스 공유(CORS, Cross-Origin Resource Sharing)는 브라우저가 다른 출처의 리소스 요청을 안전하게 허용하기 위해 만든 HTTP 헤더 기반의 표준 메커니즘이다. 브라우저는 기본적으로 동일 출처 정책에 따라 스크립트가 다른 출처의 응답을 읽지 못하게 막는데, 서버가 CORS 헤더로 명시적으로 허용한 경우에만 이 제한을 풀어 준다. 따라서 개발자가 흔히 만나는 CORS 에러는 브라우저의 버그가 아니라 서버가 허용 의사를 밝히지 않았다는 신호다.

동일 출처 정책과 출처의 정의

동일 출처 정책(SOP, Same-Origin Policy)은 한 출처에서 실행되는 스크립트가 다른 출처의 응답 내용에 접근하지 못하게 하는 브라우저 보안 규칙이다. 출처(origin)는 프로토콜, 호스트, 포트 세 가지의 조합으로 정의되며 셋 중 하나라도 다르면 다른 출처로 취급된다.

  • https://devfit.dev 와 http://devfit.dev 는 프로토콜이 달라 다른 출처다.
  • https://devfit.dev 와 https://api.devfit.dev 는 호스트가 달라 다른 출처다.
  • http://localhost:3000 과 http://localhost:8080 은 포트가 달라 다른 출처다.

이 정책이 없다면 악성 사이트의 스크립트가 사용자의 브라우저에 저장된 인증 쿠키를 실어 은행 API를 호출하고 그 응답을 그대로 읽어 갈 수 있다. SOP는 이런 정보 탈취를 원천 차단하고, CORS는 그중 신뢰할 수 있는 교차 출처 요청만 선별적으로 열어 주는 완화 장치다. 중요한 점은 이 검사가 브라우저에서만 일어난다는 것이다. 서버 간 통신이나 curl 요청에는 CORS가 적용되지 않는다.

단순 요청과 프리플라이트

브라우저는 교차 출처 요청을 두 부류로 나누어 처리한다.

단순 요청(Simple Request)

GET, HEAD, POST 메서드이면서 Content-Type이 text/plain, multipart/form-data, application/x-www-form-urlencoded 중 하나이고 커스텀 헤더가 없는 요청이다. 브라우저는 이 요청을 바로 보내되, 응답에 Access-Control-Allow-Origin 헤더가 없으면 응답 내용을 스크립트에 넘기지 않고 버린다. 즉 요청 자체는 서버에 도달하며, 차단되는 것은 응답의 열람이다.

프리플라이트 요청(Preflight Request)

PUT, DELETE, PATCH 메서드를 쓰거나 Content-Type: application/json 또는 Authorization 같은 헤더를 붙이면 브라우저가 본 요청 전에 OPTIONS 메서드로 사전 확인을 보낸다. 동작 과정은 다음과 같다.

  1. 브라우저가 OPTIONS 요청에 Origin, Access-Control-Request-Method, Access-Control-Request-Headers 헤더를 담아 보낸다.
  2. 서버가 허용 출처, 허용 메서드, 허용 헤더를 응답 헤더로 알려 준다.
  3. 브라우저가 본 요청의 조건이 허용 범위에 들어가는지 검사한다.
  4. 통과하면 본 요청을 보내고, Access-Control-Max-Age에 지정된 시간 동안 프리플라이트 결과를 캐시해 반복을 줄인다.
OPTIONS /api/users HTTP/1.1
Origin: https://devfit.dev
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://devfit.dev
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

핵심 헤더 정리

헤더방향역할
Origin요청요청을 보낸 페이지의 출처를 알린다
Access-Control-Allow-Origin응답허용할 출처를 지정한다. * 또는 특정 출처 하나
Access-Control-Allow-Methods응답허용할 HTTP 메서드 목록
Access-Control-Allow-Headers응답본 요청에서 쓸 수 있는 헤더 목록
Access-Control-Allow-Credentials응답쿠키 등 자격 증명 포함 요청 허용 여부
Access-Control-Expose-Headers응답스크립트가 읽을 수 있는 응답 헤더 추가 지정

자격 증명(credentials)을 포함하는 요청에는 특별한 제약이 있다. 클라이언트가 credentials: 'include' 로 쿠키를 보내려면 서버는 Access-Control-Allow-Credentials: true 를 내려야 하고, 이때 Access-Control-Allow-Origin에 와일드카드 * 를 쓸 수 없으며 구체적인 출처를 명시해야 한다.

실무에서의 올바른 해결

CORS 에러의 올바른 해결은 언제나 서버 쪽 설정이다. 브라우저 보안 플래그를 끄거나 임의의 우회 프록시를 쓰는 것은 개발 편의일 뿐 해결이 아니다. Spring 기반 서버라면 다음과 같이 전역 설정을 둔다.

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://devfit.dev")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Content-Type", "Authorization")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

개발 환경에서는 프런트엔드 개발 서버의 프록시 기능으로 요청을 같은 출처처럼 보이게 우회하는 방법을 많이 쓴다. Next.js의 rewrites나 Vite의 server.proxy 가 대표적이다. 이는 브라우저가 아닌 개발 서버가 대신 요청을 전달하므로 CORS 검사가 발생하지 않는 원리다. 운영 환경에서는 API 게이트웨이나 리버스 프록시로 프런트와 API를 같은 출처로 묶거나, 서버에 정확한 CORS 헤더를 설정하는 두 가지 방향 중에서 선택한다. 또한 허용 출처를 * 로 열어 두는 것은 인증이 필요한 API에서는 보안상 피해야 하며, 신뢰하는 출처 목록을 서버에서 관리하는 편이 안전하다.

면접에서 이렇게 물어본다

  • 동일 출처 정책이 왜 필요한지 설명하고, 출처가 같다고 판단하는 기준 세 가지를 말해 보라.
  • 프리플라이트 요청은 언제 발생하며, 어떤 헤더가 오가는지 설명해 보라.
  • CORS 에러가 발생했는데 서버 로그에는 요청이 정상 기록되어 있다. 이 현상이 왜 가능한가.
  • 쿠키를 포함한 교차 출처 요청을 허용하려면 서버와 클라이언트에서 각각 무엇을 설정해야 하는가.
  • Access-Control-Allow-Origin에 와일드카드를 쓰면 안 되는 경우는 언제인가.
네트워크 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기