1. Aklatan
  2. HTTP와 웹
  3. HTTP 헤더

Na-update 1 buwan ang nakalipas

서버에 무언가를 요청하면, 응답은 두 부분으로 옵니다. 본문은 요청한 것 자체입니다 — HTML, 이미지, JSON 데이터. 헤더는 서버가 방금 전달한 것이 무엇인지, 그리고 그것으로 무엇을 해야 하는지를 알려주는 부분입니다.

택배를 받는 것으로 생각해보세요. 택배 내용물이 본문입니다. 헤더는 바깥에 붙은 취급 안내입니다: "파손 주의." "개봉 후 냉장 보관." "선물입니다 — 가격표를 보여주지 마세요."

모든 응답이 답하는 질문들

모든 HTTP 응답은 암묵적으로 일련의 질문에 답합니다:

이게 뭔가요? Content-Type 헤더는 브라우저에게 HTML인지, JSON인지, 이미지인지, PDF인지 알려줍니다. 없으면 브라우저는 바이트 스트림을 받고도 어떻게 해석해야 할지 모릅니다.

크기가 얼마나 되나요? Content-Length는 브라우저가 다운로드 진행 상황을 표시하고 모든 것을 수신했는지 알 수 있게 해줍니다.

압축되어 있나요? Content-Encoding은 브라우저에게 사용 전에 응답을 압축 해제하라고 알려줍니다.

캐싱할 수 있나요? Cache-Control과 관련 헤더들은 브라우저와 CDN이 복사본을 저장할 수 있는지, 얼마나 오래 저장할 수 있는지를 결정합니다.

어떤 보안 규칙이 적용되나요? Content-Security-Policy와 Strict-Transport-Security 같은 헤더들은 브라우저에게 무엇을 차단하고 무엇을 강제할지 알려줍니다.

쿠키를 저장해야 하나요? Set-Cookie 헤더는 요청 간 지속적인 상태를 만듭니다.

다른 곳으로 가야 하나요? Location 헤더는 (리다이렉트 상태 코드와 함께) 브라우저에게 다른 URL을 요청하라고 알려줍니다.

모든 응답 헤더는 이러한 질문 중 하나에 대한 답입니다.

콘텐츠 헤더: 무엇을 받는 건가요

Content-Type

가장 기본적인 응답 헤더입니다. 브라우저에게 방금 받은 바이트를 어떻게 해석할지 알려줍니다:

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

Content-Type이 없으면 브라우저는 바이트 스트림을 받고도 무엇을 보고 있는지 알 수 없습니다. 렌더링할 HTML인가요? 파싱할 JSON인가요? 표시할 이미지인가요? 바이트 자체는 아무 말도 해주지 않습니다.

일반적인 값들:

  • text/html — 웹페이지로 렌더링
  • application/json — 구조화된 데이터로 파싱
  • image/png — 이미지로 표시
  • application/pdf — PDF로 표시하거나 다운로드
  • text/css — 스타일시트로 적용
  • application/javascript — 코드로 실행

charset=utf-8 부분은 문자 인코딩을 지정합니다. 이것이 잘못되면 텍스트가 깨진 글자로 표시됩니다.

Content-Length

응답 본문의 바이트 크기:

Content-Length: 348

이 헤더가 진행률 표시줄을 가능하게 합니다. 전체 크기를 모르면 브라우저는 얼마나 남았는지 표시 없이 "로딩 중..."만 보여줄 수 있습니다. 또한 클라이언트가 완전한 응답을 받았는지 확인할 수 있게 해줍니다.

Content-Encoding

응답에 적용된 압축 방식을 나타냅니다:

Content-Encoding: gzip

브라우저는 처리 전에 자동으로 압축을 해제합니다. 일반적인 인코딩:

  • gzip — 널리 지원됨, 좋은 압축률
  • br — Brotli, 텍스트에 더 나은 압축률

HTML, CSS, JavaScript 같은 텍스트 콘텐츠의 경우 압축으로 전송 크기를 70-90%까지 줄일 수 있습니다. 네트워크를 통해 전송되는 바이트는 압축 상태이고, 브라우저가 원본으로 복원합니다.

Content-Disposition

콘텐츠를 인라인으로 표시할지 다운로드를 유발할지 제어합니다:

