Blink Renderer의 색각이상 시뮬레이션

마티아스 비넨스
마티아스 바인스

이 문서에서는 DevTools와 Blink Renderer에서 색각 이상 시뮬레이션을 구현한 이유와 방법을 설명합니다.

배경: 잘못된 색상 대비

저대비 텍스트는 웹에서 가장 일반적으로 자동으로 감지되는 접근성 문제입니다.

웹의 일반적인 접근성 문제 목록입니다. 저대비 텍스트가 가장 일반적인 문제입니다.

WebAIM의 상위 100만 개 웹사이트에 대한 접근성 분석에 따르면 홈페이지의 86% 이상이 낮은 대비를 나타냅니다. 평균적으로 각 홈페이지에는 36개의 고유한 인스턴스가 있는 저대비 텍스트가 있습니다.

DevTools를 사용한 대비 문제 파악, 파악, 해결

Chrome DevTools를 사용하면 개발자와 디자이너가 대비를 개선하고 웹 앱에 더 접근하기 쉬운 색 구성표를 선택할 수 있습니다.

최근 이 목록에 새로운 도구를 추가했으며 다른 도구와는 약간 다릅니다. 위의 도구는 주로 명암비 정보를 표시하고 이를 수정하는 옵션을 제공하는 데 중점을 둡니다. 우리는 DevTools에 개발자가 이 문제 공간을 더 깊이 understanding할 수 있는 방법이 여전히 없다는 것을 깨달았습니다. 이 문제를 해결하기 위해 DevTools 렌더링 탭에서 시각 결함 시뮬레이션을 구현했습니다.

Puppeteer에서 page.emulateVisionDeficiency(type) API를 사용하면 이러한 시뮬레이션을 프로그래매틱 방식으로 사용 설정할 수 있습니다.

색약

20명 중 약 1명은 색맹('색맹'이라고도 함)을 앓고 있습니다. 이러한 장애는 색상을 구별하기 어렵게 하므로 대비 문제가 커질 수 있습니다.

색각 이상이 재현되지 않은 녹은 크레용의 다채로운 그림
색각 이상이 시뮬레이션되지 않은 다채로운 녹은 크레용의 사진
ALT_TEXT_HERE
색색의 녹은 크레용 사진에 색맹 시뮬레이션이 미치는 영향
제2색맹을 모방하여 녹은 크레용의 다채로운 그림에 미치는 영향.
제2색맹 시뮬레이션이 다채로운 색상의 녹은 크레용 사진에 미치는 영향
제1색맹을 시뮬레이션하여 녹은 크레용의 다채로운 그림에 미치는 영향
적색맹 시뮬레이션이 다채로운 색상의 녹은 크레용 사진에 미치는 영향
제3색맹 시뮬레이션이 녹은 크레용의 다채로운 사진에 미치는 영향
제3색맹 시뮬레이션이 다채로운 색상의 녹은 크레용 사진에 미치는 영향

일반 시각을 가진 개발자라면 DevTools가 시각적으로 괜찮은 색상 쌍의 대비율을 잘못 표시하는 것을 볼 수 있습니다. 이는 명암비 공식에서 이러한 색각의 결함을 고려하기 때문입니다. 사용자는 경우에 따라 저대비 텍스트를 계속 읽을 수도 있지만 시각 장애가 있는 사용자에게는 이러한 권한이 없습니다.

디자이너와 개발자가 자신의 웹 앱에 이러한 시각 장애의 영향을 시뮬레이션할 수 있도록 함으로써 Google은 빠진 내용을 제공하려고 합니다. 즉, DevTools를 통해 대비 문제를 찾아 수정할 수 있을 뿐만 아니라 이제 개발자는 이러한 문제를 이해할 수도 있습니다.

HTML, CSS, SVG 및 C++로 색각 이상 시뮬레이션

이 기능의 블링크 렌더러 구현에 대해 자세히 알아보기 전에, 웹 기술을 사용하여 동일한 기능을 구현하는 방법을 이해하면 도움이 됩니다.

