클라이언트 힌트로 리소스 선택 자동화

Ilya Grigorik
Ilya Grigorik

웹용으로 빌드하면 비교할 수 없는 도달범위를 확보할 수 있습니다. 웹 애플리케이션은 클릭 한 번으로 이용할 수 있으며 브랜드나 플랫폼에 관계없이 스마트폰, 태블릿, 노트북 및 데스크톱, TV 등 거의 모든 연결된 기기에서 사용할 수 있습니다. 최적의 환경을 제공하기 위해 각 폼 팩터에 맞게 프레젠테이션과 기능을 조정하는 반응형 사이트를 빌드했습니다. 이제 애플리케이션이 최대한 빨리 로드되도록 성능 체크리스트를 실행하고 있습니다. 중요한 렌더링 경로를 최적화하고 텍스트 리소스를 압축 및 캐시했습니다. 이제 전송된 바이트의 대부분을 차지하는 이미지 리소스를 살펴보고 있습니다. 문제는 이미지 최적화가 어렵다는 점입니다.

  • 적절한 형식 (벡터 또는 래스터) 결정
  • 최적의 인코딩 형식 (jpeg, webp 등) 결정
  • 적절한 압축 설정 (손실 압축과 무손실 압축) 결정
  • 유지하거나 삭제할 메타데이터 결정
  • 각 디스플레이 + DPR 해상도에 대해 여러 변형을 만듭니다.
  • ...
  • 사용자의 네트워크 유형, 속도, 환경설정 고려

개별적으로는 잘 알려진 문제입니다. 이러한 요소는 개발자가 종종 간과하거나 소홀히 하는 대규모 최적화 공간을 만듭니다. 특히 여러 단계가 포함된 경우 사람은 동일한 검색 공간을 반복적으로 탐색하는 데 효과적이지 않습니다. 반면 컴퓨터는 이러한 유형의 작업에 능숙합니다.

이미지 및 유사한 속성을 가진 기타 리소스에 대한 효과적이고 지속 가능한 최적화 전략은 간단합니다. 자동화입니다. 리소스를 수동으로 조정하는 경우 잘못된 방법입니다. 잊어버리거나 게으름을 피우거나 다른 사람이 실수를 할 수 있습니다.

성능에 민감한 개발자의 여정

이미지 최적화 공간을 통한 검색에는 빌드 시간과 런타임이라는 두 가지 고유한 단계가 있습니다.

  • 일부 최적화는 리소스 자체에 내재되어 있습니다. 예를 들어 적절한 형식 및 인코딩 유형을 선택하고, 각 인코더의 압축 설정을 조정하고, 불필요한 메타데이터를 제거하는 등의 작업이 여기에 해당합니다. 이러한 단계는 '빌드 시간'에 실행할 수 있습니다.
  • 다른 최적화는 이를 요청하는 클라이언트의 유형 및 속성에 따라 결정되며 '런타임'에 실행되어야 합니다. 즉, 클라이언트의 DPR 및 의도한 디스플레이 너비에 적절한 리소스를 선택하고 클라이언트의 네트워크 속도, 사용자 및 애플리케이션 환경설정 등을 고려해야 합니다.

빌드 시간 도구가 있지만 더 개선될 수 있습니다. 예를 들어 각 이미지와 각 이미지 형식의 '품질' 설정을 동적으로 조정하면 많은 절감 효과를 얻을 수 있지만 아직 연구 외적으로 이를 실제로 사용하는 사용자를 보지 못했습니다. 이 부분은 혁신을 위한 준비가 잘 되어 있는 영역이지만 이 게시물의 목적을 위해 여기까지만 말씀드리겠습니다. 스토리의 런타임 부분에 집중하겠습니다.

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

애플리케이션 인텐트는 매우 간단합니다. 사용자 뷰포트의 50% 에서 이미지를 가져와 표시합니다. 대부분의 디자이너는 이곳에서 손을 씻고 바를 향합니다. 한편 팀의 성능에 민감한 개발자는 긴 밤을 보내고 있습니다.

  1. 최적의 압축을 위해 각 클라이언트에 최적의 이미지 형식(Chrome의 경우 WebP, Edge의 경우 JPEG XR, 나머지는 JPEG)을 사용하려 합니다.
  2. 최상의 시각적 품질을 얻으려면 1x, 1.5x, 2x, 2.5x, 3x 등 다양한 해상도로 각 이미지의 여러 변형을 생성해야 합니다. 그 사이에 몇 가지 더 있을 수도 있습니다.
  3. 불필요한 픽셀을 전송하지 않으려면 '사용자의 표시 영역 50%'가 실제로 무엇을 의미하는지 이해해야 합니다. 표시 영역 너비는 다양합니다.
  4. 또한 느린 네트워크를 사용하는 사용자가 자동으로 더 낮은 해상도를 가져오는 탄력적인 환경을 제공하는 것이 좋습니다. 결국은 유리로 갈 때입니다.
  5. 또한 애플리케이션은 가져와야 하는 이미지 리소스에 영향을 미치는 일부 사용자 컨트롤을 노출하므로 이를 고려해야 합니다.

