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

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

배경: 색상 대비 불량

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

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

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

DevTools를 사용하여 대비 문제를 찾고 파악하고 수정하기

Chrome DevTools를 사용하면 개발자와 디자이너가 대비를 개선하고 웹 앱의 색 구성표에 더 쉽게 액세스할 수 있습니다.

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

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

색각 이상

20명 중 약 1명이 색각 이상('색맹'이라고도 함)을 앓고 있습니다. 이러한 손상으로 인해 서로 다른 색상을 구별하기가 어려워져 대비 문제가 커질 수 있습니다.

<ph type="x-smartling-placeholder">
</ph> 색각 결함이 시뮬레이션되지 않은 녹은 크레용의 다채로운 사진입니다. <ph type="x-smartling-placeholder">
</ph> 색각 결함이 시뮬레이션되지 않은 다채로운 색상의 녹은 크레용 사진입니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> ALT_TEXT_HERE <ph type="x-smartling-placeholder">
</ph> 색색맹 시뮬레이션이 녹은 크레용의 다채로운 그림에 미치는 영향
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> 녹은 크레용의 다채로운 사진에 제2색맹 시뮬레이션이 미치는 영향입니다. <ph type="x-smartling-placeholder">
</ph> 녹은 크레용의 다채로운 사진에 제2색맹 시뮬레이션이 미치는 영향입니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> 적색맹 시뮬레이션이 녹은 크레용의 화려한 사진에 미치는 영향입니다. <ph type="x-smartling-placeholder">
</ph> 적색맹 시뮬레이션이 녹은 크레용의 화려한 사진에 미치는 영향입니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> 제3색맹 시뮬레이션이 녹은 크레용의 화려한 사진에 미치는 영향입니다. <ph type="x-smartling-placeholder">
</ph> 제3색맹 시뮬레이션이 녹은 크레용의 화려한 사진에 미치는 영향입니다.

시력이 정상인 개발자라면 DevTools에서 시각적으로 문제 없어 보이는 색상 쌍의 명암비가 잘못 표시되는 것을 볼 수 있습니다. 이는 명암비 공식이 이러한 색각 결함을 고려하기 때문입니다. 경우에 따라 사용자가 저대비 텍스트를 읽을 수 있지만 시각 장애가 있는 사람에게는 이러한 권한이 없습니다.

디자이너와 개발자가 자신의 웹 앱에서 이러한 시각 결함의 영향을 시뮬레이션할 수 있도록 함으로써, 우리는 누락된 부분을 제공하는 것을 목표로 하고 있습니다. 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′]이 생성됩니다.

행렬의 각 행에는 (왼쪽에서 오른쪽으로) R, G, B, A에 대한 승수와 상수 이동 값에 대한 5번째 값, 이렇게 5개의 값이 포함됩니다. 4개의 행이 있습니다. 행렬의 첫 번째 행은 새 Red 값, 두 번째 행은 Green, 세 번째 행은 Blue, 마지막 행 Alpha를 계산하는 데 사용됩니다.

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

어쨌든 이제 이 SVG 필터가 있으므로 CSS를 사용하여 페이지의 임의 요소에 이 필터를 적용할 수 있습니다. 다른 시력 결핍에도 같은 패턴을 반복할 수 있습니다. 예를 들면 다음과 같습니다.

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

  • 페이지의 루트 요소에 필터가 이미 있을 수 있습니다. 그러면 코드에서 이를 재정의할 수 있습니다.
  • 페이지에 이미 필터 정의와 충돌하는 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 렌더기의 최종 구현은 실제로 매우 유사합니다. 다음은 동일한 기법을 기반으로 특정 필터 정의로 데이터 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 필터의 모든 기능을 이용할 수 있습니다. 현재 블링크 렌더기 기능을 구현하고 있지만, 이 작업은 웹 플랫폼을 활용하여 구현되고 있습니다.

지금까지 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 사양에는 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;
}

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

이를 통해 정확히 원하는 결과를 얻을 수 있습니다. filter 스타일을 viewport 객체에 적용할 수 있습니다. 이는 관찰 가능한 페이지 스타일이나 DOM을 어떤 식으로든 방해하지 않고 렌더링에 시각적으로 영향을 미칩니다.

결론

여기서의 작은 여정을 요약하자면, 우리는 C++ 대신 웹 기술을 사용하여 프로토타입을 빌드하는 것으로 시작한 다음, 그 일부를 Blink Renderer로 옮기는 작업을 시작했습니다.

  • 우리는 먼저 데이터 URL을 인라인 처리하여 프로토타입을 보다 독립적이게 만들었습니다.
  • 그런 다음 로드를 특별한 경우에 사용하여 내부 데이터 URL을 CSP에 맞게 만들었습니다.
  • 스타일을 Blink 내부 viewport로 이동하여 DOM에 구애받지 않고 프로그래매틱 방식으로 관찰할 수 없도록 구현을 만들었습니다.

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

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

미리보기 채널 다운로드

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

Chrome DevTools 팀에 문의하기

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

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