[CSS] 배경색과 전경색의 적절한 대비 설정하기 (feat. CIE XYZ 모델)

[CSS] 배경색과 전경색의 적절한 대비 설정하기 (feat. CIE XYZ 모델)

·

3 min read

YEONSUI 버전 2의 Button 컴포넌트를 살펴보며 한 가지 거슬리는 점을 발견했다.

Filled Primary Button 스타일은 다음과 같이 Primary 배경색과 흰색 텍스트로 이루어져 있다.

만약 primary 색상 변수가 밝은 색이라면 어떻게 보일까?

:root {
  --primary-color: 60, 100%;
}

안보인다... 😑

이러한 문제를 해결하기 위해 배경색에 따라 텍스트 색상을 바꾸는 기능을 추가할 것이다.

🟢 배경색에 따라 전경색 결정하기

첫 번째 시도: sass 안에서 해결하기 ❌

sass의 @function 기능을 사용해 hsl 값을 rgb로 바꾼 후 rgb의 값에 따라 전경색을 정하면 어떨까?

$button-color: (
  outlined: (
    primary: #{hslToRgb(var(--Primary-Color-6))},
  )
);

안타깝게도 @function은 css 변수를 매개변수로 받을 수 없다.

두 번째 시도: 컴포넌트 안에서 해결하기 ⭕️

sass로 처리하는 것은 불가능하니, 컴포넌트 안에서 자바스크립트로 구현해 인라인 스타일로 구현할 수 있을 것 같다.

  1. 우선, button 요소에 접근해 background-color 값을 가져온다.

     export const Button = (...) => {
       const buttonRef = useRef<HTMLButtonElement>(null)
    
       useEffect(() => {
         if (buttonRef && buttonRef.current && styleType === 'filled') {
           console.log(buttonRef.current.style)
         }
       }, [])
    
       return (
         <button ref={buttonRef}>
           ...
         </button>
       )
     }
    

    아무것도 안뜬다🤔 buttonRef.current.style은 인라인 스타일 속성만 보여준다. 우리는 className 속성으로 정의한 스타일을 알아야 하기 때문에 다른 방법을 사용해야 한다.

       useEffect(() => {
         if (buttonRef && buttonRef.current && styleType === 'filled') {
           const computedStyle = window.getComputedStyle(buttonRef.current)
           console.log(computedStyle)
         }
       }, [])
    

    getCompoutedStyle 함수를 사용해 DOM 요소의 모든 css 속성 값을 받을 수 있다.

       useEffect(() => {
         if (buttonRef && buttonRef.current && styleType === 'filled') {
           const computedStyle = window.getComputedStyle(buttonRef.current)
           console.log(computedStyle.backgroundColor)
         }
       }, [])
    

    hsl 값으로 정의했지만 rgb 값으로 변환하여 반환한다. hsl을 rgb로 바꾸는 로직을 짤 필요가 없어서 할 일이 하나 줄었다! 오예!

  1. rgb 문자열을 숫자 배열로 변환한다.

       useEffect(() => {
         if (buttonRef && buttonRef.current && styleType === 'filled') {
           const computedStyle = window.getComputedStyle(buttonRef.current)
           const { backgroundColor: rgbString } = computedStyle
           const matches = rgbString.match(/\d+/g)
           const rgb = matches?.map((match) => parseInt(match, 10))
           console.log(rgb)
         }
       }, [])
    

    rgb 값을 원활하게 사용하기 위해 r, g, b(, a)에 해당하는 숫자에 대한 배열을 만들어주었다.

  2. 전경색을 정한다.

       useEffect(() => {
         if (buttonRef && buttonRef.current && styleType === 'filled') {
           const computedStyle = window.getComputedStyle(buttonRef.current)
           const { backgroundColor: rgbString } = computedStyle
           const matches = rgbString.match(/\d+/g)
           const rgb = matches?.map((match) => parseInt(match, 10))
           if (rgb && rgb.length === 3) {
             const averageRGB = rgb.reduce((acc, val) => acc + val, 0) / rgb.length
             const textColor = averageRGB > 127 ? '#000' : '#fff'
             buttonRef.current.style.color = textColor
           }
         }
       }, [])
    

    rgb 값의 평균을 구하고, 그것이 rgb의 중앙값인 127보다 크다면 밝은 색상이니 #000, 그렇지 않다면 어두운 색상이니 #fff를 전경색으로 한다.

이제 Filled Primary Button에 밝은 primary 색상 변수를 넣어도 잘 보인다. 🎉

그런데 한 가지 문제를 또 발견하였다.

분명 너무 밝은 초록색인데 흰색이 전경색으로 선정되었다.

🔵 CIE XYZ 모델로 인간이 인지하는 색 표현하기

**CIE XYZ 모델이란?

**색을 인간의 시각에 가깝게 표현하기 위해 설계된 색 공간이다. RGB 색상보다 더 인간의 시각에 가까운 밝기와 색을 계산하는 데 사용한다.

RGB 값을 CIE XYZ 모델로 변환하는 공식은 다음과 같다:

이렇게 주어진 RGB 색상의 밝기를 계산하는 공식을 luma 공식이라고 한다.

이 중 사람이 인지하는 상대적 밝기를 나타내는 Y 값을 사용해보자.

useEffect(() => {
    if (buttonRef && buttonRef.current && styleType === 'filled') {
      const computedStyle = window.getComputedStyle(buttonRef.current)
      const { backgroundColor: rgbString } = computedStyle
      const matches = rgbString.match(/\d+/g)
      const rgb = matches?.map((match) => parseInt(match, 10))
      if (rgb && rgb.length === 3) {
        const luma = 0.2126729 * rgb[0] + 0.7151522 * rgb[1] + 0.072175 * rgb[2]
        const textColor = luma > 127 ? '#000' : '#fff'
        buttonRef.current.style.color = textColor
      }
    }
  }, [])

rgb 값의 평균이 아닌, rgb 값에 각자의 행렬 값을 곱하고 모두 합산한다. 이 값은 중간 값인 127을 기준으로 동일하게 전경값을 구할 수 있다.

이제 어떠한 색상에서도 안전한 대비를 가지는 전경색을 보장할 수 있게 되었다!🎉