메뉴 버튼
HTTP Content Negotiation 포스트 썸네일 이미지
2026년 3월 15일

HTTP Content Negotiation 완벽 이해하기

network
http
content-negotiation
study

HTTP Content Negotiation 완벽 이해하기

대 AI 시대에 네트워크 같은 기초 개념을 천천히 공부하는 것이 어쩌면 시대에 역행하는 것처럼 느껴질 때가 있다. 하지만 결국 AI를 잘 활용하는 것도 중요한 능력이지만, 내가 기본적인 이해가 없다면 과연 제대로 활용할 수 있을까라는 의문이 들었다. 그런 생각이 들면서 다시 네트워크 공부를 시작하게 되었다.

웹 개발을 하다 보면 같은 URL인데도 상황에 따라 서로 다른 응답이 반환되는 경우가 있다. 예를 들어 어떤 요청에서는 HTML 페이지가 반환되지만, 어떤 요청에서는 JSON 데이터가 반환되기도 한다.

또 어떤 브라우저에서는 WebP 이미지를 받고, 다른 브라우저에서는 JPEG 이미지를 받기도 한다. 이런 동작은 단순한 우연이 아니라 HTTP의 Content Negotiation(콘텐츠 협상)이라는 메커니즘에 의해 이루어진다.

콘텐츠 협상은 클라이언트와 서버가 어떤 형식의 데이터를 주고받을지 협의하는 과정이다. 이번 글에서는 다음 내용을 중심으로 콘텐츠 협상을 정리해보려고 한다.

1. Content Negotiation이 뭘까?

HTTP에서 이야기 하는 Content Negotiation은 동일한 URI에서 리소스의 서로 다른 버전을 제공하기 위한 매커니즘이다.

여기서 핵심이 되는 문장은 다음과 같다.

동일한 URI에서 리소스의 서로 다른 버전을 제공한다.

처음 보면 이 문장이 조금 추상적으로 느껴질 수 있는데, 사실 이것이 HTTP 콘텐츠 협상의 핵심 개념이다.

예를 들어 /article/123 URI에 GET HTTP Method로 요청을 보낸다면, 같은 요청이라도 서버가 상황을 보고 다르게 응답한다.

GET /article/123

겉보기에는 항상 동일한 요청이지만, 실제로는 서버가 상황에 따라 서로 다른 응답을 반환할 수 있다.

예를 들어 다음과 같은 상황이 있을 수 있다.

상황서버 응답
브라우저 언어 = 한국어한국어 페이지
브라우저 언어 = 영어영어 페이지
Accept: application/jsonJSON 데이터
Accept: text/htmlHTML 페이지

URI는 동일하지만, 클라이언트가 어떤 조건으로 요청했는지에 따라 서버가 다른 버전의 리소스를 선택해 응답하는 것이다. 예를 들어 브라우저나 클라이언트가 서버에게 다음과 같은 정보를 전달한다고 생각해 보자.

사용자 요청 조건

// 사용자 요청 조건
•	HTML 형식을 원함
•	한국어 콘텐츠를 원함
•	gzip 압축을 처리할 수 있음

이 경우 서버는 가능한 조건을 고려해 다음과 같은 응답을 보낼 수 있다.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Language: ko
Content-Encoding: gzip

정리하면 Content Negotiation이란 같은 URL에 대해 요청 헤더(언어, 포맷, 압축 방식 등)를 기준으로 서버가 가장 적절한 리소스 표현을 선택해 응답하는 방식을 의미한다.

2. Resource vs Representation

컨텐츠 협상(Content Negotiation)은 본질적으로 하나의 Resource에 대해 여러 Representation 중 하나를 선택하는 과정이다.

흐름을 단순하게 표현하면 다음과 같다.

 하나의 리소스가 있고, 그 리소스를 표현하는 여러 형태가 존재하며, 그중 클라이언트의 요청 조건에 맞는 표현을 서버가 선택하는 과정이 콘텐츠 협상이다.

