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

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

배경: 색상 대비가 좋지 않음

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

웹에서 발생하는 일반적인 접근성 문제 목록입니다. 저대비 텍스트가 가장 흔한 문제입니다.

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

DevTools를 사용하여 대비 문제를 찾고 이해하고 수정하기

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

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

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

색약

20명 중 약 1명은 색각이상 (일반적으로 '색맹'이라고 함)을 앓고 있습니다. 이러한 장애는 색상의 차이를 구별하기 어렵게 만들고 이로 인해 대비 문제가 커질 수 있습니다.

색각 결함이 시뮬레이션되지 않은 녹은 크레용의 다채로운 사진입니다.
색각 결함이 시뮬레이션되지 않은 다채로운 색상의 녹은 크레용 사진입니다.
ALT_TEXT_HERE
색맹 시뮬레이션이 녹은 크레용의 다채로운 그림에 미치는 영향입니다.
녹은 크레용의 다채로운 사진에 제2색맹을 시뮬레이션한 효과
제2색맹 시뮬레이션이 녹은 크레용의 다채로운 그림에 미치는 영향입니다.
녹은 크레용의 다채로운 사진에 적색맹 시뮬레이션을 적용한 결과
적색맹 시뮬레이션이 녹은 크레용의 다채로운 색감에 미치는 영향입니다.
제3색맹 시뮬레이션이 녹은 크레용의 화려한 사진에 미치는 영향입니다.
녹은 크레용의 다채로운 사진에 트리탄토피아를 시뮬레이션한 효과입니다.

시력이 정상인 개발자는 DevTools에서 시각적으로 적절하다고 생각하는 색상 쌍에 나쁜 대비율을 표시할 수 있습니다. 명암비 수식에서 이러한 색맹을 고려하기 때문에 이러한 현상이 발생합니다. 경우에 따라 저대비 텍스트도 읽을 수 있지만 시각 장애가 있는 사용자는 읽을 수 없습니다.

Google은 디자이너와 개발자가 이러한 시각 장애가 자체 웹 앱에 미치는 영향을 시뮬레이션할 수 있도록 하여 누락된 부분을 제공하고자 합니다. 이제 DevTools를 사용하여 대비 문제를 찾고 수정할 뿐만 아니라 이해할 수도 있습니다.

HTML, CSS, SVG, C++로 색약 시뮬레이션

이 기능의 Blink 렌더러 구현을 자세히 살펴보기 전에 웹 기술을 사용하여 상응하는 기능을 구현하는 방법을 이해하는 것이 좋습니다.

이러한 각 색각 결함 시뮬레이션을 페이지 전체를 덮는 오버레이로 생각할 수 있습니다. 웹 플랫폼에는 이를 실행하는 방법이 있습니다. 바로 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의 배수와 상수 이동 값의 다섯 번째 값이 포함됩니다. 4개의 행이 있습니다. 행렬의 첫 번째 행은 새 Red 값, 두 번째 행은 Green, 세 번째 행은 Blue, 마지막 행 Alpha를 계산하는 데 사용됩니다.

이 예의 정확한 수치가 어디에서 비롯되었는지 궁금할 수 있습니다. 이 색상 매트릭스가 제2색맹에 근접한 근사치인 이유는 무엇인가요? 정답은 바로 과학입니다. 이 값은 Machado, Oliveira, Fernandes의 생리학적으로 정확한 색각이상 시뮬레이션 모델을 기반으로 합니다.

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

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

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

특이 사례는 제외하고 이 접근 방식의 주요 문제는 프로그래매틱 방식으로 페이지를 관찰 가능하게 변경한다는 점입니다. 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 종속 항목 방지

두 번째 부분부터 시작하겠습니다. 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를 유효한 독립형 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 네임스페이스 선언입니다. 두 번째 추가 사항은 <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>

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

지금까지는 웹 기술을 사용하여 시각 장애를 시뮬레이션하는 방법만 알아봤습니다. 흥미롭게도 Blink 렌더러의 최종 구현은 실제로 매우 유사합니다. 다음은 동일한 기법을 기반으로 특정 필터 정의로 데이터 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 필터의 모든 기능을 이용할 수 있습니다. 현재 Blink 렌더기 기능을 구현하고 있지만, 이 작업은 웹 플랫폼을 활용하여 구현되고 있습니다.

