업데이트됨 1개월 전
모든 HTTP 교환은 눈에 보이지 않는 협상으로 시작됩니다.
브라우저가 HTML의 첫 바이트를 받기 전에, API가 JSON을 반환하기 전에, 먼저 대화가 오고 갑니다. 클라이언트가 말합니다: "나는 macOS의 Chrome이야. JSON을 선호하지만 HTML도 돼. gzip도 지원해. 그리고 화요일에 캐시한 사본이 있는데—더 최신 버전 있어?" 서버가 답합니다: "JSON으로 줄게, 4,847바이트이고 gzip으로 압축했어. 한 시간 동안 캐시해. 그리고 앞으로는 반드시 HTTPS를 써야 해."
이 대화는 헤더 안에서 이루어집니다. 헤더는 업무 이야기를 시작하기 전에 나누는 가벼운 인사와 같습니다—어떤 콘텐츠도 오가기 전에 양측이 조건에 합의할 수 있게 해주는 메타데이터입니다.
헤더의 구조
헤더는 단순한 키-값 쌍입니다:
각 헤더는 특정 질문에 답합니다. Content-Type은 "이게 뭔가요?"에, Cache-Control은 "얼마나 오래 보관할 수 있나요?"에, Authorization은 "당신은 누구인가요?"에 답합니다.
헤더 이름은 대소문자를 구분하지 않습니다(관례적으로 Title-Case로 표기하지만). 값은 헤더에 따라 대소문자를 구분할 수 있습니다. 문법적으로 알아야 할 것은 이게 전부입니다.
두 방향, 서로 다른 질문
요청 헤더는 클라이언트에서 서버로 향하는 질문입니다:
- "어떤 형식을 지원하나요?" (Accept)
- "저는 누구인가요?" (Authorization)
- "어디서 온 요청인가요?" (Origin, Referer)
- "제가 캐시한 것보다 더 최신 버전이 있나요?" (If-Modified-Since)
- "어떤 클라이언트를 쓰고 있나요?" (User-Agent)
응답 헤더는 서버에서 클라이언트로 향하는 답변입니다:
- "보내는 내용이 이것입니다" (Content-Type, Content-Length)
- "이 기간 동안 캐시하세요" (Cache-Control, Expires)
- "보안 요구사항입니다" (Strict-Transport-Security)
- "나중을 위해 저장해 두세요" (Set-Cookie)
대화는 양방향으로 흐르지만, 오가는 질문의 종류는 다릅니다.
헤더가 실제로 하는 일
콘텐츠 설명. Content-Type은 상자 안에 무엇이 들었는지 알려줍니다. Content-Length는 크기를, Content-Encoding은 압축 방식을 알려줍니다. 이것들이 없으면 브라우저는 추측해야 하는데, 추측은 보안 취약점과 페이지 오류로 이어집니다.
캐싱. Cache-Control은 아마도 성능에 가장 큰 영향을 미치는 헤더일 것입니다. 단 하나의 지시어—max-age=31536000—가 수백만 건의 불필요한 다운로드를 없앨 수 있습니다. ETag와 Last-Modified를 활용하면 클라이언트가 전부 다시 다운로드하는 대신 "이게 바뀌었나요?"라고만 물어볼 수 있습니다.
인증. Authorization 헤더는 인증 정보를 전달합니다. 서버는 WWW-Authenticate로 응답하여 "인증 정보가 필요하고, 형식은 이렇게 해주세요"라고 알립니다. 이 과정은 거의 모든 API 호출에서 일어납니다.
보안. Strict-Transport-Security는 "항상 HTTPS를 사용하라"고 말합니다. Content-Security-Policy는 "이 출처에서만 스크립트를 로드하라"고 말합니다. X-Frame-Options는 "다른 사이트가 나를 삽입하지 못하게 하라"고 말합니다. 이 헤더들은 브라우저가 시행하는 보안 정책입니다.
교차 출처 접근. CORS 헤더(Access-Control-Allow-Origin 등)는 "이 웹사이트가 내 응답을 읽어도 되나요?"에 답합니다. 이것이 없으면 한 도메인의 JavaScript가 다른 도메인의 응답을 읽을 수 없습니다—브라우저의 기본 보안 모델입니다.
흐름
- 브라우저가 자신이 누구인지, 무엇을 원하는지 담은 헤더를 추가해 요청을 구성합니다
- 헤더는 본문보다 먼저 일반 텍스트로 전송됩니다
- 서버는 헤더를 읽어 요청을 파악합니다
- 서버는 무엇을 보내는지, 어떻게 처리할지 담은 헤더를 추가해 응답을 구성합니다
- 브라우저는 응답 헤더를 읽어 뒤따르는 본문을 어떻게 처리할지 결정합니다
클라이언트와 서버 사이에서 프록시, CDN, 로드 밸런서 같은 중계 장치들이 헤더를 추가하거나 제거하거나 수정할 수 있습니다. Connection 같은 일부 헤더는 "hop-by-hop" 헤더로, 전체 경로가 아닌 한 구간에만 적용됩니다.
표준 헤더와 사용자 정의 헤더
대부분의 헤더는 IETF에서 표준화되어 있습니다. Content-Type, Authorization, Cache-Control—이것들은 모든 브라우저와 서버가 동일하게 이해하는 정의된 의미를 가집니다.
애플리케이션에 특화된 필요에 따라 사용자 정의 헤더를 만들 수 있습니다. 예전 관례는 "X-"를 접두사로 붙이는 것이었지만(X-Request-ID, X-Forwarded-For), 이제는 더 이상 권장되지 않습니다. 요즘은 그냥 설명적인 이름을 사용하면 됩니다.
흔히 오해하는 것들
헤더는 HTTP에서 암호화되지 않습니다. HTTPS를 사용하지 않으면 Authorization 헤더가 평문으로 전송됩니다. HTTPS가 중요한 이유가 바로 이것입니다—본문뿐만 아니라 헤더를 보호하기 위해서도요.
헤더에는 크기 제한이 있습니다. 서버는 일반적으로 약 8KB를 초과하는 헤더가 담긴 요청을 거부합니다. 헤더에 너무 많은 것을 넣으려 한다면, 잘못된 방법을 쓰고 있는 것입니다.
모든 헤더가 목적지까지 살아남는 건 아닙니다. 중계 장치들은 일부 헤더를 제거합니다. Connection 같은 hop-by-hop 헤더는 의도적으로 전달되지 않습니다. 서버가 받는 헤더가 클라이언트가 보낸 헤더와 다를 수 있습니다.
대화를 직접 확인하는 방법
모든 브라우저에서 이 대화를 직접 볼 수 있습니다:
- 개발자 도구 열기 (F12 또는 마우스 오른쪽 버튼 클릭 → 검사)
- 네트워크 탭으로 이동
- 요청을 발생시키는 작업 수행
- 원하는 요청을 클릭하면 헤더를 확인할 수 있습니다
브라우저가 요청한 내용과 서버가 응답한 내용, 협상의 양측을 모두 볼 수 있습니다. 인증이 실패하거나, 캐시가 오래된 콘텐츠를 제공하거나, CORS가 API 호출을 차단할 때—답은 대부분 헤더에 있습니다.
HTTP 헤더 자주 묻는 질문
프레임워크가 헤더를 처리해주는데 왜 이해해야 하나요?
프레임워크는 일반적인 경우를 추상화해주지만, 디버깅할 때는 실제로 무슨 일이 일어나는지 직접 봐야 합니다. API가 403을 반환할 때, 캐시가 오래된 내용을 제공할 때, CORS가 요청을 차단할 때—헤더가 그 이유를 알려줍니다. 프레임워크는 스스로를 디버그할 수 없습니다.
헤더와 쿼리 파라미터의 차이는 무엇인가요?
쿼리 파라미터는 URL의 일부입니다(?page=2&sort=name). 눈에 보이고, 북마크할 수 있으며, 주로 원하는 리소스를 설명합니다. 헤더는 요청 자체에 대한 메타데이터입니다—인증 정보, 콘텐츠 기본 설정, 캐싱 지시어처럼요. 목적이 다르고, 방식도 다릅니다.
JavaScript에서 아무 헤더나 설정할 수 있나요?
아니요. 브라우저는 특정 헤더(Host, Content-Length, Cookie 등)에 대한 임의 제어를 막습니다. 허용하면 보안에 문제가 생기기 때문입니다. 이런 "제한 헤더"는 브라우저 자체만 설정할 수 있습니다.
왜 어떤 헤더는 X-로 시작하나요?
역사적으로 X-는 "실험적(experimental)" 또는 "확장(extension)"을 의미했습니다—어떤 표준에도 속하지 않는 사용자 정의 헤더라는 뜻이었죠. 그러나 너무 많은 "실험적" 헤더가 영구적인 표준이 되었고, 접두사 자체가 아무런 의미도 더하지 않게 되었습니다. 그래서 이 관례는 이제 더 이상 권장되지 않습니다. 새로운 사용자 정의 헤더는 그냥 직관적인 이름을 사용하면 됩니다.
이 페이지가 도움이 되었나요?