디자이너는 가독성을 최적화하기 위해 표시 영역 크기가 작은 경우 100% 너비로 다른 이미지를 표시해야 한다는 것을 깨닫습니다. 즉, 이제 애셋 하나를 더 대상으로 동일한 프로세스를 반복한 후 가져오기를 표시 영역 크기에 따라 조건부로 설정해야 합니다. 이 작업이 쉽지 않다고 말씀드렸나요? 좋습니다. 시작하겠습니다. picture 요소를 사용하면 꽤 많은 작업을 할 수 있습니다.

<picture>
    <!-- serve WebP to Chrome and Opera -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.webp 200w, /image/thing-400.webp 400w,
        /image/thing-800.webp 800w, /image/thing-1200.webp 1200w,
        /image/thing-1600.webp 1600w, /image/thing-2000.webp 2000w"
    type="image/webp">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.webp 200w, /image/thing-crop-400.webp 400w,
        /image/thing-crop-800.webp 800w, /image/thing-crop-1200.webp 1200w,
        /image/thing-crop-1600.webp 1600w, /image/thing-crop-2000.webp 2000w"
    type="image/webp">
    <!-- serve JPEGXR to Edge -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpgxr 200w, /image/thing-400.jpgxr 400w,
        /image/thing-800.jpgxr 800w, /image/thing-1200.jpgxr 1200w,
        /image/thing-1600.jpgxr 1600w, /image/thing-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpgxr 200w, /image/thing-crop-400.jpgxr 400w,
        /image/thing-crop-800.jpgxr 800w, /image/thing-crop-1200.jpgxr 1200w,
        /image/thing-crop-1600.jpgxr 1600w, /image/thing-crop-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <!-- serve JPEG to others -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpg 200w, /image/thing-400.jpg 400w,
        /image/thing-800.jpg 800w, /image/thing-1200.jpg 1200w,
        /image/thing-1600.jpg 1600w, /image/thing-2000.jpg 2000w">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpg 200w, /image/thing-crop-400.jpg 400w,
        /image/thing-crop-800.jpg 800w, /image/thing-crop-1200.jpg 1200w,
        /image/thing-crop-1600.jpg 1600w, /image/thing-crop-2000.jpg 2000w">
    <!-- fallback for browsers that don't support picture -->
    <img src="/image/thing.jpg" width="50%">
</picture>

아트 디렉션, 형식 선택을 처리하고 클라이언트 기기의 DPR 및 뷰포트 너비의 가변성을 고려하여 각 이미지의 6가지 변형을 제공했습니다. 놀라워요.

안타깝게도 picture 요소를 사용하면 클라이언트의 연결 유형이나 속도에 따라 어떻게 동작해야 하는지에 관한 규칙을 정의할 수 없습니다. 하지만 처리 알고리즘은 경우에 따라 사용자 에이전트가 가져오는 리소스를 조정하도록 허용합니다(5단계 참고). 사용자 에이전트가 충분히 똑똑하기를 바랄 뿐입니다. 참고: 현재 구현은 모두 그렇지 않습니다. 마찬가지로 picture 요소에는 앱 또는 사용자 환경설정을 고려하는 앱별 로직을 허용하는 후크가 없습니다. 이 마지막 두 비트를 얻으려면 위의 모든 로직을 JavaScript로 이동해야 하지만 그러면 picture에서 제공하는 미리 로드 스캐너 최적화가 손실됩니다. 죄송합니다.

이러한 제한사항을 제외하고는 작동합니다. 적어도 이 특정 저작물의 경우는 그렇습니다. 여기서 진짜로 장기적인 과제는 디자이너나 개발자가 모든 애셋에 대해 이와 같은 코드를 직접 제작할 수 없다는 점입니다. 처음에는 재미있는 두뇌 퍼즐이지만 그 후에는 매력을 잃습니다. 자동화가 필요합니다. IDE 또는 기타 콘텐츠 변환 도구를 사용하면 위의 템플릿을 자동으로 생성하고 저장할 수 있습니다.

클라이언트 힌트를 사용하여 리소스 선택 자동화

심호흡을 하고 의심을 멈추고 다음 예시를 생각해 보세요.

<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
...
<picture>
    <source media="(min-width: 50em)" sizes="50vw" srcset="/image/thing">
    <img sizes="100vw" src="/image/thing-crop">
</picture>

