HTTP Content-Type과 Accept 차이
API 요청에 Content-Type: application/json과 Accept: application/json을 함께 넣는 코드를 자주 본다.
값은 같아 보여도 두 헤더가 설명하는 대상은 다르다.
Content-Type은 현재 메시지에 담긴 콘텐츠의 형식을 설명한다.
요청의 Accept는 클라이언트가 응답으로 받을 수 있는 형식을 서버에 알린다.
두 헤더의 방향
HTTP 요청과 응답에는 각각 헤더와 콘텐츠가 있을 수 있다.Content-Type은 자신이 포함된 요청이나 응답의 실제 콘텐츠를 가리킨다.
일반적인 API 호출에서 Accept는 요청에 포함되며 앞으로 받을 응답의 형식에 대한 선호를 가리킨다.
요청 본문의 형식을 설명하지 않는다.
RFC 9110은 서버가 응답에 Accept를 넣어 같은 리소스로 이어질 요청 콘텐츠에서 선호하는 미디어 타입을 알리는 용도도 정의한다.
이 글에서는 웹 API에서 가장 흔한 요청 헤더 용법을 기준으로 비교한다.
요청 메시지는 다음과 같다.
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{"name":"Dohyun"}응답 메시지는 다음과 같다.
HTTP/1.1 201 Created
Content-Type: application/json
{"id":42,"name":"Dohyun"}요청은 JSON 본문을 보내고 JSON 응답을 원한다는 뜻이다.
응답은 실제로 JSON 콘텐츠를 보냈다고 설명한다.
요청 본문을 보내지 않는 일반적인 GET에서는 요청 Content-Type이 필요하지 않은 경우가 많지만 Accept는 여전히 응답 선호를 전달할 수 있다.
method와 body의 기본 기준은 HTTP GET과 POST 차이에서 더 자세히 볼 수 있다.
Content-Type의 역할
요청의 Content-Type은 서버가 본문을 어떤 방식으로 해석해야 하는지 알려준다.
JSON 문자열에는 application/json, 일반 HTML 폼 전송에는 application/x-www-form-urlencoded, 파일을 포함한 폼에는 multipart/form-data가 주로 사용된다.
응답의 Content-Type은 클라이언트가 받은 표현의 실제 형식을 설명한다.
요청과 응답의 Content-Type은 서로 독립적이다.
서버가 JSON 요청을 받은 뒤 HTML이나 빈 응답을 보낼 수도 있으므로 각 메시지의 실제 콘텐츠에 맞춰야 한다.
Accept의 역할
Accept는 서버가 같은 리소스를 여러 형식으로 제공할 수 있을 때 콘텐츠 협상의 기준이 된다.
클라이언트는 쉼표로 여러 media type을 보내고 q 값으로 상대적인 선호도를 나타낼 수 있다.
Accept: text/html, application/json;q=0.8, */*;q=0.1이 값은 HTML을 가장 선호하고, JSON은 그보다 낮은 우선순위로 허용하며, 다른 형식도 낮은 우선순위로 받을 수 있다는 뜻이다.*/*는 모든 media type을 가리킨다.
Accept가 없으면 클라이언트가 응답의 media type에 별도 선호를 두지 않은 것으로 해석할 수 있다.
서버가 Accept에 따라 응답을 바꾼다면 캐시가 형식별 응답을 구분하도록 Vary: Accept도 함께 고려해야 한다.
JSON 요청과 응답
Fetch API로 JSON을 보낼 때는 객체를 문자열로 변환하고 요청 본문의 형식을 명시한다.
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ name: 'Dohyun' }),
})
const user = await response.json()Content-Type을 적는 것만으로 JavaScript 객체가 JSON 문자열로 바뀌지는 않는다.JSON.stringify()가 요청 본문을 직렬화하고, 헤더는 그 결과의 형식을 설명한다.
반대로 response.json()은 응답 본문을 읽고 JSON으로 파싱하는 동작이다.
HTTP 요청 도구의 처리 차이는 fetch와 axios 차이에서 이어서 볼 수 있다.
상태 코드와 구현 주의점
서버가 요청의 Content-Type을 지원하지 않으면 415 Unsupported Media Type으로 응답할 수 있다.
JSON만 받는 API에 text/plain 본문을 보낸 경우가 여기에 가깝다.
서버가 Accept 조건에 맞는 응답을 만들 수 없고 해당 선호를 무시한 응답도 보내지 않기로 했다면 406 Not Acceptable을 사용할 수 있다.
두 상태를 구분하면 요청 본문의 문제인지 응답 협상의 문제인지 빠르게 찾을 수 있다.
FormData를 보낼 때는 Content-Type: multipart/form-data를 직접 지정하지 않아야 한다.
const formData = new FormData()
formData.append('avatar', file)
await fetch('/api/profile', {
method: 'POST',
body: formData,
})브라우저가 각 필드를 나누는 boundary를 포함한 Content-Type을 자동으로 만든다.
개발자가 헤더를 직접 덮어쓰면 boundary가 빠져 서버가 본문을 해석하지 못할 수 있다.
교차 출처 요청에서 application/json 같은 Content-Type은 preflight 조건에도 영향을 줄 수 있다.
이 경우 헤더를 지우는 방식보다 서버의 허용 설정을 CORS 에러와 해결 기준에 맞춰 확인해야 한다.
참고 자료























