CORS policy blocked 에러 5분 진단 — No Access-Control-Allow-Origin 해결 코드
지금 콘솔에 빨간 에러가 떠서 검색해 들어오셨죠? 개념 강의는 건너뛰고 바로 진단으로 갑니다. 딱 3줄 요약: **CORS는 "다른 origin(프로토콜+도메인+포트)으로 보낸 요청을 브라우저가 막는 보안 정책"**입니다. 중요한 건, 이 차단을 푸는 헤더는 프론트가 아니라 서버(또는 그 앞단 Nginx)가 내려줘야 한다는 점이에요. 즉 대부분의 CORS 에러는 백엔드 설정 문제입니다.
에러 메시지부터 해부하기
가장 흔한 메시지는 이겁니다.
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/jsonAuthorization같은 커스텀 헤더 추가
위 조건이 하나도 없는 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 미들웨어)
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
특정 컨트롤러만 열 때는 어노테이션:
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }전역으로 깔끔하게 가려면 Bean 방식을 추천합니다.
@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
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' + 와일드카드(*) 금지
쿠키 기반 인증에서 자주 터지는 조합입니다. 프론트가 이렇게 보내면:
fetch('https://api.myapp.com/me', { credentials: 'include' });서버가 Access-Control-Allow-Origin: *를 내리는 순간 이런 에러가 납니다.
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):
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};webpack devServer:
devServer: {
proxy: { '/api': 'http://localhost:8080' },
}/api로 시작하는 요청을 dev 서버가 대신 백엔드로 보내므로 브라우저 입장에선 동일 origin입니다. 하지만 이건 빌드된 프로덕션엔 적용되지 않습니다. 운영 환경에선 반드시 위의 서버측 CORS 설정으로 풀어야 해요.
결론: 진단 → 해결 치트시트
- 에러 메시지 읽기 —
No Access-Control-Allow-Origin= 서버 문제 확정 - Network 탭 — OPTIONS 행/ACAO 헤더/Status로 프론트 vs 서버 판정
- 서버에 헤더 추가 — Express
cors(), SpringCorsConfigurationSource, Nginxadd_header ... always - preflight 처리 — OPTIONS는 204, Methods/Headers/Max-Age 세트로
- credentials 쓰면 — 와일드카드 금지, 구체 origin + Allow-Credentials true
- 로컬은 프록시, 운영은 서버 설정 — 절대 혼동 금지
마지막으로 행동 가이드 하나. 프로덕션에서 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 헤더를 설정해야 합니다.
AI 도구는 자료 조사와 초안 작성의 보조 수단으로 사용될 수 있습니다. Nodelog는 공개 전 내용과 출처를 검토하고, 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서를 함께 확인하도록 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.