프로젝트를 진행하던 중, 작지만 당황스러운 문제를 마주했습니다. 사용자 ID를 받아서 처리하는 간단한 POST API를 호출하는데 예상치 못한 CORS 에러가 발생한 것입니다.
// 문제가 된 코드
axios.post('/api/users', {}, {
params: { userId: 123 }
});
GET 요청에서는 params
로 쿼리 파라미터를 문제없이 전달할 수 있었는데, POST에서는 왜 CORS 에러가 발생하는지 이해할 수 없었습니다. 단순히 해결책을 찾는 것에서 그치지 않고, 왜 이런 문제가 발생했는지 근본 원인을 파악해보기로 했습니다.
Axios 공식 문서에는 다음과 같이 명시되어 있습니다:
"Setting the Content-Type header is not required as Axios guesses it based on the payload type"
Axios는 전달받은 data
의 타입에 따라 자동으로 Content-Type을 설정해주는 기능이 있습니다. 실제 소스코드를 확인해보면:
// axios transformRequest 로직 (실제 소스코드 기반)
transformRequest: [function transformRequest(data, headers) {
// 일반 객체 체크 - 여기서 문제 발생!
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json');
return JSON.stringify(data);
}
return data;
}]
빈 객체 {}
도 "객체"이므로 axios가 자동으로 Content-Type: application/json
을 설정했습니다.
핵심 문제: 서버에서 허락하지 않은 Content-Type이었습니다.
// 실제 전송된 요청
POST /api/users?userId=123
Content-Type: application/json ← 서버가 허락하지 않은 타입!
Body: {}
post메서드에 application/json
을 사용했기 때문에, complext request가 되었고 preflight 요청에서 서버가 해당 Content-Type을 허용하지 않아서 CORS 오류가 발생했습니다.
서버에서 허락하는 Content-Type으로 변경하는 것이 해결책이었습니다.
// 해결 코드
const formData = new URLSearchParams({ userId: 123 });
axios.post('/api/users', formData);
// 실제 전송되는 요청
// POST /api/users
// Content-Type: application/x-www-form-urlencoded ← 서버가 허락하는 타입!
// Body: userId=123
URLSearchParams를 선택한 이유:
application/x-www-form-urlencoded
를 사용하면 Simple Request 조건 만족✅ Simple Request 조건
1. 허용된 메서드: GET
, HEAD
, POST
2. 허용된 헤더: 사용자 정의 헤더 없음 (몇 가지 예외 제외)
3. 허용된 Content-Type:
application/x-www-form-urlencoded
multipart/form-data
text/plain
현대 웹 개발에서는 RESTful API가 표준이 되었고, JSON이 데이터 교환의 주요 형식입니다. 하지만 CORS 관점에서 보면 흥미로운 차이가 있습니다.
구분 | Simple Request | Modern RESTful API |
---|---|---|
Content-Type | application/x-www-form-urlencoded | application/json |
데이터 형식 | key1=value1&key2=value2 | {"key1": "value1", "key2": "value2"} |
웹 등장 시기 | 1990년대 (HTML 폼) | 2000년대 (AJAX 시대) |
CORS 분류 | Simple Request | Complex Request |
Preflight 필요성 | ❌ 불필요 | ✅ 필요 (OPTIONS 요청) |
핵심 이유: JSON은 브라우저 관점에서 "새로운 기능"이기 때문입니다.
MDN CORS 문서에 따르면, Simple Request는 HTML 4.0의 <form>
요소가 이미 동일한 종류의 요청을 30년간 가능하게 했기 때문에 굳이 서버에게 preflight를 구할 필요가 없다는 것입니다.
반면, JSON은 2000년대 XMLHttpRequest 등장과 함께 나온 것으로, 브라우저가 "안전성이 검증되지 않은 새로운 방식"으로 분류합니다. 따라서 preflight를 통해 서버가 허락한 형식인지 확인하는 절차가 필요합니다.
// ✅ Simple Request 허용 Content-Type (1990년대부터 존재)
- application/x-www-form-urlencoded // HTML 폼의 기본값
- multipart/form-data // 파일 업로드용
- text/plain // 단순 텍스트
// ❌ Complex Request 유발 Content-Type (2000년대 이후 등장)
- application/json // AJAX/XMLHttpRequest 시대 산물
- application/xml // API 통신용
- 기타 커스텀 타입들
단순한 CORS 에러에서 시작해 Simple Request와 Complex Request간 preflight의 차이가 생긴 배경을 알게된 값진 경험이었습니다.
앞으로는 단순히 "작동하는 코드"를 넘어서, 웹의 역사와 브라우저 보안 정책을 이해하고 상황에 맞는 최적의 선택을 할 수 있는 개발자가 되고 싶습니다.