Content-Disposition: attachment; filename="report.pdf"

inline(대부분의 콘텐츠에서 기본값)으로 설정되면 브라우저가 응답을 직접 표시합니다. attachment로 설정되면 사용자에게 파일 저장을 묻습니다. filename 매개변수는 저장할 파일명을 제안합니다.

이를 통해 서버가 동일한 바이트를 보내더라도 하나의 URL은 브라우저에서 PDF를 표시하고 다른 URL은 다운로드 대화 상자를 띄울 수 있습니다.

캐싱 헤더: 복사본을 보관해도 되나요

Cache-Control

응답을 어떻게 캐싱할 수 있는지 제어하는 기본 헤더:

Cache-Control: public, max-age=3600, must-revalidate

중요한 지시어들:

public — CDN, 프록시, 브라우저 등 모든 캐시가 이것을 저장할 수 있습니다. 이미지나 스크립트 같은 정적 자산에 사용하세요.

private — 공유 캐시가 아닌 사용자의 브라우저만 캐시할 수 있습니다. 개인화된 콘텐츠에 사용하세요.

max-age=3600 — 응답의 유효 기간은 3600초(1시간)입니다. 이 기간 동안 캐시는 서버에 확인 없이 응답을 제공할 수 있습니다.

no-cache — 캐시는 저장된 복사본을 사용하기 전에 서버에 검증해야 합니다. 캐시는 응답을 보관하지만 항상 유효한지 확인합니다.

no-store — 전혀 캐싱하지 않습니다. 응답을 디스크에 기록해서는 안 됩니다. 민감한 데이터에 사용하세요.

must-revalidate — 만료된 후에는 캐시가 반드시 재검증해야 합니다. 서버에 접근할 수 없는 상황이라도 만료된 콘텐츠를 제공할 수 없습니다.

ETag

리소스의 특정 버전을 고유하게 식별하는 식별자:

ETag: "a1b2c3d4e5f6"

ETag가 캐싱을 효율적으로 만드는 방법은 이렇습니다: 브라우저는 ETag와 함께 응답을 캐싱합니다. 나중에 캐시 항목이 만료되면 브라우저는 리소스를 다시 요청하지만 If-None-Match 헤더에 ETag를 포함합니다. 콘텐츠가 변경되지 않았다면 서버는 304 Not Modified로 응답합니다 — 본문 없이 캐시된 버전이 여전히 유효하다는 확인만 합니다.

이렇게 잠재적으로 큰 다운로드가 가벼운 검증 요청 하나로 줄어듭니다.

Last-Modified

리소스가 마지막으로 변경된 시간:

Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

ETag와 유사하지만 콘텐츠 식별자 대신 타임스탬프를 기반으로 합니다. 브라우저는 검증을 위해 If-Modified-Since 헤더에 이 값을 다시 보냅니다. ETag보다 정밀도가 낮지만(1초 단위) 구현이 더 간단합니다.

쿠키 헤더: 이것을 기억해주세요

브라우저에게 쿠키를 저장하도록 지시합니다:

Set-Cookie: sessionId=abc123; Secure; HttpOnly; SameSite=Strict; Max-Age=3600

쿠키 값(sessionId=abc123)이 데이터입니다. 속성들은 동작 방식을 제어합니다:

Secure — HTTPS를 통해서만 이 쿠키를 전송합니다. 안전하지 않은 연결에서 가로채기를 방지합니다.

HttpOnly — JavaScript가 이 쿠키에 접근할 수 없습니다. document.cookie를 통해 쿠키를 훔치려는 XSS 공격으로부터 세션 토큰을 보호합니다.

SameSite — 교차 사이트 요청 시 쿠키 전송 여부를 제어합니다:

  • Strict — 교차 사이트로 절대 전송하지 않음 (가장 강력한 보호)
  • Lax — 최상위 탐색에서는 전송하지만 내장된 요청에서는 전송하지 않음
  • None — 항상 전송 (Secure 필요; 합법적인 교차 사이트 시나리오에서 사용)

Max-Age — 쿠키가 만료될 때까지의 초 수. 이것이나 Expires가 없으면 쿠키는 브라우저를 닫을 때 사라지는 "세션 쿠키"가 됩니다.

