1. 라이브러리
  2. HTTP와 웹
  3. HTTP 헤더

업데이트됨 1개월 전

모든 HTTP 응답에는 브라우저가 무언가 유용한 작업을 하기 전에 반드시 답해야 할 질문이 담겨 있습니다: "내가 지금 보고 있는 게 뭐지?"

Content-Type 헤더가 그 답을 제공합니다. 이것이 없으면, 사진으로 렌더링되어야 할 동일한 바이트가 의미 없는 쓰레기 데이터가 되거나, 더 나쁜 경우 브라우저가 실행할 것으로 예상하지 못했던 실행 코드가 됩니다.

Content-Type이 실제로 하는 일

Content-Type은 전송되는 데이터의 미디어 타입(MIME 타입이라고도 함)을 지정합니다. 요청과 응답 모두에 사용됩니다:

응답에서는 브라우저에게 수신한 내용을 어떻게 해석할지 알려줍니다:

Content-Type: text/html; charset=utf-8

요청에서는 서버에게 전송하는 내용을 어떻게 파싱할지 알려줍니다:

Content-Type: application/json

이것을 잘못 설정하면 문제가 조용히 발생합니다. 잘못된 Content-Type으로 JSON을 전송하면 서버가 파싱하지 못합니다. text/html로 이미지를 제공하면 브라우저가 이진 데이터를 웹페이지로 렌더링하려고 시도하는 것을 보게 됩니다.

MIME 타입 구조

MIME는 "Multipurpose Internet Mail Extensions"의 약자로, 원래 이메일을 위해 설계되었다가 HTTP에 채택되었습니다. 형식은 간단합니다:

타입/서브타입

타입은 큰 범주이고, 서브타입은 구체적인 형식입니다:

  • text/html — 텍스트 범주, HTML 형식
  • image/png — 이미지 범주, PNG 형식
  • application/json — 애플리케이션 범주, JSON 형식

일부 MIME 타입에는 매개변수가 포함됩니다:

text/html; charset=utf-8
multipart/form-data; boundary=----WebKitFormBoundary

자주 쓰이는 MIME 타입

텍스트

MIME 타입용도
text/htmlHTML 웹페이지
text/plain마크업 없는 일반 텍스트
text/cssCSS 스타일시트
text/javascriptJavaScript 코드

애플리케이션

MIME 타입용도
application/jsonJSON 데이터 (현대 API의 표준)
application/xmlXML 데이터
application/pdfPDF 문서
application/zipZIP 압축 파일
application/octet-stream일반 이진 데이터 (특정 타입을 알 수 없을 때)
application/x-www-form-urlencodedURL 매개변수로 인코딩된 폼 데이터

이미지

MIME 타입용도
image/pngPNG 이미지
image/jpegJPEG 이미지
image/gifGIF 이미지
image/svg+xmlSVG 벡터 그래픽
image/webpWebP (현대적이고 효율적인 형식)

동영상 및 오디오

MIME 타입용도
video/mp4MP4 동영상
video/webmWebM 동영상
audio/mpegMP3 오디오
audio/wavWAV 오디오

멀티파트

multipart/form-data는 특별합니다. 단일 요청 내에서 다양한 콘텐츠 타입을 혼합할 수 있게 해주며, 텍스트 필드와 파일 업로드를 모두 포함하는 폼에 필수적입니다:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

문자 인코딩

텍스트 콘텐츠의 경우, charset 매개변수는 문자가 어떻게 인코딩되는지를 지정합니다:

Content-Type: text/html; charset=utf-8

항상 UTF-8을 사용하세요. 모든 언어의 모든 문자를 지원하고, ASCII와 하위 호환되며, 보편적인 표준입니다. ISO-8859-1이나 Shift_JIS 같은 다른 인코딩은 레거시 호환성을 위해 존재하지만, 오래된 시스템을 유지 보수하는 경우가 아니라면 UTF-8이 정답입니다.

charset을 생략하면 브라우저가 추측해야 하고, 추측은 문자가 깨지는 결과로 이어집니다. 국제 문자가 물음표나 의미 없는 기호로 변하는, 악명 높은 "문자 깨짐(mojibake)" 현상이 바로 그것입니다.

콘텐츠 협상

클라이언트는 Accept 헤더를 사용하여 특정 형식을 요청할 수 있습니다:

Accept: application/json, application/xml;q=0.9, */*;q=0.8

q 매개변수는 선호도(0~1)를 나타냅니다. 여기서 JSON이 가장 선호되고(암묵적 q=1.0), XML도 허용 가능하며(q=0.9), 그 외 무엇이든 최후의 수단입니다(q=0.8).

서버는 이를 검토하여 가장 적합한 형식으로 응답합니다. 이를 통해 단일 API 엔드포인트가 각 클라이언트의 요청에 따라 웹 앱에는 JSON으로, 레거시 시스템에는 XML로 응답할 수 있습니다.

잘못 설정하면 위험한 이유

보안 취약점

잘못 표시된 콘텐츠는 브라우저를 의도치 않은 공범으로 만들 수 있습니다:

  1. 공격자가 악성 JavaScript가 포함된 HTML 파일을 업로드합니다
  2. 서버가 이를 저장하는데, 아마 "profile-picture.jpg"로 저장될 수 있습니다
  3. 서버가 Content-Type: text/html로 해당 파일을 제공합니다
  4. 브라우저가 JavaScript를 순순히 실행합니다—XSS 공격 완료

X-Content-Type-Options: nosniff 헤더는 브라우저가 Content-Type을 재추측하는 것을 방지합니다. 이 헤더를 사용하면 브라우저가 설정된 Content-Type을 그대로 신뢰합니다. 그러니 반드시 올바르게 설정해야 합니다.

눈에 띄지 않는 API 오류

이것은 디버깅할 때 가장 흔히 만나는 골치거리 중 하나입니다:

// 이것은 아무 오류 없이 실패합니다—서버가 쓰레기 데이터를 받습니다
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'text/plain'  // 잘못됨!
    },
    body: JSON.stringify({ name: 'Alice' })
});

// 이것은 작동합니다—Content-Type이 데이터와 일치합니다
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'Alice' })
});

서버는 Content-Type을 보고 본문을 어떻게 파싱할지 결정합니다. 일치하지 않으면 데이터가 의미 없는 내용으로 도착합니다.

다운로드와 화면 표시가 깨지는 경우

브라우저는 전적으로 Content-Type을 기반으로 무엇을 할지 결정합니다:

  • text/html → 웹페이지로 파싱하고 렌더링
  • application/json → 원시 텍스트로 표시 (또는 다운로드 제공)
  • image/png → 인라인 이미지로 렌더링
  • application/pdf → PDF 뷰어 열기
  • application/octet-stream → 다운로드 프롬프트 표시

text/html로 PDF를 제공하면 브라우저가 이진 데이터를 웹페이지로 렌더링하려고 합니다. 화면 가득 알 수 없는 문자들이 뜨겠죠.

파일 확장자는 (HTTP에서) 중요하지 않습니다

.jpg, .pdf, .html 같은 파일 확장자는 운영 체제를 위한 규칙입니다. HTTP는 신경 쓰지 않습니다. Content-Type 헤더만이 유일한 진실입니다.

vacation.jpg라는 파일이 Content-Type: application/pdf로 제공되면 PDF로 열립니다. malware.exe라는 파일이 Content-Type: image/png로 제공되면 (깨진) 이미지로 표시됩니다.

그래서 이런 규칙을 지키는 것이 좋습니다: 확장자를 Content-Type과 일치시키세요. HTTP가 요구하기 때문이 아니라, 시스템을 유지 보수하는 사람들이 고마워할 것이기 때문입니다.

사용자 정의 MIME 타입

애플리케이션은 일반적으로 application/vnd. 접두사(벤더별)로 사용자 정의 타입을 정의할 수 있습니다:

Content-Type: application/vnd.github.v3+json

이 GitHub API 예제는 벤더(GitHub), 버전(v3), 기반 형식(JSON)을 모두 하나의 타입으로 인코딩합니다. API 버전 관리와 독점 형식에 유용합니다.

Content-Type 문제 디버깅

브라우저 개발자 도구로 이를 쉽게 확인할 수 있습니다:

  1. 개발자 도구 열기 (F12)
  2. 네트워크 탭으로 이동
  3. 요청 클릭
  4. 헤더 섹션 확인

요청의 Content-Type(전송했다고 선언한 것)과 응답의 Content-Type(서버가 반환했다고 선언한 것)을 모두 볼 수 있습니다. 무언가 이유 없이 작동하지 않을 때, 정답은 종종 여기에 숨어 있습니다.

Content-Type과 MIME 타입에 관한 자주 묻는 질문

Content-Type 헤더를 설정하지 않으면 어떻게 되나요?

브라우저는 "MIME 스니핑"을 통해 추측을 시도합니다. 실제 바이트를 검사하여 타입을 추론하는 것이죠. 이는 신뢰할 수 없고 위험합니다. <script> 태그가 포함된 텍스트 파일이 HTML로 추측되어 실행될 수 있습니다. 항상 Content-Type을 명시적으로 설정하세요.

JSON API가 "undefined"를 반환하거나 파싱에 실패하는 이유는 무엇인가요?

대개 Content-Type 불일치 때문입니다. Content-Type: text/plain으로 JSON을 전송하거나, 서버가 Content-Type: application/json 없이 응답하고 있는 것입니다. 개발자 도구에서 요청과 응답 양쪽 모두 확인하세요.

application/javascript와 text/javascript의 차이는 무엇인가요?

text/javascript는 원래 타입이고, application/javascript는 RFC 4329에 따른 기술적으로 올바른 타입입니다. 둘 다 어디서나 작동합니다. 사용하는 도구가 생성하는 것을 그대로 쓰면 됩니다. 이건 굳이 싸울 가치가 없는 문제입니다.

파일 다운로드에 application/octet-stream을 사용해야 하나요?

application/octet-stream은 "이것이 무엇인지 모르겠다"는 타입으로, 다운로드 프롬프트를 발생시킵니다. 파일 타입을 정말 모를 때만 사용하세요. 그렇지 않다면 구체적인 타입(application/pdf, image/png 등)을 사용하고, Content-Disposition 헤더로 다운로드 동작을 제어하세요.

이 페이지가 도움이 되었나요?

😔
🤨
😃