이러한 각 색약 시뮬레이션은 페이지 전체를 덮는 오버레이로 생각할 수 있습니다. 웹 플랫폼에는 이를 위한 방법이 있습니다. 바로 CSS 필터입니다. CSS filter 속성을 사용하면 blur, contrast, grayscale, hue-rotate 등 사전 정의된 필터 함수를 사용할 수 있습니다. 더 세밀한 제어를 위해 filter 속성은 맞춤 SVG 필터 정의를 가리킬 수 있는 URL도 허용합니다.

<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

위의 예에서는 색상 매트릭스를 기반으로 맞춤 필터 정의를 사용합니다. 개념적으로 모든 픽셀의 [Red, Green, Blue, Alpha] 색상 값은 행렬 곱셈을 통해 새로운 색상 [R′, G′, B′, A′]을 생성합니다.

행렬의 각 행에는 5개의 값이 포함됩니다. R, G, B, A의 승수(왼쪽에서 오른쪽으로)와 5번째 값(상수 변화값의 경우)입니다. 4개의 행이 있습니다. 행렬의 첫 번째 행은 새 빨간색 값을 계산하는 데 사용되고, 두 번째 행인 Green, 세 번째 행인 Blue, 마지막 행인 알파가 계산됩니다.

이 예에서 정확한 수치가 어디서 나온 것인지 궁금하실 것입니다. 이 색 매트릭스가 제2색맹의 적절한 근사값인 이유는 무엇인가요? 정답은 바로 과학입니다. 이 값은 마차도, 올리베이라, 페르난데스의 생리학적으로 정확한 색약 결핍 시뮬레이션 모델을 기반으로 합니다.

어쨌든 이 SVG 필터가 있으므로 CSS를 사용하여 페이지의 임의 요소에 적용할 수 있습니다. 다른 시각 장애에도 같은 패턴을 반복할 수 있습니다. 다음은 이에 대한 데모입니다.

원한다면 다음과 같이 DevTools 기능을 빌드할 수 있습니다. 사용자가 DevTools UI의 시각 장애를 에뮬레이션할 때 검사된 문서에 SVG 필터를 삽입한 다음 루트 요소에 필터 스타일을 적용합니다. 그러나 이러한 접근 방식에는 몇 가지 문제가 있습니다.

  • 페이지의 루트 요소에 이미 필터가 있을 수 있으며, 이 경우 Google 코드에서 이를 재정의할 수 있습니다.
  • 페이지에 이미 id="deuteranopia"가 있는 요소가 있어 필터 정의와 일치하지 않을 수 있습니다.
  • 페이지는 특정 DOM 구조를 사용할 수 있으며 <svg>를 DOM에 삽입하면 이러한 가정을 위반할 수 있습니다.

예외적인 경우를 제외하고, 이 접근 방식의 주요 문제는 프로그래밍 방식으로 관찰 가능한 변경사항을 페이지에 적용한다는 점입니다. DevTools 사용자가 DOM을 검사할 때 추가한 적이 없는 <svg> 요소나 작성한 적이 없는 CSS filter가 갑자기 표시될 수 있습니다. 혼란스러울 수 있습니다. DevTools에서 이 기능을 구현하려면 이러한 단점이 없는 솔루션이 필요합니다.

사용자 경험을 줄이는 방법을 알아보겠습니다. 이 솔루션에는 숨겨야 하는 두 부분이 있습니다. 1) filter 속성이 있는 CSS 스타일, 2) 현재 DOM의 일부인 SVG 필터 정의입니다.

<!-- Part 1: the CSS style with the filter property -->
<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

문서 내 SVG 종속 항목 방지

파트 2부터 시작해 보겠습니다. DOM에 SVG를 추가하지 않도록 하려면 어떻게 해야 할까요? 한 가지 아이디어는 별도의 SVG 파일로 이동하는 것입니다. 위의 HTML에서 <svg>…</svg>를 복사하여 filter.svg로 저장할 수 있지만 먼저 몇 가지를 변경해야 합니다. HTML의 인라인 SVG는 HTML 파싱 규칙을 따릅니다. 즉, 경우에 따라 속성 값을 따옴표로 묶지 않아도 됩니다. 그러나 별도의 파일에 포함된 SVG는 유효한 XML이어야 하며 XML 파싱은 HTML보다 훨씬 엄격합니다. 다음은 SVG-in-HTML 스니펫입니다.