1) Resource

Resource는 웹에서 접근 가능한 논리적인 대상을 의미한다.

여기서 말하는 논리적인 대상이라는 것은 실제 파일을 의미하는 것이 아니라, 웹에서 식별 가능한 개념적인 대상을 의미한다.

MDN 문서에서도 특정 문서를 리소스라고 표현하는데, 여기서 말하는 문서 역시 웹에서 접근 가능한 대상이라는 의미다. 이렇게 하나의 추상적인 개념으로 이해하면 리소스라는 개념을 훨씬 쉽게 이해할 수 있다.

예를 들어 다음과 같은 것들이 리소스가 될 수 있다.

/users/1
/article/123
/image/logo

이 URI들은 모두 웹에서 접근 가능한 하나의 리소스를 가리킨다.

2) Representation

Representation은 리소스의 실제 표현 형태를 의미한다.

같은 리소스라도 어떤 형식으로 표현하느냐에 따라 여러 형태가 존재할 수 있다. 콘텐츠 협상은 이러한 여러 표현 중에서 가장 적절한 표현을 선택하는 과정이라고 볼 수 있다.

예를 들어 하나의 리소스가 다음과 같은 형태로 표현될 수 있다.

representation설명
JSONAPI 데이터
HTML웹 페이지
XML다른 시스템용
gzip HTML압축된 HTML
한국어 HTML언어 변형

이처럼 하나의 리소스는 다양한 방식으로 표현될 수 있으며, 클라이언트의 요청 조건에 따라 그중 하나가 선택된다. 컨텐츠 협상은 결국 “어떤 Representation을 선택할 것인가”를 결정하는 과정이라고 이해할 수 있다.

3. 컨텐츠 협상 방식을 알아보자.

컨텐츠 협상에는 대표적으로 두 가지 방식이 존재한다.

1) Server-driven Negotiation (서버 주도 협상)

가장 일반적으로 사용되는 방식이다.

클라이언트가 요청을 보내면 HTTP 헤더를 통해 자신의 선호 조건을 전달하고, 서버는 이 정보를 바탕으로 가장 적절한 representation을 선택해 응답한다.

흐름을 단순하게 표현하면 다음과 같다.

Client

GET /resource
Accept: application/json
Accept-Language: ko
Accept-Encoding: gzip

Server

선택된 representation 반환

클라이언트는 요청 헤더를 통해 다음과 같은 정보를 전달할 수 있다.

서버는 이러한 정보를 참고해 가장 적절한 표현(representation)을 선택해 응답한다. 현재 웹에서 가장 널리 사용되는 콘텐츠 협상 방식이다.

2) Agent-driven Negotiation (클라이언트 주도 협상)

이 방식은 서버가 여러 리소스 옵션을 제시하고, 클라이언트가 그중 하나를 선택하는 방식이다.

예를 들어 서버가 다음과 같은 응답을 보낼 수 있다.

HTTP 300 Multiple Choices
/article.en.html
/article.ko.html
/article.jp.html

위처럼 선택 가능한 리소스를 함께 제공하는데 이 경우 클라이언트는 서버가 제시한 여러 리소스 중에서 원하는 것을 직접 선택해 다시 요청하게 된다.

하지만 이 방식에는 몇 가지 단점이 존재하는데 요청이 여러 번 발생할 수 있다는 점, 자동화가 어렵고 구현 복잡도가 높다는 점이다.

이러한 이유로 실제 웹에서는 Agent-driven 방식보다 Server-driven 방식이 훨씬 더 널리 사용된다.

4. 그래서 왜 Content Negotiation이 필요한데?

그렇다면 왜 이런 콘텐츠 협상이 필요할까? 가장 큰 이유는 웹에는 다양한 클라이언트가 존재하기 때문이다. 예를 들어 같은 리소스를 요청하더라도 사용하는 클라이언트에 따라 원하는 데이터 형식이 달라질 수 있다.

