/개발/CORS No Access-Control-Allow-Origin 에러 5분 진단·해결 코드
개발CORS 에러 해결Access-Control-Allow-Origin

CORS No Access-Control-Allow-Origin 에러 5분 진단·해결 코드

'No Access-Control-Allow-Origin header is present' CORS 에러를 Network 탭으로 5분 진단하고 Express·Spring·Nginx 복붙 코드와 preflight OPTIONS, credentials include 함정까지 한 번에 해결합니다.

CORS No Access-Control-Allow-Origin 에러 5분 진단·해결 코드

CORS policy blocked 에러 5분 진단 — No Access-Control-Allow-Origin 해결 코드

지금 콘솔에 빨간 에러가 떠서 검색해 들어오셨죠? 개념 강의는 건너뛰고 바로 진단으로 갑니다. 딱 3줄 요약: **CORS는 "다른 origin(프로토콜+도메인+포트)으로 보낸 요청을 브라우저가 막는 보안 정책"**입니다. 중요한 건, 이 차단을 푸는 헤더는 프론트가 아니라 서버(또는 그 앞단 Nginx)가 내려줘야 한다는 점이에요. 즉 대부분의 CORS 에러는 백엔드 설정 문제입니다.

에러 메시지부터 해부하기

가장 흔한 메시지는 이겁니다.

CODE
Access to fetch at 'https://api.myapp.com/users' from origin 'https://myapp.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

구절별로 읽으면 진단이 끝납니다.

  • Access to fetch at 'https://api.myapp.com/users' → 요청을 보낸 대상 서버
  • from origin 'https://myapp.com' → 요청을 보낸 출처(프론트)
  • No 'Access-Control-Allow-Origin' header is present서버 응답에 허용 헤더가 없다는 핵심 원인

마지막 줄이 결론입니다. 서버가 Access-Control-Allow-Origin을 안 내려줬다는 뜻이니, 고칠 곳은 백엔드예요. 참고로 이 에러가 떠도 서버 로그를 보면 요청은 정상적으로 들어와 200을 반환한 경우가 많습니다. 응답은 도착했지만 브라우저가 헤더가 없어서 JS에 전달하지 않고 버린 것이죠.

5분 진단: Network 탭으로 책임 소재 가르기

개발자도구 → Network 탭을 열고 막힌 요청을 클릭하세요. 아래 체크리스트로 5분 안에 판정합니다.

① preflight(OPTIONS) 행이 보이는가? 요청 목록에 같은 URL의 OPTIONS 메서드 행이 먼저 나타나면 preflight가 발생한 겁니다. preflight는 다음 조건에서 자동으로 붙어요.

  • PUT, DELETE, PATCH 메서드
  • Content-Type: application/json
  • Authorization 같은 커스텀 헤더 추가

위 조건이 하나도 없는 GET/POST(form 형식)는 **단순요청(simple request)**이라 OPTIONS 없이 바로 갑니다.

② Response Headers에 Access-Control-Allow-Origin이 있는가?

③ Status가 무엇인가?

OPTIONS 상태ACAO 헤더판정
OPTIONS 없음 + 본요청 응답에 ACAO 없음없음서버 문제 — 헤더 설정 추가
OPTIONS가 404/500 실패-서버 문제 — OPTIONS 라우트 미처리
OPTIONS 200/204인데 ACAO 없음없음서버 문제 — preflight 응답 헤더 누락
ACAO가 *인데 credentials 사용*함정 (아래 4번 참고)
모든 헤더 정상인데도 막힘정상프론트의 credentials/origin 오타 의심

요약하면 헤더가 없으면 거의 100% 서버 문제입니다. 프론트에서 아무리 fetch 옵션을 바꿔도 안 풀려요.

서버별 해결 코드 복붙

Express (cors 미들웨어)

JavaScript
const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',   // 운영 도메인 명시
  credentials: true,             // 쿠키/인증 헤더 허용 시
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,                 // preflight 캐시 24시간
}));

cors 미들웨어는 OPTIONS preflight에 204 응답을 자동 처리해 줍니다. 여러 origin을 허용하려면 origin에 배열이나 함수를 넣으세요.

Spring Boot

특정 컨트롤러만 열 때는 어노테이션:

JAVA
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }

전역으로 깔끔하게 가려면 Bean 방식을 추천합니다.

JAVA
@Configuration
public class CorsConfig {
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Content-Type","Authorization"));
    config.setAllowCredentials(true);
    config.setMaxAge(86400L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
  }
}

Spring Security를 쓴다면 http.cors(Customizer.withDefaults())를 추가해 위 Bean이 적용되게 해야 합니다. 안 그러면 필터 체인이 OPTIONS를 먼저 막아버려요.

Nginx

Nginx
location /api/ {
    add_header Access-Control-Allow-Origin  "https://myapp.com" 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-Allow-Credentials "true" always;
    add_header Access-Control-Max-Age 86400 always;

    if ($request_method = OPTIONS) {
        return 204;   # preflight는 빈 본문 204로 즉시 응답
    }

    proxy_pass http://backend;
}

