무지개빛총천연배경색에서안전한색대비를가지는전경색으로가독성을확보하기대작전

BSK·2024년 2월 29일
4
post-thumbnail

색과 관련된 문제들은 늘 쉬워 보이면서도 어려운 문제입니다. 당장의 말초적인 감각으로 인지할 수 있기에 직관적으로 풀어낼 수 있을 것 같지만, 더 깊게 들어갈수록 깔려있는 개념들의 복잡함에 질색을 하곤 합니다.
최초의 전자식 컴퓨터가 만들어진 지 4/5세기가 되어가고 있지만 인류는 아직도 색을 표현하는 데에 고민을 하고 있습니다. 물론, 우리가 일반적으로 접하는 화면의 색상 정보는 R,G,B(때로는 W가 더해진)의 색을 낼 수 있는 작은 단위들의 조합이지만, 우리의 눈이 세상을 인지하는 방법과는 또 다른 이야기입니다. (sRGB color gamut에서의 blue-yellow gradient에서의 gray-dead-zone 문제를 보시면 이해가 쉬울 수 있습니다.)

UI를 만드는 과정에서, 배경색(주로 사용자나 다른 이미지로부터 결정되는 배경색)에 따른 적절한 전경색(주로 흰색, 검은색 등의 텍스트 색상)을 결정하는 과정은 흔한 일입니다. 나이브한 방법으로는 hex값의 평균을 통해 RGB 채널에서 grayscale로의 매핑을 통한 후에 이를 기반으로 전경색을 구할 수 있을 것입니다.

이를 React에서 간단히 아래와 같이 구현해 보았습니다.

import { FC } from 'react';

import './style.css';

type HexString = `#${string}`;
type RenderBoxProps = {
  bgColor: HexString;
};

const hexToRGB = (hexString: HexString): [number, number, number] => {
  const parseHex = (x) => parseInt(x, 16);
  return [
    parseHex(hexString.slice(1, 3)),
    parseHex(hexString.slice(3, 5)),
    parseHex(hexString.slice(5, 7)),
  ];
};

const add = (x, y) => x + y;

const rgbToHex = (rgb: [number, number, number]): HexString => {
  const convertChannel = (x: number) => x.toString(16).padStart(2, '0');
  return `#${rgb.map(convertChannel).reduce(add)}`;
};

const RenderBox = ({ bgColor }: RenderBoxProps) => {
  const rgb = hexToRGB(bgColor);

  // approach 1. naive average
  const foregroundColor = rgb.reduce(add) / 3 > 127 ? '#000000' : '#ffffff';

  return (
    <div
      className={'box'}
      style={{
        backgroundColor: bgColor,
        color: foregroundColor,
      }}
    >
      foreground
    </div>
  );
};

export const App: FC<{ name: string }> = ({ name }) => {
  return (
    <div className={'container'}>
      <RenderBox bgColor={'#ff0000'} />
      <RenderBox bgColor={'#00ff00'} />
      <RenderBox bgColor={'#0000ff'} />
    </div>
  );
};

그러나 이 방법에는 문제가 있습니다. 초록 계열 색상에서 충분한 대비를 만들지 못하는 것입니다.

우리의 안구에는 원추세포와 간상세포가 위치해 있고, 이들은 외부에서 들어오는 빛의 자극의 정도를 전기 신호로 변환하여 뇌로 전달합니다.

그리고 이 중 색의 인지를 담당하는 원추세포에는 세 종류가 있는데, 이들의 수는 같지 않습니다. 따라서 뇌가 인지하는 밝기에는 이를 고려한 내용을 반영해야 합니다. CIE XYZ(혹은 CIE 1931) 모델은 평균적인 인간의 시야각 2' 내외의 원추세포의 반응(표준 색 관찰자)을 기준으로 설계되었습니다. 이는 RGB를 계산되어지는 색상 공간보다 더 우리가 인지하는 색을 잘 표현할 수 있습니다. RGB에서 CIE XYZ로의 변환 식은 다음과 같습니다.

[XYZ]=[SrXrSgXgSbXbSrYrSgYgSbYbSrZrSgZgSbZb][RGB]\begin{bmatrix} X \\ Y \\ Z \end{bmatrix} = \begin{bmatrix} S_rX_r & S_gX_g & S_bX_b \\ S_rY_r & S_gY_g & S_bY_b \\ S_rZ_r & S_gZ_g & S_bZ_b \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}

아래의 사이트에서 가운데 들어갈 3x3 행렬의 가중치를 구할 수 있습니다. 본문에서는 sRGB를 예시로 들겠습니다. 그리고, 우리가 필요한 색상의 밝기에 해당하는 것은 Y의 값 뿐이므로, 다음의 코드와 같이 구할 수 있습니다.

 /*matrix for sRGB to XYZ
 0.4124564  0.3575761  0.1804375
 0.2126729  0.7151522  0.0721750
 0.0193339  0.1191920  0.9503041
 */
const luminanceFromRGB = ([red,green,blue]:[number,number,number])=>
  0.2126729 * red + 0.7151522 * green + 0.0721750 * blue

이러한 변환은 luma coding이라 합니다. 이 값의 중간값(127)을 기준으로, 전경색을 결정할 수 있습니다.

const XYZRenderBox = ({
  bgColor,
  label,
}: RenderBoxProps & { label?: boolean }) => {
  const xyz = rgbToluminance(hexToRGB(bgColor));

  const foregroundColor = xyz > 127 ? '#000000' : '#ffffff';

  return (
    <div
      className={'box'}
      style={{
        backgroundColor: bgColor,
        color: foregroundColor,
      }}
    >
      {(label ?? true) && 'foreground'}
    </div>
  );
};

이제 녹색 영역에서의 전경/배경의 대비를 보장할 수 있게 되었습니다.


위의 방법으로 대부분의 경우를 커버할 수 있지만, CIE XYZ 모델은 색간 거리가 균등하지 않습니다. 최근에는 이 점을 보완한 OKLab 같은 좋은 모델들이 제시되었습니다.

  • 글 내용이 편집한 대로 적용이 되지 않고 계속 롤백이 되네요.. ㅠㅠ
profile
Biscuit Basket

3개의 댓글

comment-user-thumbnail
2024년 3월 4일

와!! 저도 색상에 관심이 많아서 이런건 어떻게 구하나 궁금했어요!! 혹시 요런 내용은 어디서 배울 수 있나요??

1개의 답글