각 클라이언트는 자신이 처리하기 쉬운 형식을 선호한다. 이를 조금 더 쉽게 이해하기 위해 비유를 들어보자.

  1. 클라이언트마다 원하는 형식이 다 다른걸

가구를 주문 제작해주는 상점 사장님이 서버라 한다면 가구를 사러오는 손님들이 다 원하는 바는 다를 것이다. 같은 “원목 의자”를 사러왔더라도 요구하는 의자가 다 다르듯이 말이다. 예를 든다면 아래처럼 말할 수 있다.

클라이언트: 나는 JSON이 좋아
서버: HTML도 있는데?
클라이언트: JSON 주세요
서버: 알겠습니다 JSON 응답

아래처럼 클라이언트가 원하는 형식을 가지고 서버에게 간다고 생각해보자.

그럴 경우 서버는 클라이언트 요구에 맞는 표현을 제공해야하는 이 과정이 있어야 같은 리소스를 다양한 환경(브라우저, 언어, 포맷, 네트워크)에 맞게 제공할 수가 있는 것이다.

클라이언트원하는 형식
브라우저HTML
API 클라이언트JSON
이미지 요청WebP
저속 네트워크압축 데이터

이처럼 같은 리소스라도 환경(브라우저, 언어, 포맷, 네트워크)에 따라 다른 표현이 필요하다. 콘텐츠 협상은 바로 이 문제를 해결하기 위해 존재한다.

  1. URL을 단순하게 유지할 수 있다.

콘텐츠 협상이 없다면 서로 다른 표현을 제공하기 위해 URL이 다음과 같이 늘어날 수 있다.

/users/1.json
/users/1.xml
/users/1.html

/users/1?format=json
/users/1?format=xml

하지만 HTTP의 철학 중 하나는 Resource = 하나의 URI라는 개념이다.

리소스는 하나의 URI로 식별하고, 어떤 표현을 사용할지는 헤더를 통해 협상하는 방식을 사용할 수 있다. 이 방식의 장점은 URL을 단순하게 유지할 수 있다는 점이다.

  1. 성능 최적화에도 도움을 준다.

그 외에도 브라우저에서 전달하는 Accept, Accept-Encoding과 같은 헤더 정보를 통해 이미지 최적화(예: webp를 지원하는 브라우저이 경우 webp를 반환)나 압축 최적화 등의 성능 최적화를 가능하게 해준다는 점이 컨텐츠 협상의 장점이자 사용 이유가 될 수 있을 것 같다.

콘텐츠 협상은 웹 성능을 개선하는 중요한 도구가 될 수 있다.

5. 주요 협상 헤더를 알아보자.

콘텐츠 협상은 하나의 리소스(Resource) 에 대해 클라이언트와 서버가 어떤 표현(Representation) 을 주고받을지 결정하는 과정이다. 전체 흐름을 간단히 정리하면 다음과 같다.

이 과정에서 사용되는 주요 헤더들을 하나씩 살펴보자.

1) Accept

Accept 헤더는 클라이언트가 어떤 미디어 타입을 받을 수 있는지 서버에 알려주는 요청 헤더다. 여기서 말하는 미디어 타입(MIME type)은 HTTP 메시지의 body 데이터가 어떤 형식인지 나타내는 타입이다.

형식은 다음과 같다.

Accept: type/subtype

// 예시
Accept: text/html
Accept: application/json
Accept: image/webp

여기서 중요한 건 Accept“내가 지금 보내는 데이터 타입” 이 아니라 “응답으로 받고 싶은 데이터 타입” 을 말한다는 점이다.

(1) 왜 Accept 헤더가 왜 필요할까?

같은 리소스라도 표현 방식이 여러 가지일 수 있기 때문이다.