<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

이 독립 실행형 SVG (및 XML)를 유효하게 만들려면 몇 가지 변경사항을 적용해야 합니다. 어느 게 좋을까요?

<svg xmlns="http://www.w3.org/2000/svg">
 
<filter id="deuteranopia">
   
<feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000"
/>
 
</filter>
</svg>

첫 번째 변경사항은 상단의 XML 네임스페이스 선언입니다. 두 번째로 추가하는 요소는 이른바 'solidus'입니다. <feColorMatrix> 태그를 나타내는 슬래시는 요소를 열고 닫습니다. 이 마지막 변경은 실제로 필요하지 않지만 (대신 명시적인 </feColorMatrix> 닫는 태그를 고수할 수 있음) XML과 SVG-in-HTML에서 모두 이 /> 약식을 지원하므로 활용할 수도 있습니다.

어쨌든 이렇게 변경하면 마침내 이 파일을 유효한 SVG 파일로 저장하고 HTML 문서의 CSS filter 속성 값에서 가리킬 수 있습니다.

<style>
  :root {
    filter: url(filters.svg#deuteranopia);
  }
</style>

더 이상 문서에 SVG를 삽입할 필요가 없습니다. 그것은 이미 훨씬 나아졌습니다. 하지만 이제 별도의 파일에 의존합니다. 여전히 종속 항목입니다. 어떻게 하면 없앨 수 있을까요?

그 결과, 실제로 파일이 필요하지 않습니다. 데이터 URL을 사용하여 URL 내의 전체 파일을 인코딩할 수 있습니다. 이렇게 하려면 말 그대로 이전에 있었던 SVG 파일의 콘텐츠를 가져와서 data: 접두사를 추가하고 적절한 MIME 유형을 구성합니다. 그러면 동일한 SVG 파일을 나타내는 유효한 데이터 URL이 완성됩니다.

data:image/svg+xml,
  <svg xmlns="http://www.w3.org/2000/svg">
    <filter id="deuteranopia">
      <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                             0.280  0.673  0.047  0.000  0.000
                            -0.012  0.043  0.969  0.000  0.000
                             0.000  0.000  0.000  1.000  0.000" />
    </filter>
  </svg>

이렇게 하면 더 이상 파일을 어디에도 저장할 필요가 없고, HTML 문서에서 사용하기 위해 디스크에서 또는 네트워크를 통해 로드할 필요가 없다는 장점이 있습니다. 따라서 이전처럼 파일 이름을 참조하는 대신 이제 데이터 URL을 가리킬 수 있습니다.

<style>
  :root {
    filter: url('data:image/svg+xml,\
      <svg xmlns="http://www.w3.org/2000/svg">\
        <filter id="deuteranopia">\
          <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000\
                                 0.280  0.673  0.047  0.000  0.000\
                                -0.012  0.043  0.969  0.000  0.000\
                                 0.000  0.000  0.000  1.000  0.000" />\
        </filter>\
      </svg>#deuteranopia');
  }
</style>

이전과 마찬가지로 사용하려는 필터의 ID를 URL 끝에 지정합니다. URL에서 SVG 문서를 Base64로 인코딩할 필요는 없습니다. 그렇게 하면 가독성이 저하되고 파일 크기가 늘어날 뿐입니다. 데이터 URL의 줄바꿈 문자가 CSS 문자열 리터럴을 종료하지 않도록 각 줄 끝에 백슬래시를 추가했습니다.

지금까지는 웹 기술을 사용하여 시각 장애를 시뮬레이션하는 방법에 대해서만 이야기했습니다. 흥미롭게도, Blink Renderer의 최종 구현은 실제로 매우 유사합니다. 다음은 동일한 기법을 기반으로 지정된 필터 정의로 데이터 URL을 만들기 위해 추가한 C++ 도우미 유틸리티입니다.

AtomicString CreateFilterDataUrl(const char* piece) {
  AtomicString url =
      "data:image/svg+xml,"
        "<svg xmlns=\"http://www.w3.org/2000/svg\">"
          "<filter id=\"f\">" +
            StringView(piece) +
          "</filter>"
        "</svg>"
      "#f";
  return url;
}

이 변수를 사용해 필요한 모든 필터를 생성하는 방법은 다음과 같습니다.

AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
  switch (vision_deficiency) {
    case VisionDeficiency::kAchromatopsia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kBlurredVision:
      return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
    case VisionDeficiency::kDeuteranopia:
      return CreateFilterDataUrl(
          "<feColorMatrix values=\""
          " 0.367  0.861 -0.228  0.000  0.000 "
          " 0.280  0.673  0.047  0.000  0.000 "
          "-0.012  0.043  0.969  0.000  0.000 "
          " 0.000  0.000  0.000  1.000  0.000 "
          "\"/>");
    case VisionDeficiency::kProtanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kTritanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kNoVisionDeficiency:
      NOTREACHED();
      return "";
  }
}

