안녕하세요! 프론트엔드 개발자 여러분, 오늘도 새로운 지식을 쌓아볼 준비 되셨나요? 🚀
이번에 다룰 주제는 조금 낯설 수 있지만 알아두면 서버와 훨씬 더 스마트하게 소통할 수 있는 HTTP 클라이언트 힌트(HTTP Client hints)입니다. 브라우저가 자신의 정보를 서버에 살짝 귀띔해 줘서, 서버가 사용자 맞춤형으로 더 빠르고 예쁜 웹페이지를 내려주게 하는 기능이죠.
공식 문서 내용을 빠짐없이 번역하면서, 실무에서 어떻게 이해하고 쓰면 좋을지 제 꿀팁까지 넉넉하게 담아 알려드릴게요!
클라이언트 힌트(Client hints)란, 서버가 클라이언트(브라우저)에게 "너의 기기, 네트워크 상태, 사용자 정보, 브라우저 설정에 대해 좀 더 알려줄래?"라고 적극적으로(proactively) 요청할 수 있는 일련의 HTTP 요청 헤더(HTTP request header) 필드들을 말합니다.
클라이언트가 이 요청을 수락해서 정보를 제공해 주면, 서버는 그 정보를 바탕으로 사용자에게 어떤 리소스를 보내는 게 가장 좋을지 똑똑하게 결정할 수 있어요.
다양한 "힌트" 헤더 목록은 HTTP 헤더 페이지에서 전체를 확인할 수 있고, 이 글의 힌트 유형(Hint types) 섹션에서도 요약해서 설명해 드릴 거예요.
클라이언트 힌트가 어떻게 동작하는지 4단계로 쉽게 알아볼까요?
브라우저가 웹페이지를 처음 로드하기 위해 서버에 첫 번째 요청을 보낼 때, 항상 User-Agent 헤더를 보냅니다. (이건 옛날부터 하던 전통적인 방식이죠.)
여기에 더해서, 브라우저는 기본적으로 Sec-CH-UA-* 라는 형태의 헤더 세트를 서버에 같이 보냅니다. 이 기본 세트를 우리는 저 엔트로피 힌트(low entropy hints)라고 부릅니다. 예를 들어, 안드로이드 폰에서 접속했다면 대략 이런 식으로 보내게 돼요:
Sec-CH-UA: "Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
Sec-CH-UA-Platform: "Android"
Sec-CH-UA-Mobile: ?1
이 헤더들은 각각 이런 뜻입니다:
Sec-CH-UA: 주요 브라우저 버전과 그와 관련된 브랜드 이름들을 알려줍니다.Sec-CH-UA-Platform: 사용 중인 플랫폼(OS)입니다. (예: Android, Windows 등)Sec-CH-UA-Mobile: 브라우저가 모바일 기기에서 실행 중인지(?1, 예/True) 아니면 데스크탑인지(?0, 아니오/False) 알려주는 불리언(boolean) 값입니다.서버는 첫 번째 요청을 받고 나서, "나는 클라이언트 힌트를 다룰 줄 알아! 그러니까 다음 요청부터는 내게 정보를 더 보내줘!"라고 응답할 수 있습니다. 이때 사용하는 응답 헤더가 바로 Accept-CH입니다. 이 헤더 안에는 쉼표로 구분해서 서버가 추가로 받고 싶은 힌트들의 목록을 적어 보냅니다.
Accept-CH: Sec-CH-UA-Model, Sec-CH-UA-Form-Factors
2번에서 보낸 기본 헤더 세트는 어차피 항상 보내는 거고, 서버는 이 예시에서 다음 두 가지를 더 요구한 거예요:
Sec-CH-UA-Model: 사용 중인 구체적인 기기 모델명 (예: Pixel 9, Galaxy S24)Sec-CH-UA-Form-Factors: 기기의 폼 팩터(형태). 즉, 사용자가 화면 크기, 컨트롤 방식 등을 통해 기기와 어떻게 상호작용하는지 알려줍니다.만약 브라우저가 "응, 정보 더 줘도 괜찮아"라고 판단(허용)했다면, 브라우저 창이나 탭이 닫힐 때까지 이후에 이어지는 모든 요청에 서버가 원했던 추가 헤더들을 꽉꽉 채워서 보냅니다. 아까 그 안드로이드 폰의 경우, 이후 요청부터는 이렇게 업데이트된 헤더가 날아가겠죠:
Sec-CH-UA: "Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
Sec-CH-UA-Platform: "Android"
Sec-CH-UA-Mobile: ?1
Sec-CH-UA-Model: "Pixel 9"
Sec-CH-UA-Form-Factors: "Mobile"
이런 접근 방식은 아주 효율적입니다. 왜냐하면 서버는 자기가 유용하게 써먹을 수 있는 정보만 딱 집어서 요구하거든요. 또한 클라이언트(브라우저) 입장에서도 어떤 정보를 서버에 넘겨주는 게 안전할지 직접 결정할 수 있기 때문에, 기존의 구형 방식보다 훨씬 '프라이버시를 존중(privacy-preserving)'하는 방식이라고 할 수 있습니다.
📝 참고 (Note):
서버에서 HTTP 응답 헤더로Accept-CH를 보내는 방법 말고도, HTML 문서 안의<meta>태그와http-equiv속성을 사용해서 클라이언트 힌트를 요구할 수도 있습니다! (프론트엔드 개발자가 직접 HTML 뼈대에 박아 넣을 때 쓰죠.)<meta http-equiv="Accept-CH" content="Width, Downlink, Sec-CH-UA" />
만약 서버가 클라이언트 힌트 정보를 보고 "폰이니까 작은 이미지 보내야지", "데스크탑이니까 큰 이미지 보내야지" 하고 서로 다른 리소스를 내려준다고 생각해 봅시다.
이때 서버는 반드시 응답에 Vary 헤더를 포함시켜서, "이 응답은 클라이언트 힌트 값에 따라 달라진 결과물이야!"라고 캐시 서버나 브라우저에게 알려줘야 합니다.
그래야 캐시가 똑같은 URL이라도 힌트 헤더 값(예: Width, ECT)마다 각각 다른 버전의 리소스를 따로따로 예쁘게 저장해 둘 수 있거든요.
Vary: Accept, Width, ECT
다만, 값이 너무 시시각각으로 변하는 클라이언트 힌트의 경우엔 차라리 Vary를 쓰지 않거나 다른 캐싱 전략을 고려하는 게 나을 수도 있습니다. 왜냐하면 값이 조금만 달라져도 캐시를 새로 생성하게 되어서 사실상 '캐시 불가능(uncacheable)'한 상태가 되어버리기 때문이에요. 주로 다운로드 속도를 나타내는 Downlink나 통신 지연 시간을 나타내는 RTT 같은 네트워크 관련 힌트들이 그렇습니다.
더 자세한 내용은 HTTP Caching > Vary 문서를 확인해 보세요!
서버가 Accept-CH 응답 헤더를 통해 자기가 원하는 클라이언트 힌트 목록을 명시하면, 브라우저(사용자 에이전트)는 그 힌트들(또는 그중 공유를 허락한 일부)을 해당 브라우징 세션이 끝날 때까지 이어지는 모든 요청에 계속 추가해서 보냅니다.
즉, "나 이런 정보 줘!"라는 서버의 요청은 사용자가 브라우저를 완전히 끄기 전까지 유효(만료되지 않음)합니다.
만약 서버가 중간에 "이제 다른 힌트를 줘"라고 마음이 바뀌었다면, 새로운 목록을 담은 Accept-CH 응답 헤더를 다시 보내서 힌트 세트를 아예 교체해 버릴 수 있습니다. "이제 아무 힌트도 안 줘도 돼"라고 하고 싶다면, Accept-CH에 아무것도 안 적어서(빈 목록) 보내면 됩니다.
📝 참고 (Note):
특정 출처(Origin)에 대해 설정된 클라이언트 힌트를 초기화하고 싶다면, 서버 측에서 해당 출처 내의 URL로Clear-Site-Data: "clientHints"응답 헤더를 보내서 깔끔하게 지울 수도 있습니다.
클라이언트 힌트는 크게 두 가지, 고 엔트로피(high entropy) 힌트와 저 엔트로피(low entropy) 힌트로 나뉩니다.
(여기서 엔트로피란 '정보의 양'이나 '사용자를 특정 지을 수 있는 고유성'을 의미한다고 보면 됩니다.)
저 엔트로피 힌트는 사용자의 프라이버시를 침해할 만한 유의미한 정보가 거의 들어있지 않아서, 사용자를 추적(핑거프린팅, Fingerprint)하는 데 악용될 위험이 낮은 힌트들을 말합니다.
이 녀석들은 서버가 Accept-CH로 명시적으로 달라고 요구하지 않았더라도, 권한 정책(permission policy)에 따라 클라이언트가 기본적으로 매 요청마다 그냥 툭툭 끼워 보낼 수도 있습니다.
대표적인 저 엔트로피 힌트는 딱 4가지입니다:
Save-Data (데이터 절약 모드 켜짐 여부)Sec-CH-UA (브라우저 브랜드 및 버전)Sec-CH-UA-Mobile (모바일 여부)Sec-CH-UA-Platform (운영체제)반대로 고 엔트로피 힌트는 사용자를 식별하고 핑거프린팅하는 데 악용될 잠재적인 위험이 높은 구체적인 정보들을 말합니다.
그렇기 때문에 이 정보들을 서버에 넘겨줄지 말지는 전적으로 브라우저가 깐깐하게 판단(Gated)해서 결정하게 됩니다. 사용자가 브라우저 설정에서 어떻게 해뒀는지, 사용자에게 권한을 요청했는지, 또는 권한 정책이 어떻게 되어있는지에 따라 제공 여부가 달라지죠.
방금 위에서 본 저 엔트로피 힌트 4가지를 제외한 나머지 모~~든 클라이언트 힌트는 전부 고 엔트로피 힌트로 간주하시면 됩니다. (예: 기기 모델명, 화면 너비 등)
필수(Critical) 클라이언트 힌트란, 이 힌트를 화면을 그리기 전에(렌더링 전에) 미리 적용하지 않으면 페이지가 완전 이상하게 보이거나 사용성이 박살 날 정도로 결정적인 영향을 미치는 힌트를 말합니다.
가장 대표적인 예가 바로 Sec-CH-Prefers-Reduced-Motion (애니메이션 감소 옵션)입니다. 사용자가 운영체제 설정에서 어지럼증 등의 이유로 "애니메이션 줄이기"를 켜놨는데 이걸 무시하고 페이지가 렌더링 돼버리면 큰일 나겠죠? 그래서 이런 건 필수 힌트로 취급됩니다.
서버는 Accept-CH로 힌트를 요청할 때, Critical-CH라는 응답 헤더를 같이 보내서 "내가 요구한 힌트 중에 이건 진짜 없으면 안 되는 필수 힌트야!"라고 명시할 수 있습니다. (당연히 Critical-CH에 적힌 헤더는 Accept-CH에도 적혀있어야 합니다.)
브라우저가 서버로부터 Critical-CH 응답을 받았는데, 아차! 자기가 처음에 보낸 요청에 그 필수 힌트가 쏙 빠져있었다는 걸 깨달았다고 해볼게요. 그럼 브라우저는 화면 렌더링을 멈추고, 재빨리 필수 힌트를 포함시켜서 똑같은 요청을 서버에 다시 한번(Retry) 자동으로 쏩니다.
이 멋진 메커니즘 덕분에, 처음 요청에 실수로 힌트가 누락되었거나 서버 설정이 바뀌었더라도 사용자의 필수 설정값(애니메이션 감소 등)이 화면을 그리기 전에 무조건 완벽하게 반영되는 걸 보장할 수 있습니다.
예를 들어, 서버가 클라이언트에게 "나 Sec-CH-Prefers-Reduced-Motion 정보 받을 수 있고, 이거 진짜 '필수'야!"라고 알려주는 응답입니다:
HTTP/1.1 200 OK
Content-Type: text/html
Accept-CH: Sec-CH-Prefers-Reduced-Motion
Vary: Sec-CH-Prefers-Reduced-Motion
Critical-CH: Sec-CH-Prefers-Reduced-Motion
📝 참고 (Note):
위 예시를 보면Vary헤더에도 이 힌트가 꼼꼼하게 들어가 있죠? 브라우저에게 "이 페이지 URL이 똑같더라도 애니메이션 감소 설정값에 따라 보여줄 내용이 완전히 달라지니까, 캐시도 따로따로 나눠서 저장해 줘!"라고 알려주는 겁니다.Critical-CH에 적은 헤더는 짝꿍처럼Accept-CH와Vary헤더에도 모두 들어가 있어야 합니다.
첫 요청에 이 정보가 없었던 클라이언트는, 자기가 애니메이션 감소 모드(reduce)를 켜놨다는 걸 알아채고 재빨리 다시 한번 요청(Retry)을 보냅니다:
GET / HTTP/1.1
Host: example.com
Sec-CH-Prefers-Reduced-Motion: "reduce"
요약하자면, Accept-CH는 "너한테 있는 정보 다 주면 좋겠어~" 하고 가볍게 요청하는 것이고, Critical-CH는 "화면 제대로 띄우려면 이 정보만큼은 무조건, 당장 필요해!"라고 강조하는 것이라고 이해하시면 됩니다.
자, 그럼 클라이언트 힌트에는 어떤 것들이 있는지 종류별로 알아볼까요?
사용자 에이전트(UA) 클라이언트 힌트는 브라우저 이름, 운영체제(OS), 그리고 기기 정보에 따라 서버가 응답을 다르게 내려줄 수 있게 해줍니다.
Sec-CH-UA-* 로 시작하는 헤더들이 여기에 속하며, 전체 목록은 User agent client hints headers에서 확인하실 수 있습니다.
또한 프론트엔드 개발자들은 자바스크립트 내장 API인 User Agent Client Hints API를 통해 이 힌트 정보들을 코드상에서 쉽게 꺼내 쓸 수도 있습니다!
💡 강사의 실무 TIP!
"어? 기존에도User-Agent문자열 뜯어보면 다 알 수 있던 거 아니에요?" 네, 맞습니다! 하지만 옛날 방식인User-Agent헤더는 쓸데없는 정보가 너무 엉켜있고, 사용자를 특정하는 데 쓰일(핑거프린팅) 위험이 컸어요. 반면 새롭게 등장한 이 UA 클라이언트 힌트는 필요한 정보만 깔끔하게 정돈해서 주고, 프라이버시 보호에도 훨씬 유리합니다. 앞으로는 이 새롭고 우아한 방식이 옛날 방식을 완전히 대체할 예정이니 꼭 친해져 두세요!
🚨 주의사항 (Note):
보안상의 이유로 UA 클라이언트 힌트는 fenced frames (격리된 프레임) 안에서는 사용할 수 없도록 막혀있습니다. 권한이 위임되면서 자칫 사용자 데이터가 유출(leak)될 우려가 있기 때문이에요.
사용자가 브라우저나 OS에 설정해 둔 CSS 미디어 기능(CSS media features) 취향에 맞춰 서버가 반응할 수 있게 해주는 힌트입니다. 예를 들어 다크 모드/라이트 모드, 애니메이션 최소화 같은 것들이죠.
대표적인 헤더: Sec-CH-Prefers-Reduced-Motion, Sec-CH-Prefers-Color-Scheme
기기의 남은 메모리 용량이나 화면 크기(속성) 같은 물리적인 스펙에 따라 서버가 응답을 조절할 수 있게 해줍니다.
대표적인 헤더: Sec-CH-Device-Memory (기기 메모리), Sec-CH-DPR (기기 픽셀 비율), Sec-CH-Viewport-Height, Sec-CH-Viewport-Width (화면 가로/세로 길이)
사용자의 데이터 절약 모드 켜짐 여부, 네트워크 대역폭(속도), 지연 시간(핑) 상태에 따라 서버가 다른 응답을 주게 해줍니다.
대표적인 헤더: Save-Data, Downlink (다운로드 속도), ECT (유효 연결 유형), RTT (왕복 지연 시간)
클라이언트 힌트를 영리하게 쓰면, 사용자가 모바일 폰으로 접속했는지 아니면 태블릿으로 접속했는지 서버가 미리 눈치채고 맞춤형 레이아웃을 내려주는 반응형 디자인을 서버 단에서부터 챙길 수 있습니다.
안드로이드 모바일 '스마트폰'에서 접속하면 기본적으로 이런 힌트를 보냅니다:
Sec-CH-UA: "Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
Sec-CH-UA-Platform: "Android"
Sec-CH-UA-Mobile: ?1
반면, 안드로이드 '태블릿'에서 접속하면 모바일(Mobile) 여부가 1이 아니라 0으로 날아갑니다!
Sec-CH-UA: "Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
Sec-CH-UA-Platform: "Android"
Sec-CH-UA-Mobile: ?0
이렇듯 Sec-CH-UA-Mobile 헤더 하나만 딱 봐도 이 기기가 진짜 모바일 기기인지 아닌지 서버가 손쉽게 알 수 있어요. 만약 기기 스펙에 꼭 맞는 아주 세밀한 작업이 필요하다면, 고 엔트로피 힌트인 Sec-CH-UA-Model이나 Sec-CH-UA-Form-Factors를 요청해서 기기명이나 형태를 알아낼 수도 있습니다.
💡 강사의 실무 TIP!
"에이~ 그냥 프론트엔드에서 CSS@media query쓰면 되잖아요?"
네, 맞습니다! 대부분의 일반적인 반응형 디자인은 프론트단에서 CSS 미디어 쿼리(media queries)를 쓰는 게 훨씬 편하고 유연합니다.하지만! 만약 여러분이 거대한 서비스를 구축 중인데, 모바일용 CSS 파일과 데스크탑용 CSS 파일이 아예 독립적으로 나뉘어져 있다면 어떨까요? 프론트에서 판단하게 두면 쓸데없이 두 파일을 다 받아오거나 깜빡임이 생길 수 있겠죠. 이때 클라이언트 힌트를 쓰면 서버가 애초에 사용자 기기에 딱 맞는 CSS 파일만 골라서 내려줄 수 있습니다.
또한 CSS 미디어 쿼리로 체크하던 다크 모드(Sec-CH-Prefers-Color-Scheme), 애니메이션 감소(Sec-CH-Prefers-Reduced-Motion) 설정도 클라이언트 힌트를 통해 서버가 미리 알아채고 맞춤형으로 렌더링해서 줄 수 있다는 엄청난 장점이 있답니다!
더 궁금한 내용이 있다면 아래의 MDN 원문 링크와 구글 개발자 문서를 참고해 보세요!
Vary HTTP 헤더 (Vary HTTP Header)이 페이지는 MDN 기여자들에 의해 2025년 12월 15일에 마지막으로 수정되었습니다.
오늘 배운 내용, 어떠셨나요? 당장 실무에서 전부 적용하진 않더라도 "아, 이제는 서버와 브라우저가 옛날 방식이 아니라 이렇게 스마트하고 안전하게 힌트를 주고받는구나!"라는 걸 아는 것만으로도 여러분은 한 단계 더 성장하신 겁니다! 수고하셨습니다. 🚀