예를 들어 /users/1이라는 리소스는 다음과 같은 방식으로 표현될 수 있다.

브라우저는 보통 HTML을 원하고, API 클라이언트는 JSON을 원한다. 서버는 요청을 보낸 클라이언트가 어떤 형식을 원하는지 알아야 한다. 이때 사용되는 것이 바로 Accept 헤더다.

(2) 여러 타입을 나열할 수 있다

클라이언트는 여러 타입을 동시에 받을 수 있으며 선호도를 함께 전달할 수 있다.

Accept: text/html, application/json

이 경우 의미는 다음과 같다.

HTML도 받을 수 있고 JSON도 받을 수 있다.

하지만 이 정보만으로는 어떤 것을 더 선호하는지 알기 어렵기 때문에 등장하는 것이 q 값(quality factor)이다.

(3) q 값(quality factor)

q 값은 각 미디어 타입의 상대적인 선호도를 나타낸다.

Accept: text/html;q=0.9, application/json;q=0.8

둘 다 가능하면 HTML을 더 선호한다는 뜻이다. 여기서 중요한 점은 q 값이 절대 점수가 아니라 우선순위 비교용 값이라는 것이다. 값이 클수록 선호도가 높다.

일반적으로 1.0이 가장 높고, 생략하면 기본값은 1.0으로 본다. 아래와 같은 경우 application/jsonq=1.0으로 간주된다.

Accept: application/json, text/html;q=0.5

(4) 와일드카드

Accept에서는 와일드카드도 사용할 수 있다.

Accept: image/*
Accept: */*