하나의 응답에 여러 개의 Set-Cookie 헤더를 포함하여 여러 쿠키를 한꺼번에 설정할 수 있습니다.

보안 헤더: 이 규칙들을 적용하세요

Strict-Transport-Security (HSTS)

브라우저에게 항상 HTTPS를 사용하도록 지시합니다:

Strict-Transport-Security: max-age=31536000; includeSubDomains

브라우저가 이 헤더를 받으면 지정된 기간 동안 모든 HTTP 요청을 자동으로 HTTPS로 변환합니다. 사용자가 http://를 직접 입력하거나 HTTP 링크를 클릭해도 브라우저가 무언가를 보내기 전에 HTTPS로 업그레이드합니다.

이렇게 하면 공격자가 HTTPS로 리다이렉트되기 전의 초기 HTTP 요청을 가로채는 것을 방지합니다.

Content-Security-Policy (CSP)

페이지에서 로드할 수 있는 리소스를 정의합니다:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com

CSP는 허용 목록을 만듭니다. 명시적으로 허용되지 않은 리소스는 차단됩니다. XSS 공격에 대한 강력한 방어 수단입니다 — 공격자가 악성 스크립트 태그를 삽입해도, 출처가 허용 목록에 없으면 실행되지 않습니다.

지시어는 다양한 리소스 유형에 대한 출처 규칙을 지정합니다: JavaScript에 대한 script-src, 이미지에 대한 img-src, CSS에 대한 style-src 등. default-src는 명시적으로 지정되지 않은 모든 것에 대한 기본값을 설정합니다.

X-Content-Type-Options

X-Content-Type-Options: nosniff

브라우저가 콘텐츠 유형을 "스니핑"하는 것을 방지합니다. 이 헤더가 없으면 브라우저가 Content-Type 헤더를 무시하고 콘텐츠 자체를 기반으로 유형을 추측할 수 있습니다. 사용자가 업로드한 콘텐츠가 실행 가능한 JavaScript로 잘못 해석되면 보안 취약점이 됩니다. 이 헤더는 브라우저가 Content-Type을 그대로 신뢰하도록 강제합니다.

X-Frame-Options

페이지를 프레임에 삽입할 수 있는지 제어합니다:

X-Frame-Options: DENY
  • DENY — 누구도 이 페이지를 프레임으로 삽입할 수 없음
  • SAMEORIGIN — 동일한 출처의 페이지만 프레임으로 삽입할 수 있음

이 헤더는 악성 사이트가 보이지 않는 iframe에 페이지를 삽입하고 사용자를 속여 클릭하게 만드는 클릭재킹 공격을 방지합니다.

CORS 헤더: 교차 출처 접근

한 출처의 JavaScript가 다른 출처의 리소스에 접근하려 할 때, 브라우저는 기본적으로 이를 차단합니다. CORS 헤더는 명시적인 접근 허가를 부여합니다.

Access-Control-Allow-Origin

가장 기본적인 CORS 헤더:

Access-Control-Allow-Origin: https://www.example.com

이 헤더는 "https://www.example.com의 JavaScript가 이 응답을 읽도록 허용됩니다"라는 의미입니다. 이 헤더가 없으면 브라우저는 응답을 가져오지만 JavaScript가 내용에 접근하는 것을 거부합니다.

와일드카드 *는 모든 출처를 허용하지만, 인증 정보(쿠키)와 함께 사용할 수 없습니다.

Access-Control-Allow-Methods

교차 출처 요청에 허용된 HTTP 메서드:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

이 헤더는 사전 요청(preflight)에 대한 응답에 포함되며, 실제 요청에서 사용할 수 있는 메서드를 브라우저에 알려줍니다.

Access-Control-Allow-Headers

허용된 요청 헤더:

Access-Control-Allow-Headers: Content-Type, Authorization

사용자 정의 헤더는 명시적인 허가가 필요합니다. 이 헤더가 없으면 브라우저는 기본 안전 목록 이외의 헤더를 포함하는 교차 출처 요청을 거부합니다.

Access-Control-Max-Age

브라우저가 사전 요청 결과를 캐시할 수 있는 기간:

Access-Control-Max-Age: 86400

