CORS 에러 해결 — Access-Control-Allow-Origin 완벽 정리 (No 'Access-Control-Allow-Origin' header)
증상: CORS 에러 메시지를 만났을 때
브라우저 콘솔에 다음과 같은 빨간 메시지가 뜨면 그것이 바로 CORS 에러입니다.
Access to fetch at 'https://api.example.com/users' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.핵심은 두 가지입니다. 첫째, 이 에러는 브라우저가 막은 것입니다. 서버는 정상적으로 응답을 보냈을 수도 있습니다. 둘째, 원인은 응답에 Access-Control-Allow-Origin 헤더가 없거나, 그 값이 내 출처(origin)와 맞지 않기 때문입니다.
왜 CORS 에러가 나는가
CORS(Cross-Origin Resource Sharing)는 한 출처(scheme + host + port)의 웹 페이지가 다른 출처의 리소스를 가져올 때 적용되는 브라우저 보안 정책입니다. 예를 들어 http://localhost:3000에서 http://localhost:8080의 API를 부르면, 포트가 달라 '다른 출처'로 간주됩니다.
흔한 원인 4가지
- 서버가 Allow-Origin 헤더를 아예 안 보냄 — 가장 흔합니다. 서버에 CORS 설정이 없는 경우.
- Origin 값 불일치 — 서버가
https://prod.com만 허용하는데 로컬에서 호출. - Preflight(OPTIONS) 요청 실패 —
Content-Type: application/json이나 커스텀 헤더를 쓰면 브라우저가 먼저 OPTIONS 요청을 보내는데, 서버가 이를 처리하지 못함. - credentials(쿠키) 사용 시 와일드카드 충돌 —
Allow-Origin: *와 쿠키 전송은 동시에 쓸 수 없습니다.
케이스별 해결
1) 정석: 서버에서 CORS 헤더를 허용
해결의 원칙은 "서버가 응답 헤더에 Access-Control-Allow-Origin을 추가"하는 것입니다. 프론트에서는 끌 수 없습니다.
Express (Node.js)
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000', // 허용할 출처
credentials: true // 쿠키 쓸 때만
}));Spring Boot
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class UserController { ... }Nginx (리버스 프록시)
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always;2) Preflight(OPTIONS) 처리
JSON 본문을 POST하면 브라우저가 본 요청 전에 OPTIONS 요청을 먼저 던집니다. 서버는 이 OPTIONS에 200/204와 함께 Access-Control-Allow-Methods, Access-Control-Allow-Headers를 응답해야 합니다. cors 미들웨어는 이를 자동 처리하지만, 직접 라우팅한다면 OPTIONS 메서드를 빠뜨리지 않았는지 확인하세요.
3) 개발 단계: 프록시로 우회
서버를 못 고치는 상황(외부 API 등)이라면 개발 서버의 프록시를 씁니다. 같은 출처처럼 보이게 만들어 CORS 자체를 회피합니다.
// vite.config.js
export default {
server: {
proxy: { '/api': 'http://localhost:8080' }
}
}4) credentials와 와일드카드 충돌
쿠키나 인증 헤더를 보내려면 프론트에서 credentials: 'include'를 쓰는데, 이때 서버의 Allow-Origin은 *가 아니라 정확한 출처 문자열이어야 하고 Access-Control-Allow-Credentials: true도 필요합니다.
원인별 비교
| 증상 | 원인 | 해결 |
|---|---|---|
| No 'Access-Control-Allow-Origin' header | 서버에 CORS 설정 없음 | 서버에서 Allow-Origin 추가 |
| OPTIONS 요청만 실패 | Preflight 미처리 | OPTIONS 응답 + Allow-Methods/Headers |
| credentials와 '*' 동시 사용 거부 | 와일드카드+쿠키 충돌 | 출처 명시 + Allow-Credentials: true |
| 로컬은 되는데 배포만 실패 | 배포 출처 미허용 | 운영 도메인을 origin에 추가 |
예방
- API 서버 설계 시 처음부터 허용 출처 목록을 환경변수로 관리하세요.
Allow-Origin: *남발은 보안 위험. 필요한 출처만 명시.- 로컬/스테이징/운영 출처를 각각 화이트리스트에 등록.
- 네트워크 탭에서 응답 헤더를 직접 확인하는 습관을 들이세요.
자주 묻는 질문
Q1. 프론트엔드 코드만 고쳐서 CORS를 끌 수 있나요?
아니요. CORS는 브라우저가 서버 응답 헤더를 보고 판단합니다. 근본 해결은 서버에서 헤더를 추가하는 것이며, 프론트에서는 개발용 프록시로 우회만 가능합니다.
Q2. Postman에서는 잘 되는데 브라우저에서만 CORS 에러가 나요.
정상입니다. CORS는 브라우저만 적용하는 정책입니다. Postman·curl은 CORS를 검사하지 않으므로, 브라우저 에러는 서버 헤더 문제로 보면 됩니다.
Q3. Allow-Origin을 '*'로 했는데도 쿠키가 안 가요.
와일드카드와 credentials는 함께 쓸 수 없습니다. Allow-Origin에 정확한 출처를 적고 Access-Control-Allow-Credentials: true를 추가하세요.