핵심은 두 가지입니다. always 플래그가 없으면 4xx/5xx 응답에는 헤더가 안 붙어서 에러 상황에서 CORS가 또 터집니다. 그리고 OPTIONS는 본문 없이 204로 바로 끊어줘야 백엔드까지 안 가고 가볍게 처리돼요.

preflight 응답에 꼭 넣어야 하는 헤더

OPTIONS preflight는 "본요청 보내도 되니?"라고 미리 물어보는 단계입니다. 브라우저는 이 응답을 보고 통과 여부를 결정해요.

  • Access-Control-Allow-Methods: 허용 메서드. 없으면 PUT/DELETE가 막힘
  • Access-Control-Allow-Headers: 허용 커스텀 헤더. Authorization 빠지면 토큰 요청 차단
  • Access-Control-Max-Age: preflight 결과 캐시 시간. 매 요청마다 OPTIONS가 두 번 나가는 걸 줄여줌

OPTIONS는 데이터를 받는 게 아니므로 본문 없이 204가 정석입니다.

단골 함정 두 가지

① credentials: 'include' + 와일드카드(*) 금지

쿠키 기반 인증에서 자주 터지는 조합입니다. 프론트가 이렇게 보내면:

JavaScript
fetch('https://api.myapp.com/me', { credentials: 'include' });

서버가 Access-Control-Allow-Origin: *를 내리는 순간 이런 에러가 납니다.

CODE
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.

해결: 와일드카드 대신 구체 origin을 명시하고 Access-Control-Allow-Credentials: true를 동반해야 합니다. 위 Express/Spring 예시처럼요. 여기에 쿠키 자체도 SameSite=None; Secure 속성이 있어야 크로스 사이트로 전송되니 세트로 챙기세요.

실무 경험 한마디: BFF·마이크로서비스로 프론트와 API를 분리하면서 CORS 이슈가 폭증했습니다. 제 경우 가장 시간을 잡아먹은 건 코드가 아니라 Spring Security 필터가 OPTIONS를 먼저 401로 막던 상황이었어요. Network 탭에서 OPTIONS가 401이면 CORS 설정이 아니라 인증 필터를 의심하세요.

② 로컬은 dev 프록시로 우회 (단, 프로덕션 해결책 아님)

로컬 개발 중이라면 프록시로 같은 origin인 척 만들어 CORS를 아예 우회할 수 있습니다.

Vite (vite.config.js):

JavaScript
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
};

webpack devServer:

JavaScript
devServer: {
  proxy: { '/api': 'http://localhost:8080' },
}

/api로 시작하는 요청을 dev 서버가 대신 백엔드로 보내므로 브라우저 입장에선 동일 origin입니다. 하지만 이건 빌드된 프로덕션엔 적용되지 않습니다. 운영 환경에선 반드시 위의 서버측 CORS 설정으로 풀어야 해요.

결론: 진단 → 해결 치트시트

  1. 에러 메시지 읽기No Access-Control-Allow-Origin = 서버 문제 확정
  2. Network 탭 — OPTIONS 행/ACAO 헤더/Status로 프론트 vs 서버 판정
  3. 서버에 헤더 추가 — Express cors(), Spring CorsConfigurationSource, Nginx add_header ... always
  4. preflight 처리 — OPTIONS는 204, Methods/Headers/Max-Age 세트로
  5. credentials 쓰면 — 와일드카드 금지, 구체 origin + Allow-Credentials true
  6. 로컬은 프록시, 운영은 서버 설정 — 절대 혼동 금지

마지막으로 행동 가이드 하나. 프로덕션에서 Access-Control-Allow-Origin: * 남발은 금지입니다. 당장 에러는 사라지지만 인증이 필요한 API를 아무 사이트나 호출할 수 있게 열어버리는 셈이에요. 허용 origin은 명시적으로 화이트리스트 관리하세요.

자주 묻는 질문 (FAQ)

Q. 서버 로그엔 요청이 200으로 찍히는데 왜 프론트는 CORS 에러가 나나요? A. 응답은 도착했지만 Access-Control-Allow-Origin 헤더가 없어서 브라우저가 JS로 전달하기 직전에 차단한 겁니다. 서버가 정상 동작해도 헤더만 빠지면 에러가 납니다. 서버에 CORS 응답 헤더를 추가하세요.

Q. OPTIONS 요청이 401/404로 실패해요. 어떻게 하나요? A. preflight 단계에서 막힌 겁니다. Spring Security 등 인증 필터가 OPTIONS를 인증 대상으로 처리하거나, 라우터에 OPTIONS 핸들러가 없는 경우입니다. OPTIONS는 인증 예외로 두고 204를 반환하도록 설정하세요.

Q. 로컬 Vite 프록시로는 잘 되는데 배포하면 다시 CORS가 터집니다. A. dev 프록시는 개발 서버에서만 동작하고 빌드 결과물에는 포함되지 않습니다. 운영에서는 백엔드나 Nginx에 직접 CORS 헤더를 설정해야 합니다.

✦ ✦ ✦
편집 검토 · Editorial Review

AI 도구는 자료 조사와 초안 작성의 보조 수단으로 사용될 수 있습니다. Nodelog는 공개 전 내용과 출처를 검토하고, 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서를 함께 확인하도록 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 ·

댓글

첫 번째 댓글을 남겨보세요.