이제 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처럼 동작하지만 이름이 --internal-devtools-filter와 같이 다른 새로운 Chrome 내부 CSS 속성을 만드는 것이었습니다. 그런 다음 이 속성이 DevTools 또는 DOM의 계산된 스타일에 표시되지 않도록 하는 특수 로직을 추가할 수 있습니다. 필요한 요소인 루트 요소에서만 작동하도록 할 수도 있습니다. 하지만 이 솔루션은 이상적이지 않습니다. filter에 이미 있는 기능을 중복으로 제공하게 되며, 이 비표준 속성을 숨기려고 노력하더라도 웹 개발자가 이를 발견하고 사용하기 시작할 수 있으므로 웹 플랫폼에 좋지 않습니다. DOM에서 관찰할 수 없지만 CSS 스타일을 적용하는 다른 방법이 필요합니다. 도와주실 수 있을까요?

CSS 사양에는 사용하는 시각적 형식 지정 모델을 소개하는 섹션이 있으며 여기서 주요 개념 중 하나는 뷰포트입니다. 사용자가 웹페이지를 참고하는 시각적 뷰입니다. 이와 밀접하게 관련된 개념은 초기 포함 블록으로, 이는 사양 수준에서만 존재하는 스타일 지정 가능한 뷰포트 <div>와 유사합니다. 사양에서는 이 '뷰포트' 개념을 곳곳에서 참조합니다. 예를 들어 콘텐츠가 표시되지 않을 때 브라우저에서 스크롤바를 표시하는 방법을 알고 계신가요? 이는 모두 이 '표시 영역'을 기반으로 CSS 사양에 정의되어 있습니다.

viewport는 Blink 렌더러 내에서도 구현 세부정보로 존재합니다. 다음은 사양에 따라 기본 뷰포트 스타일을 적용하는 코드입니다.

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;
}

이 코드가 뷰포트 (더 정확하게는 초기 포함 블록)의 z-index, display, position, overflow를 처리하는지 확인하기 위해 C++ 또는 Blink의 스타일 엔진의 복잡성을 이해할 필요는 없습니다. 이것들은 모두 CSS에서 익숙할 수 있는 개념입니다. CSS 속성으로 직접 변환되지는 않지만, 전체적으로 이 viewport 객체는 DOM 요소와 마찬가지로 Blink 내에서 CSS를 사용하여 스타일을 지정할 수 있는 요소라고 생각할 수 있습니다. 단, DOM의 일부가 아닙니다.

이렇게 하면 원하는 결과를 얻을 수 있습니다. 관찰 가능한 페이지 스타일이나 DOM을 방해하지 않고 렌더링에 시각적으로 영향을 미치는 viewport 객체에 filter 스타일을 적용할 수 있습니다.

결론

지금까지의 여정을 요약하면, C++ 대신 웹 기술을 사용하여 프로토타입을 빌드한 다음 일부를 Blink 렌더러로 이동하는 작업을 시작했습니다.

  • 먼저 데이터 URL을 인라인으로 삽입하여 프로토타입을 더 독립형으로 만들었습니다.
  • 그런 다음 로드를 특별히 처리하여 이러한 내부 데이터 URL을 CSP에 적합하게 만들었습니다.
  • 스타일을 Blink 내부 viewport로 이동하여 구현을 DOM과 무관하게 만들고 프로그래매틱 방식으로 관찰할 수 없도록 했습니다.

이 구현의 고유한 점은 HTML/CSS/SVG 프로토타입이 최종 기술 설계에 영향을 미쳤다는 것입니다. Blink Renderer 내에서도 Web Platform을 사용하는 방법을 찾았습니다.

자세한 배경 정보는 설계 제안서 또는 모든 관련 패치를 참조하는 Chromium 추적 버그를 참고하세요.

미리보기 채널 다운로드

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

Chrome DevTools 팀에 문의하기

다음 옵션을 사용하여 DevTools와 관련된 새로운 기능, 업데이트 또는 기타 사항을 논의하세요.