놀랍게도 위의 예시가 훨씬 더 긴 위의 그림 마크업과 동일한 기능을 모두 제공하기에 충분합니다. 또한 나중에 설명하겠지만 이미지 리소스를 가져오는 방법, 가져올 이미지, 가져올 시점을 개발자가 완전히 제어할 수 있습니다. '마법'은 첫 번째 줄에 있으며, 이 줄은 클라이언트 힌트 보고를 사용 설정하고 브라우저에 기기 픽셀 비율 (DPR), 레이아웃 뷰포트 너비 (Viewport-Width), 리소스의 의도된 디스플레이 너비 (Width)를 서버에 광고하도록 지시합니다.

클라이언트 힌트가 사용 설정된 경우 결과 클라이언트 측 마크업은 프레젠테이션 요구사항만 유지합니다. 디자이너는 이미지 유형, 클라이언트 해상도, 전송된 바이트를 줄이기 위한 최적의 브레이크포인트 또는 기타 리소스 선택 기준에 관해 걱정할 필요가 없습니다. 사실 그들은 그동안 그렇게 하지 않았으며 그렇게 할 필요도 없습니다. 또한 실제 리소스 선택은 클라이언트와 서버에서 협상하므로 개발자가 위의 마크업을 다시 작성하고 확장할 필요가 없습니다.

Chrome 46에서는 DPR, Width, Viewport-Width 힌트를 기본적으로 지원합니다. 힌트는 기본적으로 사용 중지되어 있으며 위의 <meta http-equiv="Accept-CH" content="...">는 Chrome에 지정된 헤더를 아웃바운드 요청에 추가하도록 지시하는 선택 신호 역할을 합니다. 이제 샘플 이미지 요청의 요청 및 응답 헤더를 살펴보겠습니다.

클라이언트 힌트 협상 다이어그램

Chrome은 Accept 요청 헤더를 통해 WebP 형식 지원을 광고합니다. 마찬가지로 새 Edge 브라우저는 Accept 헤더를 통해 JPEG XR 지원을 광고합니다.

다음 세 개의 요청 헤더는 클라이언트 기기의 기기 픽셀 비율 (3x), 레이아웃 뷰포트 너비(460px), 리소스의 의도된 디스플레이 너비 (230px)를 광고하는 클라이언트 힌트 헤더입니다. 이렇게 하면 사전 생성된 리소스의 가용성, 리소스 재인코딩 또는 크기 조절의 비용, 리소스의 인기도, 현재 서버 부하 등 자체 정책에 따라 최적의 이미지 변형을 선택하는 데 필요한 모든 정보가 서버에 제공됩니다. 이 특정 경우에는 서버가 DPRWidth 힌트를 사용하고 Content-Type, Content-DPR, Vary 헤더에 표시된 대로 WebP 리소스를 반환합니다.

마법 같은 방법은 없습니다. 리소스 선택을 HTML 마크업에서 클라이언트와 서버 간의 요청-응답 협상으로 이동했습니다. 따라서 HTML은 프레젠테이션 요구사항에만 관심이 있으며 모든 디자이너와 개발자가 작성할 수 있는 신뢰할 수 있는 항목이지만 이미지 최적화 공간을 통한 검색은 컴퓨터로 지연되며 이제 대규모로 쉽게 자동화할 수 있습니다. 성능에 민감한 개발자를 기억하시나요? 이제 제공된 힌트를 활용하고 적절한 응답을 반환할 수 있는 이미지 서비스를 작성해야 합니다. 원하는 언어 또는 서버를 사용하거나 서드 파티 서비스 또는 CDN이 이를 대신하도록 할 수 있습니다.

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

위의 사람도 기억하시나요? 이제 클라이언트 힌트를 사용하면 추가 마크업을 하지 않아도 단순한 이미지 태그가 DPR, 뷰포트, 너비를 인식합니다. 아트 디렉션을 추가해야 하는 경우 위에서 설명한 대로 picture 태그를 사용하면 되며, 그렇지 않은 경우 기존의 모든 이미지 태그가 훨씬 더 스마트해집니다. 클라이언트 힌트는 기존 imgpicture 요소를 개선합니다.

서비스 워커로 리소스 선택 제어

ServiceWorker는 실제로 브라우저에서 실행되는 클라이언트 측 프록시입니다. 이 도구는 모든 발신 요청을 가로채고 응답을 검사, 재작성, 캐시, 심지어 합성할 수 있도록 지원합니다. 이미지도 마찬가지이며 클라이언트 힌트가 사용 설정된 경우 활성 ServiceWorker는 이미지 요청을 식별하고, 제공된 클라이언트 힌트를 검사하고, 자체 처리 로직을 정의할 수 있습니다.

self.onfetch = function(event) {
    var req = event.request.clone();
    console.log("SW received request for: " + req.url)
    for (var entry of req.headers.entries()) {
    console.log("\t" + entry[0] +": " + entry[1])
    }
    ...
}
클라이언트가 serviceWorker를 암시합니다.