이 기법으로 인해, 아무것도 다시 구현하거나 방법을 다시 개발하지 않고도 SVG 필터의 모든 기능에 액세스할 수 있습니다. Google은 블링크 렌더러 기능을 구현하고 있지만 이 기능은 웹 플랫폼을 활용하고 있습니다.

지금까지 SVG 필터를 구성하고 CSS filter 속성 값 내에서 사용할 수 있는 데이터 URL로 변환하는 방법을 알아봤습니다. 이 기법에 문제가 있다고 생각할 수 있나요? 데이터 URL을 차단하는 Content-Security-Policy이 대상 페이지에 있을 수 있으므로 모든 경우에 로드되는 데이터 URL에 실제로 의존할 수는 없습니다. 최종 블링크 수준 구현에서는 로드 중에 이러한 '내부' 데이터 URL에 대해 CSP를 우회하도록 특별히 주의를 기울여야 합니다.

특이한 사례를 제외하고 몇 가지 좋은 진전을 이루었습니다. 동일한 문서에 있는 인라인 <svg>에 더 이상 의존하지 않으므로 솔루션을 단일 독립 실행형 CSS filter 속성 정의로 줄였습니다. 좋습니다. 이제 이것도 제거해 보겠습니다.

문서 내 CSS 종속 항목 피하기

지금까지 다룬 내용은 다음과 같습니다.

<style>
  :root {
    filter: url('data:…');
  }
</style>

우리는 여전히 이 CSS filter 속성에 의존합니다. 이 속성은 실제 문서에서 filter를 재정의하여 작업을 손상시킬 수 있습니다. 또한 DevTools에서 계산된 스타일을 검사할 때도 표시되므로 혼란스러울 수 있습니다. 이러한 문제를 방지하려면 어떻게 해야 하나요? 개발자가 프로그래매틱 방식으로 관찰할 수 없도록 문서에 필터를 추가하는 방법을 찾아야 합니다.

생각해 낸 한 가지 아이디어는 filter처럼 작동하지만 이름이 다른 새로운 Chrome 내부 CSS 속성(예: --internal-devtools-filter)을 만드는 것이었습니다. 그런 다음, 이 속성이 DevTools 또는 DOM의 계산된 스타일에 표시되지 않도록 특수 로직을 추가할 수 있습니다. 그것이 필요한 한 요소, 즉 루트 요소에만 작동하도록 할 수도 있습니다. 그러나 이 솔루션은 바람직하지 않습니다. filter에 이미 존재하는 기능을 복제합니다. 비표준 속성을 숨기려고 해도 웹 개발자가 해당 속성을 발견하고 사용하기 시작할 수 있으므로 웹 플랫폼에는 좋지 않습니다. DOM에서 관찰할 수 없는 CSS 스타일을 적용하는 다른 방법이 필요합니다. 좋은 방법이 있을까요?

CSS 사양에 사용되는 시각적 형식 지정 모델을 소개하는 섹션이 있으며 이 섹션의 주요 개념 중 하나는 표시 영역입니다. 이 보기는 사용자가 웹페이지를 참조하는 시각적 뷰입니다. 밀접하게 관련된 개념은 이니셜 포함 블록입니다. 이는 사양 수준에서만 존재하는 스타일이 지정된 표시 영역 <div>과 비슷한 개념입니다. 사양에서는 장소 전체에 걸쳐 이러한 '표시 영역' 개념을 지칭합니다. 예를 들어 콘텐츠가 맞지 않을 때 브라우저에서 스크롤바가 어떻게 표시되는지 알고 있나요? 이는 모두 이 '표시 영역'을 기반으로 CSS 사양에 정의되어 있습니다.

