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로 처리하는 것은 불가능하니, 컴포넌트 안에서 자바스크립트로 구현해 인라인 스타일로 구현할 수 있을 것 같다.
우선, 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로 바꾸는 로직을 짤 필요가 없어서 할 일이 하나 줄었다! 오예!
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)에 해당하는 숫자에 대한 배열을 만들어주었다.
전경색을 정한다.
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을 기준으로 동일하게 전경값을 구할 수 있다.
이제 어떠한 색상에서도 안전한 대비를 가지는 전경색을 보장할 수 있게 되었다!🎉