Content-Type과 CORS 오류

badahertz52·2025년 6월 21일
1

web

목록 보기
3/3
post-thumbnail

🤯 이슈 발생: 예상치 못한 CORS 에러

프로젝트를 진행하던 중, 작지만 당황스러운 문제를 마주했습니다. 사용자 ID를 받아서 처리하는 간단한 POST API를 호출하는데 예상치 못한 CORS 에러가 발생한 것입니다.

// 문제가 된 코드
axios.post('/api/users', {}, {
  params: { userId: 123 }
});

GET 요청에서는 params로 쿼리 파라미터를 문제없이 전달할 수 있었는데, POST에서는 왜 CORS 에러가 발생하는지 이해할 수 없었습니다. 단순히 해결책을 찾는 것에서 그치지 않고, 왜 이런 문제가 발생했는지 근본 원인을 파악해보기로 했습니다.

🥸 원인 분석: axios의 자동 헤더 설정과 서버 CORS 정책의 충돌

1. axios의 자동 Content-Type 설정

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을 설정했습니다.

2. 서버 CORS 정책과의 충돌

핵심 문제: 서버에서 허락하지 않은 Content-Type이었습니다.

// 실제 전송된 요청
POST /api/users?userId=123
Content-Type: application/json  ← 서버가 허락하지 않은 타입!
Body: {}

post메서드에 application/json을 사용했기 때문에, complext request가 되었고 preflight 요청에서 서버가 해당 Content-Type을 허용하지 않아서 CORS 오류가 발생했습니다.

해결책: URLSearchParams를 통한 Simple Request 만들기

서버에서 허락하는 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 조건 만족
  • preflight 요청 없이 바로 전송되어 CORS 문제 회피
  • 서버에서 ID를 쿼리 파라미터로 요청하고 있으며, URLSearchParams를 사용하면 별도로 Content-Type을 지정하지 않아도 axios에서 Simple Request가 가능한 헤더로 자동 설정

✅ Simple Request 조건
1. 허용된 메서드: GET, HEAD, POST
2. 허용된 헤더: 사용자 정의 헤더 없음 (몇 가지 예외 제외)
3. 허용된 Content-Type:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

심화 학습:JSON vs Form-encoded, CORS 관점에서의 차이

현대 웹 개발에서는 RESTful API가 표준이 되었고, JSON이 데이터 교환의 주요 형식입니다. 하지만 CORS 관점에서 보면 흥미로운 차이가 있습니다.

구분Simple RequestModern RESTful API
Content-Typeapplication/x-www-form-urlencodedapplication/json
데이터 형식key1=value1&key2=value2{"key1": "value1", "key2": "value2"}
웹 등장 시기1990년대 (HTML 폼)2000년대 (AJAX 시대)
CORS 분류Simple RequestComplex Request
Preflight 필요성❌ 불필요✅ 필요 (OPTIONS 요청)

왜 application/json은 preflight를 일으키는가?

핵심 이유: 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의 차이가 생긴 배경을 알게된 값진 경험이었습니다.

앞으로는 단순히 "작동하는 코드"를 넘어서, 웹의 역사와 브라우저 보안 정책을 이해하고 상황에 맞는 최적의 선택을 할 수 있는 개발자가 되고 싶습니다.


참고 자료

profile
세상과 사람을 잇는 개발을 꿈꾸는 프론트엔드 개발자

0개의 댓글