viewport는 블링크 렌더러 내에도 구현 세부정보로 존재합니다. 사양에 따라 기본 표시 영역 스타일을 적용하는 코드는 다음과 같습니다.

scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
  scoped_refptr<ComputedStyle> viewport_style =
      InitialStyleForElement(GetDocument());
  viewport_style->SetZIndex(0);
  viewport_style->SetIsStackingContextWithoutContainment(true);
  viewport_style->SetDisplay(EDisplay::kBlock);
  viewport_style->SetPosition(EPosition::kAbsolute);
  viewport_style->SetOverflowX(EOverflow::kAuto);
  viewport_style->SetOverflowY(EOverflow::kAuto);
  // …
  return viewport_style;
}

C++나 Blink 스타일 엔진의 복잡한 문제를 이해하지 않아도 이 코드가 표시 영역의 z-index, display, position, overflow를 표시 영역 (또는 초기에 포함된 블록)의 더 정확하게 처리하는지 확인할 수 있습니다. 모두 CSS에서 익숙하실 수 있는 개념입니다. CSS 속성으로 직접 변환되지는 않는 컨텍스트 스태킹과 관련된 다른 마법이 있습니다. 하지만 전반적으로 이 viewport 객체는 DOM의 일부가 아니라는 점을 제외하고 DOM 요소처럼 Blink 내에서 CSS를 사용하여 스타일을 지정할 수 있는 것으로 생각할 수 있습니다.

덕분에 우리가 원하는 결과를 얻을 수 있었습니다. filter 스타일을 viewport 객체에 적용할 수 있습니다. 그러면 관찰 가능한 페이지 스타일이나 DOM을 어떤 식으로든 방해하지 않고 렌더링에 시각적으로 영향을 미칠 수 있습니다.

결론

지금까지의 간단한 여정을 요약하자면, 먼저 C++ 대신 웹 기술을 사용하여 프로토타입을 빌드한 다음, 프로토타입의 일부를 Blink Renderer로 옮기는 작업을 시작했습니다.

  • 우리는 먼저 데이터 URL을 인라인 처리하여 보다 독립된 프로토타입을 만들었습니다.
  • 그런 다음 로드의 특수 대소문자 표기를 통해 내부 데이터 URL을 CSP 친화적으로 만들었습니다.
  • 스타일을 Blink-internal viewport로 이동하여 DOM에 의존하지 않고 프로그래매틱 방식으로 관찰할 수 없도록 했습니다.

이 구현에서 독특한 점은 HTML/CSS/SVG 프로토타입이 최종 기술 디자인에 영향을 주었다는 점입니다. Blink Renderer 내에서도 웹 플랫폼을 사용할 수 있는 방법을 찾았습니다.

자세한 내용은 설계 제안 또는 모든 관련 패치를 참조하는 Chromium 추적 버그를 확인하세요.

미리보기 채널 다운로드

Chrome Canary, 개발자 또는 베타를 기본 개발 브라우저로 사용하는 것이 좋습니다. 이러한 Preview 채널을 통해 최신 DevTools 기능에 액세스하고 최첨단 웹 플랫폼 API를 테스트하며 사용자보다 먼저 사이트에서 문제를 찾을 수 있습니다.

Chrome DevTools 팀에 문의하기

다음 옵션을 사용하여 게시물의 새로운 기능과 변경사항 또는 DevTools와 관련된 다른 모든 것에 대해 논의합니다.

  • crbug.com을 통해 제안이나 의견을 제출해 주세요.
  • DevTools에서 옵션 더보기   더보기   > 도움말 > DevTools 문제 보고를 사용하여 DevTools 문제를 신고합니다.
  • @ChromeDevTools로 트윗을 보냅니다.
  • DevTools의 새로운 기능 YouTube 동영상 또는 DevTools 팁 YouTube 동영상에 의견을 남겨주세요.