ServiceWorker를 사용하면 리소스 선택을 클라이언트 측에서 완전히 제어할 수 있습니다. 매우 중요한 단계입니다. 가능성은 거의 무한하므로 잘 생각해 보세요.

  • 사용자 에이전트에서 설정한 클라이언트 힌트 헤더 값을 재작성할 수 있습니다.
  • 요청에 새 클라이언트 힌트 헤더 값을 추가할 수 있습니다.
  • URL을 재작성하고 이미지 요청을 대체 서버(예: CDN)로 가리킬 수 있습니다.
    • 인프라에 더 쉽게 배포할 수 있다면 힌트 값을 헤더에서 URL 자체로 이동할 수도 있습니다.
  • 응답을 캐시하고 제공되는 리소스에 대한 자체 로직을 정의할 수 있습니다.
  • 사용자의 연결 상태에 따라 응답을 조정할 수 있습니다.
  • 애플리케이션 및 사용자 환경설정 재정의를 고려할 수 있습니다.
  • 정말로 원하는 것은 무엇이든 할 수 있습니다.

picture 요소는 HTML 마크업에서 필요한 아트 디렉션 제어를 제공합니다. 클라이언트 힌트는 리소스 선택 자동화를 사용 설정하는 결과 이미지 요청에 주석을 제공합니다. ServiceWorker는 클라이언트에서 요청 및 응답 관리 기능을 제공합니다. 확장 가능한 웹이 작동하는 모습입니다.

클라이언트 힌트 FAQ

  1. 클라이언트 힌트는 어디에서 사용할 수 있나요? Chrome 46에서 출시되었습니다. FirefoxEdge에서 고려 중입니다.

  2. 고객 힌트가 선택사항인 이유는 무엇인가요? 클라이언트 힌트를 사용하지 않는 사이트의 오버헤드를 최소화하려고 합니다. 클라이언트 힌트를 사용 설정하려면 사이트에서 페이지 마크업에 Accept-CH 헤더 또는 이에 상응하는 <meta http-equiv> 지시어를 제공해야 합니다. 둘 중 하나가 있으면 사용자 에이전트는 모든 하위 리소스 요청에 적절한 힌트를 추가합니다. 향후 특정 출처에 이 환경설정을 유지하는 추가 메커니즘을 제공할 수 있으며, 이를 통해 탐색 요청에 동일한 힌트를 전송할 수 있습니다.

  3. ServiceWorker가 있는 경우 클라이언트 힌트가 필요한 이유는 무엇인가요? ServiceWorker는 레이아웃, 리소스, 뷰포트 너비 정보에 액세스할 수 없습니다. 적어도 비용이 많이 드는 왕복을 도입하고 이미지 요청을 상당히 지연시키지 않으면 안 됩니다(예: 이미지 요청이 미리 로드 파서에 의해 시작된 경우). 클라이언트 힌트는 브라우저와 통합되어 이 데이터를 요청의 일부로 사용할 수 있도록 합니다.

  4. 클라이언트 힌트는 이미지 리소스용인가요? DPR, 뷰포트 너비, 너비 힌트의 핵심 사용 사례는 이미지 애셋의 리소스 선택을 사용 설정하는 것입니다. 그러나 유형과 관계없이 모든 하위 리소스에 동일한 힌트가 전송됩니다. 예를 들어 CSS 및 JavaScript 요청도 동일한 정보를 가져오며 이러한 리소스를 최적화하는 데도 사용할 수 있습니다.

  5. 일부 이미지 요청에서 너비가 보고되지 않는 이유는 무엇인가요? 사이트에서 이미지의 고유 크기를 사용하기 때문에 브라우저가 의도한 디스플레이 너비를 알지 못할 수 있습니다. 따라서 이러한 요청과 '표시 너비'가 없는 요청(예: JavaScript 리소스)의 경우 너비 힌트가 생략됩니다. 너비 힌트를 받으려면 이미지에 크기 값을 지정해야 합니다.

  6. <좋아하는 힌트 삽입>은 어떻나요? ServiceWorker를 사용하면 개발자가 모든 발신 요청을 가로채고 수정 (예: 새 헤더 추가)할 수 있습니다. 예를 들어 NetInfo 기반 정보를 쉽게 추가하여 현재 연결 유형을 나타낼 수 있습니다. 'ServiceWorker를 사용한 기능 보고'를 참고하세요. Chrome에 제공되는 '네이티브' 힌트 (DPR, 너비, 리소스 너비)는 순수 SW 기반 구현으로 인해 모든 이미지 요청이 지연되므로 브라우저에 구현됩니다.

  7. 자세히 알아보고 데모를 더 보려면 어디로 가야 하나요? 설명 문서를 확인하고 의견이나 다른 질문이 있으면 언제든지 GitHub에서 문제를 열어 문의해 주세요.