색상 모델과 프론트엔드
RGB, HEX, HSL, LCH, OKLCH 색상 모델의 차이와 프론트엔드 성능 관점.
근래에 신규 프로젝트를 진행하며 해결했던 어이없는 이슈 하나가 있다.
Tailwind CSS를 사용하기 위해 설정해둔 색상 값이 적용되지 않았던 것이다. 덮어쓰고 있는 값이 있다거나 CSS 변수 매핑 등 그럴법한 지점들을 조사해봤지만 원인을 찾을 수 없었다.
문제의 원인은 한참을 찾아 발견한 CSS 변수 선언부에서 찾을 수 있었다. 바로 CSS 함수, hsl()의 사용에 있어 문법적으로 잘못 표기되어 있던 것이 문제였다. 나도 과거에는 디자이너였지만 RGB, HEX 형식의 색상 값을 대개 사용했고 hsl 방식은 잘 사용하지 않았었다. 그래서 이번 이슈는 ‘hsl 방식은 왜 RGB, HEX와 같은 표기법과 다른 형태로 존재하는 걸까?‘에 대한 의문으로 이어졌다.
RGB는 Red, Green Blue의 약자를 사용한 것으로, 이 세 가지의 색을 조합해 표현하는 가산 혼합 색상 모델이다. 일반적으로 각 색상의 밝기를 8비트 정수로, 0~255 사이의 10진법 형식의 값으로 지정하며 (R, G, B) 형식으로 표현하곤 한다. 인간의 눈에는 세 가지 종류의 원추 세포가 존재하는데, 이는 각각 빨강, 초록, 파랑에 민감하다. RGB 모델은 이런 생물학적 성질을 반영해서 인간의 색 지각 방식에 설계된 것이라고 볼 수 있다.
HEX는 Hexadecimal, RGB와 유사하나, 각 색상의 10진수로 표현되는 0~255 값을 16진수로 변환하여 표현한 형식이다. 표현은 다르나 결국 RGB색상 모델을 따르는 것은 같다. 그렇다면 같은 색상 모델을 사용하는 것처럼 보이는데, 어떤 차이가 있을까?
표현에 있어 10진수와 16진수를 사용한다는 것이 큰 차이일 것이다. 이에 따라 표현에 필요한 글자 수가 달라지고, 16진수가 10진수보다 컴퓨터 친화적이라는 차이를 찾을 수 있다. 따라서 HEX 형식이 RGB 형식보다는 더 컴퓨터 친화적이다. 그러나 결국 요즘의 하드웨어 수준을 생각하면 이는 의미있는 차이라고 보긴 어려울 것이다.
HSL은 Hue, Saturation, Lightness의 약자로, 색상, 채도, 명도의 세 요소로 색을 표현하는 모델이다. 기존 RGB 모델에서는 색조만 약간 바꾼다던가, 조금 더 밝거나 흐리게 하고 싶다던가, 하는 것들이 어려웠다. 따라서 이런 니즈에 따라 색상, 채도, 명도라는 개념을 조정함으로써 조금 더 쉽고 인간 친화적으로 조정이 가능한 색상 모델이 등장한 것이다. CSS에서 사용하는 hsl() 함수는 hsl([색상의 각도] [채도] [밝기]) 형식으로 이루어지는데, 여기서 색상의 각도는 색상환 평면에서의 각도에 따른 색상을 의미하게 된다.
성능적인 차이는 어떨까? 앞서 RGB나 HEX의 경우에는 그 차이가 미미했다. 실제로 취업 준비하던 시절에 만들었던 포트폴리오에서는 비트 시프트를 이용해 RGB-HEX 형식 간 전환 기능을 구현했었다. 하지만 HSL 모델은 조금 다르다. 다음의 HSL 형식에서 RGB 형식으로 변환하는 의사 코드를 보자.
function 변환_HSL_에서_RGB(각도, 채도, 명도):
// 1: 0–1 범위 정규화
채도비율 = 채도 / 100
명도비율 = 명도 / 100
// 2: 색상 강도 차이값 계산
색상차이 = (1 - |2 × 명도비율 - 1|) × 채도비율
// 3: 색상 구간 계산 및 보조값
구간위치 = 각도 / 60
보조값 = 색상차이 × (1 - |(구간위치 mod 2) - 1|)
// 4: 분기: 6개 if/else
if 0 ≤ 구간위치 < 1: (r, g, b) = (색상차이, 보조값, 0)
else if 1 ≤ 구간위치 < 2: (r, g, b) = (보조값, 색상차이, 0)
…
// 5: 밝기 보정 및 스케일링
보정값 = 명도비율 - (색상차이 / 2)
r = round((r + 보정값) * 255)
g = round((g + 보정값) * 255)
b = round((b + 보정값) * 255)
return [r, g, b]RGB를 HEX로 변환해야 한다면 단순 진수 변환 수준의 연산 과정을 거치지만, HSL은 다르다. RGB로 변환하던, HEX로 변환하던, 산술적인 연산과 조건 분기문이 훨씬 많다. 추산되는 시공간 복잡도는 둘 다 유사하지만 연산 횟수가 훨씬 많기 때문에, HSL 방식은 애니메이션, 3D 등의 복잡하고 무거운 대량 연산 환경에서는 유의미한 차이를 만들 수 있다.
한편 비교적 최근에 떠오른 LCH, OKLCH 형식도 있다. ‘색상, 채도, 명도’의 동일한 세 개의 축을 사용한다는 것은 HSL과 유사하다. 하지만 HSL은 RGB 채널 간의 차이 비율로 채도(Saturation)를 계산한다. 따라서 밝기에 따라 채도 값이 왜곡될 수 있다. 어두운 색에서 조금의 색 차이만 있어도 강한 채도로 보이거나, 밝은 색에서는 채도가 전혀 없는 것처럼 나올 수도 있다는 것이다.
반면, LCH는 인간의 시각이 색 차이를 지각하는 방식에 기반해 채도(Chroma) 변화를 더 자연스럽고 균일하게 나타낼 수 있다. 그 이유는 인간 눈의 색 인식 축(Opponent Axis)을 모델로 삼고, 수학적으로 거리를 이용해 일정한 단계로 계산하는데, 이것이 채도 변화가 일정한 눈의 느낌으로 이어지기 때문이다.
LCH에서 보다 개선된 OKLCH 방식도 있다. 기존 LCH 방식은 채도가 극에 가까운 값을 가질 경우, 실제 지각하는 색과 어긋날 수 있다는 점이 여전히 존재했다. 이에 따라 ‘색 인식 축을 놓는 기준 공간’을 개선하여 2020년대에 새로이 제안되었고, 밝은 파스텔, 강한 네온 색상 등에서도 색 변화가 더욱 일정하게 느껴지도록 개선된 방식이라고 볼 수 있다. LCH 방식의 연산 비용에 관해서는, 행렬 곱, 감마 보정, 삼각함수 등이 필요하므로 HSL 보다도 더욱 비싼 연산을 필요로 하고, OKLCH 방식의 경우에는 LCH 방식보다 조금 더 비싸다.
웹 프론트엔드적인 측면에서는 사용하는 색상 모델에 따른 유의미한 성능 차이가 있을까? 브라우저는 CSS 파일을 받아서 처리할 때, Chromium의 Blink, Firefox의 Gecko, Safari의 Webkit 등 렌더링 엔진이 내부적으로 모든 색 표기법을 하나의 통일된 포맷으로 변환해둔다. 브라우저 렌더링 엔진의 프로세스 중에서 CSS 파일을 해석하여 CSSOM을 생성하는 과정이 있다. 이것이 렌더 트리로 이어지고, 실제 화면에 픽셀로 그려지는 과정까지 이어지게 된다.
이 CSSOM을 생성할 때 CSS 파일의 CSS 텍스트를 토큰화하고, 앞서 말한대로 색상 값을 정규화한다. 여기서 CSS 파일을 작성할 때 사용한 색상 모델의 변환 과정이 이루어진다. 리페인트나 리플로우 과정에서는 CSSOM을 재구성하는 경우는 없지만, 웹 페이지를 이동하는 경우는 어떨까?
아마 새로운 문서에 따른 해당 문서의 CSS 파일을 로드하게 될 거고, 또 새로운 CSSOM을 생성할 것이다. 그럼 이것이 누적될 때 색상 모델 별 차이가 유의미해지는 것은 아닌가라는 의문을 가질 수 있다. 하지만 색 정규화에 대한 비용은 전체 CSS 파싱 과정에서 극히 작은 부분이며, 특히 SPA 방식의 애플리케이션의 경우에는 페이지 전환 시 CSS 파일이 그대로 유지되고 이에 따라 CSSOM도 그대로 살아 있기 때문에 색 정규화가 다시 발생하지 않는다고 볼 수 있다. 그러므로 사용자에게 체감되는 성능 차이가 의식할 정도까지는 도달하지 않는다고 볼 수 있다. 또한, 어차피 브라우저 단에서의 쟁점이므로 CSR이나 SSR과 같은 렌더링 방식에 따른 차이는 의미가 없다고 볼 수 있겠다.
결론적으로, 프론트엔드 성능 최적화 측면에서는 색상 모델 자체보다는 애플리케이션의 구조, 렌더링 전략, 그리고 네트워크 효율성이 훨씬 더 중요한 요소라고 할 수 있겠다. 따라서 색상 모델의 선택은 성능보다는 목적과 상황에 봐서 결정하는 것이 좋을 것이다. 간단한 UI 구성이나 기본적인 테마 작업에서는 HEX나 RGB, HSL로도 충분하며, 더 정밀한 색 조정이나 지각적 균일성을 중시하는 디자인 시스템 구축에는 LCH나 OKLCH가 유용할 수도 있겠다. 즉, 각 색상 모델의 장점과 특징을 명확히 이해하고, 이를 상황에 따라 트레이드 오프의 적정 지점을 찾아 활용하는 것이 중요하다고 생각한다.
Reference
원문: Medium