예를 들어 브라우저가 아래와 같이 값을 보내면, 이건 “HTML이 가장 좋고, XHTML/XML도 가능하고, 최악의 경우 다른 것도 받을 수 있다”는 의미다. 즉, */*정말 아무거나 받아도 된다는 fallback 역할을 한다.

Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8

(5) 실제 브라우저에서는 왜 다르게 보일까?

브라우저는 요청하는 리소스의 종류에 따라 Accept 값을 다르게 설정한다.

상황브라우저가 기대하는 응답
주소창에서 문서 요청HTML
<img>로 이미지 요청이미지 포맷
fetch() API 호출상황에 따라 다름
<script> 로 JS 파일 요청자바스크립트 또는 범용 타입

브라우저는 하나의 고정된 Accept 값을 갖는 게 아니라 리소스의 맥락(context) 에 따라 다르게 보낸다.  Accept는 단순한 브라우저 설정이 아니라 요청의 맥락(context)을 반영하는 값이다.

(6) Accept vs Content-Type 차이

콘텐츠 협상을 공부하다 보면 AcceptContent-Type 을 자주 함께 보게 된다. 이 둘은 이름이 비슷해서 헷갈리기 쉽지만, 역할이 완전히 다르다.

가장 먼저 핵심만 정리하면 다음과 같다.

Accept희망 사항에 가깝고, Content-Type실제 전송되는 데이터의 정체를 나타낸다.

(7) 요청에서도 Content-Type이 중요하다

POST, PUT, PATCH처럼 body를 보내는 요청에서는 서버가 요청 본문을 어떻게 해석해야 하는지 알아야 한다.

예를 들어 클라이언트가 JSON 문자열을 body에 담아 보내면서도 Content-Type 을 명시하지 않으면, 서버는 이 데이터가 JSON인지, 일반 텍스트인지, 폼 데이터인지 정확히 알기 어렵다.

API 요청을 생각해보면 둘이 동시에 등장하는 경우가 많다.

예를 들어 클라이언트가 JSON body를 서버에 보내고, 응답도 JSON으로 받고 싶을 수 있다.

즉 둘 다 JSON일 수는 있지만, 하나는 보내는 데이터 설명, 하나는 받고 싶은 데이터 요청이라는 점에서 의미가 다르다.

그래서 같은 값이 보이더라도 역할은 전혀 다르다.

프론트엔드 개발자 입장에서 이 둘의 차이는 특히 API 통신에서 중요하다. 예를 들어 fetch나 axios로 요청할 때 다음 두 가지를 구분해서 생각해야 한다.

이 둘이 분리되어야 요청과 응답을 정확히 이해할 수 있다.

헤더의미주로 보는 위치질문으로 이해하기
Accept받고 싶은 응답 타입요청 헤더나는 무엇을 받고 싶은가?
Content-Type실제 전송되는 body 타입요청/응답 헤더지금 보내는 데이터는 무엇인가?

2) Accept-Encoding

Accept-Encoding은 클라이언트가 어떤 콘텐츠 인코딩(압축 방식) 을 해석할 수 있는지 서버에 알려주는 요청 헤더다.

Accept-Encoding: gzip, br

의미 : 나는 gzip도 풀 수 있고 Brotli(br)도 풀 수 있다.

여기서 말하는 Encoding은 문자 인코딩(UTF-8)이 아니라 전송 압축 방식을 의미한다.

(1) 왜 Accept-Encoding이 필요할까?

HTTP 성능 최적화에서 중요한 요소 중 하나는 전송량을 줄이는 것이다. 그 중에서 HTML, CSS, JS, JSON은 텍스트 기반이라 압축 효율이 매우 좋다.

예를 들어 200KB HTML 파일이 있다면 gzip이나 Brotli로 압축했을 때 훨씬 작은 크기로 전달할 수 있다. 하지만 서버가 압축을 사용하려면 클라이언트가 해당 압축을 해석할 수 있는지 알아야 하는데 이때 사용되는 헤더가 Accept-Encoding이다.

요청과 응답 흐름은 다음과 같다.

// 요청
Accept-Encoding: gzip, br

// 응답
Content-Encoding: br

동작 과정은 다음과 같다.

  1. 클라이언트가 지원하는 압축 방식을 서버에 전달한다.
  2. 서버는 가능한 압축 방식 중 하나를 선택한다.
  3. 서버는 Content-Encoding으로 실제 사용한 압축 방식을 알린다.
  4. 클라이언트는 응답을 받은 뒤 압축을 해제한다.

(2) Accept-Encoding과 Content-Encoding은 어떤 차이가 있을까?

// Request
Accept-Encoding: gzip, br
// Response
Content-Encoding: gzip

서버는 클라이언트가 지원한다고 말한 것 중에서 하나를 선택해야 한다.

(3) 대표적인 값들을 함께 알아보자!

의미
gzip가장 널리 사용되는 압축 방식
brBrotli, 일반적으로 더 높은 압축률
identity압축하지 않음

identity는 압축하지 않은 상태를 의미한다. 명시적으로 쓰지 않더라도 기본 fallback 개념으로 이해할 수 있다.

(4) q 값도 지원해주고 있다!

Accept-Encodingq 값을 지원한다. 아래와 같은 예시에서 클라이언트는 “가능하면 br로 달라”고 선호를 전달할 수 있다.

Accept-Encoding: br, gzip;q=0.8, identity;q=0.1

Brotli 가장 선호 → 그다음 gzip → 압축 안 한 건 마지막 수단

(5) 왜 웹 성능에서 중요하다고 할까?

압축은 이미지 최적화와 함께 체감 성능에 직접적인 영향을 준다. 특히 다음 자원에 효과가 크다.

반면 이미 압축된 포맷(JPEG, PNG, MP4 등)은 추가 압축 이득이 적을 수 있다. 그래서 서버는 보통 텍스트 자원 위주로 압축하도록 설정한다.

3) Accept-Language

Accept-Language는 클라이언트가 어떤 언어를 선호하는지 서버에 알려주는 요청 헤더다.

Accept-Language: ko, en;q=0.7

위와 같은 예시에서 서버는 이 정보를 참고해 적절한 언어 버전의 콘텐츠를 고를 수 있다. 응답에서는 보통 다음 헤더로 결과를 알린다. 서버가 “이번 응답은 한국어다”라고 명시하는 것이다.

Content-Language: ko

(1) Accept-Language는 왜 필요할까?

국제화(i18n)가 필요한 서비스에서는 같은 리소스를 여러 언어로 제공해야 한다. 예를 들어 다음 URL이 있다고 가정하자.

/docs/install

이 문서는 실제로 다음과 같은 여러 표현을 가질 수 있다.

클라이언트가 언어 선호를 전달하면 서버는 가장 적절한 언어 버전을 선택할 수 있다.

Accept-Language: ko-KR, ko;q=0.9, en-US;q=0.8, en;q=0.7

우선순위는 다음과 같다.

  1. 한국어 (대한민국)
  2. 일반 한국어
  3. 미국 영어
  4. 일반 영어

즉 단순히 언어만이 아니라 지역(locale)까지 포함한 우선순위를 표현할 수 있다.

(2) Accept-Language의 한계는 없을까?

하지만 이 헤더가 항상 사용자의 실제 언어 선호를 정확히 반영하는 것은 아니다.

이유는 다음과 같다.

예를 들어 브라우저 언어는 영어지만 사이트는 한국어로 보고 싶을 수도 있다. 그래서 서버가 이 값을 맹신하면 UX가 망가질 수 있고, 글로벌 사이트에서는 언어를 선택할 수 있는 Select UI가 존재하는 게 아닐까 싶다.

Accept-Language는 **처음 도와주는 힌트로 사용하고, 사용자가 선택할 수 있는 UI가 가장 좋은 UI라고 생각한다. (**SSR/서버 라우팅에서 초깃값 선택 혹은 다국어 사이트에서 첫 렌더 언어 결정 기준 중 하나로 활용 등)

4) Accept-Charset

Accept-Charset은 클라이언트가 어떤 문자 인코딩(character encoding) 을 이해할 수 있는지 서버에 알려주는 요청 헤더다. 응답 본문의 텍스트가 어떤 문자셋으로 인코딩되어 있는지에 대해 선호를 전달하는 것이다.

Accept-Charset: utf-8
Accept-Charset: iso-8859-1, utf-8;q=0.7, *;q=0.7

(1) 과거에는 중요했지만 지금은 거의 사용되지 않는다

과거 웹에서는 문자 인코딩이 통일되지 않았기 때문에, 서버와 클라이언트가 서로 어떤 문자셋을 쓸지 맞춰야 했다.

ISO-8859-1, EUC-KR, Shift_JIS, UTF-8 등 이런 다양한 인코딩 환경에서는 클라이언트가 “나는 이런 문자셋을 처리할 수 있다”고 알려줄 필요가 있었는데, 지금 현대 웹에서는 UTF-8이 사실상 표준이 되었기 때문에 대부분의 경우 서버도 UTF-8로 보내고, 브라우저도 UTF-8을 문제없이 처리한다. 그래서 협상할 필요 자체가 거의 사라졌다.

5) User-Agent

User-Agent는 요청을 보내는 클라이언트 소프트웨어의 정보를 담는 요청 헤더다. 예를 들어 다음과 같은 값이 들어갈 수 있다.

User-Agent: Mozilla/5.0 (...) Chrome/120.0.0.0 Safari/537.36

이 문자열에는 다음과 같은 정보가 포함될 수 있다.

(1) 왜 콘텐츠 협상에서 언급될까?

엄밀히 말하면 User-Agent는 표준 협상 헤더는 아니지만, 현실에서는 서버가 이 값을 보고 다른 버전의 콘텐츠를 제공하는 경우가 많았다.

예를 들어 다음과 같은 상황이 있다.

서버가 클라이언트 환경을 추정하여 적절한 버전을 제공하는 것이다.

(2) User-Agent 기반 분기의 문제

하지만 이 방식에는 여러 문제가 있다.

결과적으로 코드가 다음처럼 복잡해질 수 있다.

if Safari ...
if old Chrome ...
if mobile ...
if Samsung browser ...

그래서 가능하면 브라우저 이름으로 분기하지 말고 기능 감지(feature detection) 를 사용하는 방법도 있다.

어떤 브라우저인지를 묻기보다 이 기능을 실제로 지원하고 있는 지 확인하는 쪽이 더 안정적일 수 있다.

6. Vary 헤더

Vary는 콘텐츠 협상에서 캐시를 올바르게 동작시키기 위해 사용하는 응답 헤더다. 예를 들어 다음과 같은 응답이 있다고 가정해 보자.

Vary: Accept-Encoding

이 응답은 Accept-Encoding 값에 따라 달라질 수 있다. 캐시에게 “이 응답을 저장할 때 URL만 보지 말고, Accept-Encoding 값도 함께 고려해라”라고 알려주는 것이다.

1) Vary 헤더는 왜 필요할까?

같은 URL이라도 협상 결과가 다를 수 있기 때문이다. 예를 들어 /app.js 요청이 있다고 하자.

클라이언트 A

// 클라이언트 A
Accept-Encoding: gzip

클라이언트 B

Accept-Encoding: br

서버는 같은 /app.js에 대해 서로 다른 압축 버전을 줄 수 있다.

(1) 이때 캐시가 URL만 기준으로 저장하면 문제가 생긴다.

예를 들어 다음과 같은 상황이 있다고 가정해보자.

  1. gzip 응답을 캐시에 저장
  2. 나중에 br를 지원하는 클라이언트가 같은 URL 요청
  3. 캐시가 gzip 버전을 그대로 내보냄

이렇게 되면 협상 결과가 꼬이게 된다. 그래서 서버는 응답에 Vary: Accept-Encoding을 넣어 응답은 헤더 값에 따라 별개의 캐시 항목으로 관리해야 한다고 알려준다.

(2) 캐시 키가 확장된다.

// 보통 캐시
캐시 키 = URL

// Vary가 있을 경우 캐시
캐시 키 = URL + Accept-Encoding

위의 경우처럼 Vary는 캐시 키를 어떻게 분리해야 하는지 알려주는 힌트다.

2) Accept-Encoding 예시

// 요청 1
GET /index.html
Accept-Encoding: gzip

// 응답 1
Content-Encoding: gzip
Vary: Accept-Encoding

----

// 요청 2
GET /index.html
Accept-Encoding: br

// 응답 2
Content-Encoding: br
Vary: Accept-Encoding

캐시는 이 둘을 서로 다른 버전으로 저장한다. 같은 URL이라도 gzip 캐시, br 캐시를 따로 관리하게 된다.

3) Accept-Language에도 중요하다!

예를 들어 아래와 같은 헤더가 있다고 하면, 이건 같은 URL이라도 언어별로 응답이 달라짐을 뜻한다.

Vary: Accept-Language

/docs/install

/docs/install 이라는 URL이라도 한국어 버전 / 영어 버전을 별도 캐시로 유지할 수 있게 된다. 캐시는 이 두 응답을 서로 다른 캐시로 저장하게 된다.

4) Vary가 없으면 어떤 문제가 생길까?

Vary가 없으면 캐시가 협상 기준을 알 수 없다. 그 결과로 다음 같은 문제가 있을 수 있다.

 Vary는 콘텐츠 협상에서 캐시가 올바르게 동작하도록 만드는 핵심 헤더다.

5) 너무 많이 사용하면 어떻게 될까?

Vary를 많이 걸수록 캐시가 더 잘게 쪼개진다.

Vary: Accept-Encoding, Accept-Language, User-Agent

이렇게 되면 조합이 너무 많아져 캐시 적중률이 떨어질 수 있다.

Vary는 정확성을 높이지만, 너무 넓게 쓰면 캐시 효율은 낮아진다.

그래서 실무에서는 정말 필요한 기준만 Vary에 포함해야 한다.

7. Content Negotiation의 한계

콘텐츠 협상은 유용하지만 만능은 아니다. 오히려 왜 실무에서 일부만 제한적으로 쓰이는지도 이 한계를 보면 이해된다.

1) 서버는 클라이언트를 완전히 알 수 없다

서버는 요청 헤더만 보고 판단한다. 서버는 클라이언트가 실제로 무엇을 정확히 처리할 수 있는지 완벽하게 알 수 없다.

예를 들어 다음과 같은 상황이 있다.

결국 서버의 선택은 어느 정도 추정에 기반할 수밖에 없다. 이것이 콘텐츠 협상의 구조적인 한계다.

2) 헤더가 계속 늘어날 수 있다

협상 기준이 많아질수록 새로운 헤더가 필요해진다. 예를 들어 다음과 같은 기준들이 생길 수 있다.

이렇게 되면 요청마다 보내는 메타데이터가 점점 많아진다.

헤더가 많아질수록 요청 크기 / 복잡성 / 서버 판단 로직 / 캐시 분가 증가 등의 문제가 발생한다.

3) 개인정보와 fingerprinting 문제

협상 헤더는 단순한 정보처럼 보이지만 여러 값을 조합하면 사용자를 식별할 수 있는 강력한 정보가 될 수 있다. 예를 들어 다음 정보들이 조합될 수 있다.

이 정보들이 모이면 특정 사용자를 유추할 가능성이 높아진다. 즉 콘텐츠 협상 헤더는 편의성을 제공하지만 동시에 프라이버시 비용도 발생한다. 그래서 최근 웹 플랫폼에서는 다음과 같은 방향으로 발전하고 있다.

4) 캐시 효율 저하

협상 기준이 많을수록 같은 URL에 대해 응답 버전이 많아진다. 예를 들어 다음 세 가지 기준만 있어도 조합이 늘어난다.

이 경우 같은 리소스라도 여러 캐시 엔트리가 생길 수 있다. 컨텐츠 협상에서는 정확한 최적화와 캐시 효율이라는 다음 두 가지가 종종 trade-off 관계가 된다.

5) 구현 복잡성 증가

컨텐츠 협상을 제대로 구현하려면 다음 요소들을 모두 고려해야 한다.

그래서 대부분의 서비스는 다음과 같은 방식으로 부분적으로만 사용한다.

항목실무 사용 방식
Accept-Encoding적극 사용
Accept-Language초기 진입 시 사용
Accept-Charset거의 사용하지 않음
User-Agent가능하면 사용 회피
API 포맷 협상JSON 하나로 단순화

8. 전체 정리

콘텐츠 협상의 전체 흐름을 정리하면 다음과 같다.

  1. 클라이언트는 Accept 계열 헤더로 선호하는 정보를 전달한다.
  2. 서버는 가능한 representation 중 하나를 선택한다.
  3. 서버는 Content-Type, Content-Encoding, Content-Language로 선택 결과를 알린다.
  4. 캐시는 Vary 헤더를 통해 응답 분리 기준을 이해한다.
헤더역할
Accept원하는 미디어 타입
Accept-Encoding지원 가능한 압축 방식
Accept-Language선호 언어
Accept-Charset문자 인코딩 (현재 거의 사용 안 함)
User-Agent클라이언트 정보
Vary캐시 분리 기준

이 과정을 이해하면 단순히 헤더 하나의 의미를 아는 것을 넘어, HTTP가 다양한 클라이언트 환경을 어떻게 유연하게 지원하는지를 볼 수 있던 정보라 유익했다.

참고

사용자 에이전트를 사용한 브라우저 감지 - HTTP | MDN

컨텐츠 협상