
- #http
HTTP - 컨텐츠 협상
Prolip
2025-01-28
클라이언트와 서버 간의 데이터 요청 사이에 효율적으로 자원을 주고받기 위해 수행하는 컨텐츠 협상 알아보기
컨텐츠 협상
서버는 다양한 클라이언트로부터 요청을 받을 수 있다. 하나의 파일을 가지고도 어떤 클라이언트는 json 형식, 어떤 클라이언트는 html 형식으로 같은 내용이라도 클라이언트마다 필요한 문서 형식이 다를 수 있다.
압축한 문서를 읽을 수도 있고, 그렇지 못한 클라이언트도 있다. 한국인이면 한국어, 일본인이면 일본어로 선호하는 언어도 다르고 이런 다양한 클라이언트에게 최적의 자원을 제공하는게 서버의 역할이다.
이제 클라이언트와 함께 서로 주고받을 자원의 형태를 결정해야 한다. 마치 저녁 마감 전에 마트에 가서 흥정하는 것과 마찬가지로 말이다.
이런 과정을 컨텐츠 협상이라고 부르는데, 이 컨텐츠 협상에 사용되는 HTTP 헤더와 동작 방식을 알아보자.
컨텐츠 타입
HTML, CSS, JavaScript는 웹문서로 취급되어 사람이 읽을 수 잇게 렌더링된다. PDF 파일은 브라우저가 내용을 보여주고 압축 파일은 다운로드 폴더에 넣어주는데, 브라우저는 다룰 수 있는 파일의 종류가 다양하다. 같은 URL도 원하는 포맷으로 자원을 사용할 수 있다.
이 때 사용하는 헤더가 Accept 헤더로 브라우저가 서버에 원하는 형식의 자원을 우선적으로 보여달라고 요청하고, 서버는 이 요청 헤더를 보고 적합한 응답 데이터를 만들어 보내주게 된다.
가령 클라이언트가 아래와 같이 헤더를 구성한다고 생각해보면,
Accept: text/html
서버에게 html 형식을 우선적으로 보내달라고 제안을 한 셈이다.
그럼 서버는 이 내용을 바탕으로 가진 자원을 적절한 형태로 제공할 수 있게 되는 것이다.
curl https://github.com/gojimin/lecture-http -v -H "Accept: application/json" * Host github.com:443 was resolved. * IPv6: (none) * IPv4: 20.200.245.247 * Trying 20.200.245.247:443... * Connected to github.com (20.200.245.247) port 443 * schannel: disabled automatic use of client certificate * ALPN: curl offers http/1.1 * ALPN: server accepted http/1.1 * using HTTP/1.x > GET /gojimin/lecture-http HTTP/1.1 > Host: github.com > User-Agent: curl/8.9.1 > Accept: application/json > * Request completely sent off * schannel: failed to decrypt data, need more data < HTTP/1.1 200 OK < Server: GitHub.com < Date: Mon, 17 Feb 2025 09:15:59 GMT < Content-Type: application/json; charset=utf-8 < Vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, Accept-Encoding, Accept, X-Requested-With < ETag: W/"b19633b6650b4c941b5c96716a9d9824" < Cache-Control: max-age=0, private, must-revalidate < Strict-Transport-Security: max-age=31536000; includeSubdomains; preload < X-Frame-Options: deny < X-Content-Type-Options: nosniff < X-XSS-Protection: 0 < Referrer-Policy: no-referrer-when-downgrade
실제로 위와 같이 curl 클라이언트를 사용해 -H 옵션을 통해 Accept 헤더를 application/json 형태로 입력했다. 다른 내용은 두고 오른쪽 화살표 영역인 요청 헤더를 보면 Accept: application/json로 서버에게 힌트를 제공하고 있다.
아래 왼쪽 화살표 영역인 응답 헤더를 보면, Content-Type: application/json;로 서버에서 내 요청을 읽고, 제안한 파일 형식을 제공할 수 있어 응답 본문에 json 파일을 실어 보냈음을 알 수 있다.
압축
서버에서 클라이언트 측으로 파일을 전송할 때, 파일을 압축해 전송하면 그만큼 네트워크 비용을 아끼고 더 빠르게 전달할 수 있을 것이다.
이 때 사용할 수 있는 것이 Accept-Encoding 요청 헤더로 클라이언트에서 사용할 수 있는 압축 방식을 요청 헤더에 실어서 서버로 전송할 수 있다.
Accept-Encoding: gzip
이렇게 요청한다면, 클라이언트는 서버에게 ‘가능하면 gzip 형식으로 압축해서 보내줄 수 있을까요?’라고 요청한 셈이다.
그럼 서버는 이 요청을 받고 이 압축 방식을 서버에서도 사용할 수 있다면, 실제로 gzip 방식으로 압축해 클라이언트에게 반환한다.
이 때, 서버에서도 어떤 방식으로 응답을 압축했는지 알려줘야 하는데, 이때 사용 가능한 헤더가 Content-Encoding 응답 헤더다.
Content-Encoding: gzip
이렇게 서버도 응답 헤더에 클라이언트가 제안한 압축 방식으로 압축했음을 명시할 수 있다.
언어
클라이언트는 서버에게 선호하는 언어도 전달할 수 있다. 당연히 한국인이면, 한국어로 된 페이지를 원한 것이다. 적어도 나는 그렇다.
이 경우에도 클라이언트측에서 서버로 요청을 보낼 때, 협상이 가능하다. 사용 가능한 헤더는 Accept-Language 헤더로 아래의 형태로 사용이 가능하다.
Accept-Language: ko
이렇게 요청 헤더에 선호 언어를 실어 보냄으로 서버에게 이 URL의 자원을 요청하는데요 가능하다면 한글로 된 문서를 받고 싶어요. 라고 말한 것이다. 클라이언트의 제안이라고 볼 수 있다.
서버는 이 요청 헤더를 보고 한글 문서를 제공할 수 있다. 유튜브를 들어갈 때, 확인해 볼 수 있는데
accept-language: ko,en;q=0.9,en-US;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
accept-language: en,ko;q=0.9,en-US;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
위와 같이 실제로 edge 브라우저의 기본 언어 설정을 한국어로 설정한 경우, 유튜브 페이지를 요청할 때 요청 헤더에 ko를 우선으로 요청하고, 반대로 영어를 기본 언어로 설정한 경우 아래와 같이 en을 우선으로 요청한다.
사용자 정보
클라이언트는 요청을 보낼 때, 자신의 정보도 서버에 전달한다. 서버에게 자기소개를 한다고 생각하면 좋겠다.
이 때 User-Agent라는 요청 헤더를 사용할 수 있다.
User-Agent: 제품명/버전 설명
형태로 구성하는데, 실제로 엣지 브라우저의 요청 헤더를 보면
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0
이런 형태로 나온다. 엣지가 크로미움 기반이라 그런가 크롬도 나오고 사파리도 나오고 마지막에 엣지 정보가 나온다.
이렇게 엣지로 요청했음에도 여러 브라우저 정보가 나오는걸 보니 컨텐츠 협상에 사용하기엔 다소 신뢰하기 어렵다는 생각이 들 것이다.
실제로 Browser detection using the user agent - HTTP | MDN 이 문서에 따르면 어째서 User-Agent로 브라우저 감지를 피해야 하는지 나와있다.
- 웹은 누구에게나 열여 있어야 하기 때문이다. 특정 브라우저에 따라 다르게 제공하는건 좋지 않은 습관으로 브라우저별로 기능 차이를 두는 것이 아닌, 기능의 지원 여부를 체크하는게 좋다고 말한다.
- 또 위에서 살펴보았듯, User-Agent 헤더는 믿을 수 없다. 브라우저들이 서로 흉내내듯 엣지 브라우저를 사용했지만, 크롬, 사파리 정보를 포함하고 있다.
하지만 매우 드문 경우에 사용할 수 있는데, 대표적으로 현재 지원을 중단한 IE 브라우저를 감지해 제공할 수 없다는 내용을 표시할 때다.
const userAgent = req.headers['User-Agent']; const isIE = /msie|trident/i.test(userAgent); if(isIE) { res.write(<html><body>IE는 지원하지 않습니다.</body></html>); res.end(); return; }
이런식으로 말이다.
마무리
웹 어플리케이션의 성능, 사용자 경험 개선을 위해 콘텐츠 협상은 필수 개념이다. 클라이언트와 서버가 함께 최적의 컨텐츠 형식, 압축 방식, 언어를 협상해 사용자 친화적인 서비스를 제공할 수 있게 되는 것이다.
하지만 마지막에서 보았듯, User-Agent에 의존해 브라우저를 감지하는 것은 신뢰성, 유지보수성 측면에서 권장되지 않는데, 이는 기능 탐지를 통해 특정한 기능의 지원 여부를 판단하는 방식이 더 안전하고 효율적이라고 본다. 첨부된 문서를 참고해보면, 모바일 감지도 User-Agent 사용 대신 Navigator.maxTouchPoints를 사용해 사용자 기기에 터치 스크린이 있는지 확인하는 방법도 대안으로 나와있다.

HTTP - 기본 흐름 정리하기