사전 요청은 지연 시간을 늘립니다. 결과를 86400초(하루) 동안 캐싱하면 브라우저가 모든 교차 출처 요청마다 사전 요청을 보낼 필요가 없습니다.

리다이렉트 헤더

Location

어디로 가야 하는지 지정하기 위해 3xx 상태 코드와 함께 사용됩니다:

Location: https://www.example.com/new-page

응답에 리다이렉트 상태 코드(301, 302, 307, 308)가 있으면 브라우저는 자동으로 Location 헤더의 URL을 요청합니다. 리다이렉트 상태 코드는 브라우저가 리다이렉트를 캐시할 수 있는지, HTTP 메서드를 변경해야 하는지를 결정합니다.

연결 헤더

Transfer-Encoding

메시지 본문이 전송되는 방식:

Transfer-Encoding: chunked

청크 인코딩을 사용하면 서버가 전체 콘텐츠 크기를 알기 전에 전송을 시작할 수 있습니다. 각 청크는 크기 정보와 함께 도착하고, 크기가 0인 청크가 전송 완료를 알립니다. 이를 통해 최종 크기를 미리 알 수 없는 스트리밍 응답과 동적으로 생성된 콘텐츠가 가능합니다.

청크 인코딩에서는 Content-Length 헤더가 없습니다 — 청크 자체가 경계를 정의합니다.

서버 정보

Date

응답이 생성된 시간:

Date: Wed, 21 Oct 2024 07:28:00 GMT

디버깅과 캐시 유효 기간 계산에 유용합니다.

Age

응답이 캐시에 머문 시간:

Age: 3600

CDN과 프록시가 이 헤더를 추가합니다. Cache-Control이 max-age=7200이고 Age가 3600이라면, 응답은 아직 1시간의 유효 기간이 남아 있습니다.

Server

서버 소프트웨어를 식별합니다:

Server: nginx/1.18.0

정보 제공 용도이지만, 정확한 버전을 공개하면 공격자가 알려진 취약점을 노릴 수 있기 때문에 프로덕션에서는 종종 생략됩니다.

응답 헤더에 대한 자주 묻는 질문

읽을 수 있는 내용 대신 깨진 텍스트가 보이는 이유는 무엇인가요?

일반적으로 Content-Type 불일치 때문입니다. 서버가 잘못된 charset을 보냈거나(UTF-8이어야 하는데), charset을 전혀 지정하지 않은 경우입니다. 브라우저가 추측하는데, 추측은 자주 빗나갑니다. 수정은 서버 측에서 해야 합니다: Content-Type에 올바른 charset이 포함되어 있는지 확인하세요.

no-cache와 no-store의 차이는 무엇인가요?

no-cache는 "복사본은 보관하되, 사용하기 전에 항상 서버에 확인하세요"를 의미합니다. no-store는 "복사본을 전혀 보관하지 마세요"를 의미합니다. 은행 정보 같은 민감한 데이터에는 디스크에 기록되지 않도록 no-store를 사용하세요.

교차 출처 요청이 Postman에서는 작동하는데 브라우저에서 실패하는 이유는 무엇인가요?

Postman은 CORS를 강제하지 않습니다 — 브라우저가 아니기 때문입니다. 브라우저는 적절한 Access-Control-Allow-Origin 헤더가 없는 교차 출처 응답을 차단합니다. 네트워크 수준에서 요청은 성공하지만, 브라우저는 JavaScript가 그 응답 내용을 볼 수 없게 합니다.

하나의 응답에서 여러 쿠키를 설정할 수 있나요?

가능합니다. 각 쿠키마다 Set-Cookie 헤더를 하나씩 포함시키면 됩니다. 대부분의 헤더와 달리 Set-Cookie는 쉼표로 구분된 목록으로 합칠 수 없습니다.

Cache-Control과 Expires가 서로 다른 내용을 나타내면 어떻게 되나요?

Cache-Control이 우선합니다. 더 최신이고 정밀한 헤더이기 때문입니다. Expires는 HTTP/1.0 캐시와의 하위 호환성을 위해 존재하지만 Cache-Control이 있을 때는 사실상 무시됩니다.

Nakatulong ba ang pahinang ito?

😔
🤨
😃
응답 헤더 • Aklatan • Connected