레이블이 cors인 게시물을 표시합니다. 모든 게시물 표시
레이블이 cors인 게시물을 표시합니다. 모든 게시물 표시

목요일

CORS 에러 완전 정복 — 원인부터 해결까지 (초보자~실무자)

개발하다 보면 누구나 한 번쯤은 마주치는 그 빨간 에러.

Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

처음 보면 황당하다. 코드는 분명히 맞는데, 브라우저가 요청을 막는다. 이 글에서는 CORS가 왜 생기는지, 어떻게 해결하는지를 레벨별로 정리한다.


1. CORS가 뭔데?

CORS (Cross-Origin Resource Sharing) — 직역하면 "교차 출처 리소스 공유".

브라우저는 기본적으로 Same-Origin Policy(동일 출처 정책)를 따른다. 즉, http://localhost:3000에서 실행 중인 페이지가 https://api.example.com에 요청을 보내면, 출처(origin)가 다르기 때문에 브라우저가 막는다.

여기서 Origin = 프로토콜 + 도메인 + 포트 세 가지가 모두 같아야 동일 출처다.

비교동일 출처?
http://example.com vs https://example.com❌ 프로토콜 다름
http://example.com vs http://api.example.com❌ 도메인 다름
http://example.com:3000 vs http://example.com:8080❌ 포트 다름
http://example.com/a vs http://example.com/b✅ 동일 출처

2. 핵심 원리 — 브라우저가 하는 일

중요한 포인트: CORS는 서버가 막는 게 아니라 브라우저가 막는다.

서버는 이미 응답을 줬다. 하지만 브라우저가 그 응답에 Access-Control-Allow-Origin 헤더가 없으면 자바스크립트 코드에 넘겨주지 않고 차단한다. curl이나 Postman으로는 에러 없이 잘 되는 이유가 이것이다.

Preflight 요청

GET, POST 외의 메서드거나, 커스텀 헤더가 있거나, Content-Type이 application/json이면 브라우저는 실제 요청 전에 OPTIONS 메서드로 사전 확인 요청(Preflight)을 먼저 보낸다.


3. 해결 방법

🟢 초보자 — 로컬 개발 환경

프론트엔드 개발 서버 프록시 설정 (가장 흔한 방법)

CRA (package.json):

{
  "proxy": "https://api.example.com"
}

Vite (vite.config.ts):

export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

🟡 중급자 — 서버 사이드 CORS 헤더 설정

Node.js (Express):

const cors = require('cors');

app.use(cors({
  origin: ['https://myapp.com', 'https://www.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}));

Spring Boot (Java):

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://myapp.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}

Nginx:

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Length' 0;
        return 204;
    }
    proxy_pass http://backend;
}

🔴 고급 — 자주 빠지는 함정들

① credentials: true인데 * 와일드카드 쓰면 안 된다

쿠키나 Authorization 헤더 포함 요청은 Access-Control-Allow-Origin: *이어도 브라우저가 차단한다. 정확한 출처를 명시해야 한다.

# 틀린 예
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

# 맞는 예
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true

② Preflight OPTIONS 요청 처리를 빠뜨리는 경우

수동으로 헤더를 추가할 때 OPTIONS 처리를 생략하는 실수가 많다. 204를 반환해야 본 요청이 들어온다.

③ 여러 서버에서 헤더가 중복으로 붙는 경우

Access-Control-Allow-Origin이 두 개 붙으면 브라우저가 에러를 낸다. 한 레이어에서만 처리해야 한다.

④ 동적 출처 허용

const allowedOrigins = ['https://myapp.com', 'https://app2.com'];
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  next();
});

4. 상황별 체크리스트

상황해결책
로컬 개발 중 API 서버 연결 안 됨프론트 dev 서버 프록시 설정
Postman은 되는데 브라우저만 에러서버에 CORS 헤더 추가
OPTIONS 요청이 405 에러Preflight 핸들러 추가
쿠키/JWT 포함 요청이 차단됨* 대신 정확한 origin + credentials: true
배포 후 갑자기 안 됨Nginx/로드밸런서 헤더 중복 확인
서드파티 API (서버 못 건드림)내 서버를 프록시로 우회

마무리

CORS는 처음엔 황당하지만 원리를 알면 단순하다. 브라우저가 보안 정책으로 막는 것이고, 서버가 헤더로 허용해줘야 풀린다. Postman은 되는데 브라우저에서만 안 된다면 거의 무조건 CORS 문제다.

운영 환경에서는 절대 * 와일드카드 쓰지 말고, 꼭 필요한 출처만 명시하는 것이 기본이다.