HEX·RGB·HSL·OKLCH 기초 — 같은 색을 네 가지로 적는 이유

같은 색인데 왜 적는 법이 넷이나 되나

CSS를 쓰다 보면 같은 색이 여러 얼굴로 나타납니다. 디자인 시안에는 #3b82f6, 개발자도구에는 rgb(59, 130, 246), 어떤 코드베이스에는 hsl(217 91% 60%), 최근 글에서는 oklch(62.3% 0.188 260).

넷 다 같은 파란색입니다. 그런데 왜 이렇게 여러 방식이 남아 있을까요.

답부터 말하면 각각이 색을 다른 축으로 쪼개기 때문입니다. 어떤 축으로 쪼개느냐에 따라 잘하는 일이 달라집니다. 색을 주고받기만 할 거라면 HEX로 충분하지만, “이 색보다 조금만 밝은 색”을 만들려면 HEX로는 손도 못 댑니다.

HEX — 주고받기 위한 표기

#3b82f6은 빨강 3b, 초록 82, 파랑 f6을 16진수로 이어 붙인 것입니다. 10진수로는 각각 59, 130, 246입니다.

#3b82f6
 └┬┘└┬┘└┬┘
  R  G  B

세 자리 축약형(#abc)도 있는데, 각 자리를 두 번씩 반복한 것과 같습니다. #abc는 #aabbcc입니다. 절반씩 건너뛰는 게 아니라 그대로 복제입니다.

HEX의 장점은 짧고 어디서나 통한다는 것입니다. 디자인 도구, 브라우저, 문서 어디에 붙여넣어도 같은 색이 나옵니다. 단점도 분명합니다. 숫자만 보고 어떤 색인지 짐작할 수 없고, 값을 조금 바꿔서 원하는 색을 만들기도 어렵습니다. #3b82f6을 조금 밝게 하려면 세 값을 각각 얼마나 올려야 할까요? 감이 오지 않습니다.

RGB — 빛을 섞는 방식

rgb(59 130 246)은 HEX와 완전히 같은 것을 10진수로 적었을 뿐입니다. 표현하는 색도, 색을 쪼개는 축도 같습니다.

그런데도 RGB가 따로 쓰이는 이유는 두 가지입니다.

투명도를 섞을 수 있습니다. rgb(0 0 0 / 0.5)처럼 슬래시 뒤에 알파 값을 붙입니다. 그림자나 오버레이처럼 뒤가 비쳐야 하는 곳에서 이 형태가 필요합니다.

코드에서 계산하기 쉽습니다. 자바스크립트로 색을 다룰 때 16진수 문자열을 쪼개는 것보다 숫자 셋을 그대로 다루는 편이 간단합니다.

이 표기법이 축으로 삼는 것은 빛의 세기입니다. 빨강·초록·파랑 빛을 각각 얼마나 켤지 정하는 방식이고, 화면이 실제로 픽셀을 켜는 방식과 같습니다. 그래서 기계에는 자연스럽지만 사람에게는 불편합니다. “조금 더 따뜻하게”를 RGB 값으로 옮기려면 세 숫자를 동시에 조정해야 하니까요.

HSL — 사람이 만지기 쉬운 축

HSL은 같은 색을 다른 세 축으로 쪼갭니다.

  • H (Hue, 색상) — 0~360도. 색상환에서의 각도입니다. 0은 빨강, 120은 초록, 240은 파랑
  • S (Saturation, 채도) — 0~100%. 0이면 회색, 100이면 가장 선명한 색
  • L (Lightness, 명도) — 0~100%. 0이면 검정, 100이면 흰색

이 축의 장점은 의도를 그대로 옮길 수 있다는 것입니다. “같은 색인데 더 연한 것”은 L만 올리면 되고, “채도를 뺀 회색빛”은 S만 내리면 됩니다. 버튼의 기본·호버·비활성 색을 만들 때 특히 편합니다.

:root {
  --btn:         hsl(217 91% 60%);
  --btn-hover:   hsl(217 91% 52%);  /* L만 내림 */
  --btn-disabled: hsl(217 20% 60%); /* S만 내림 */
}

RGB로 같은 일을 하려면 세 값을 다 계산해야 하는데, HSL은 한 값만 만지면 됩니다. 그래서 오랫동안 팔레트를 만드는 표준 방식이었습니다.

HSL의 함정: L은 눈에 보이는 밝기가 아니다

여기서 실무자들이 걸립니다. 여러 색을 같은 L 값으로 맞춰 팔레트를 만들면, 화면에서는 밝기가 제각각으로 보입니다.

직접 확인해 보세요. 다음 두 색은 HSL에서 L이 똑같이 50%입니다.

hsl(60 100% 50%)   /* 노랑 — #ffff00 */
hsl(240 100% 50%)  /* 파랑 — #0000ff */

노랑은 눈이 부시게 밝고, 파랑은 거의 검정에 가깝게 어둡습니다. 같은 숫자인데 보이는 밝기는 하늘과 땅 차이입니다.

이유는 L이 사람의 지각이 아니라 계산상의 중간값이기 때문입니다. HSL의 L은 RGB 세 값 중 가장 큰 값과 가장 작은 값의 평균입니다.

노랑 #ffff00 → R=255, G=255, B=0 → (255 + 0) / 2 = 127.5 → L = 50%
파랑 #0000ff → R=0,   G=0,   B=255 → (255 + 0) / 2 = 127.5 → L = 50%

계산은 정확히 같습니다. 하지만 사람의 눈은 초록빛에 훨씬 민감하고 파란빛에는 둔합니다. 노랑은 빨강과 초록이 모두 켜져 있어 밝게 느껴지고, 파랑은 눈이 가장 둔한 빛만 켜져 있어 어둡게 느껴집니다. HSL의 계산에는 이 감도 차이가 전혀 반영되어 있지 않습니다.

그래서 HSL로 만든 팔레트는 색마다 무게가 달라 보입니다. 파란 계열 버튼과 노란 계열 버튼을 같은 L로 맞춰 놓으면, 노란 버튼만 붕 뜹니다.

OKLCH — 지각을 축으로 삼은 표기

OKLCH는 이 문제를 고치려고 만들어진 표기법입니다. 세 축의 이름은 비슷하지만 계산 방식이 다릅니다.

  • L (Lightness) — 0~100%. 사람이 느끼는 밝기
  • C (Chroma, 채도) — 0부터. 상한이 색상마다 다릅니다
  • H (Hue, 색상) — 0~360도

앞의 두 색을 OKLCH로 옮기면 차이가 숫자에 그대로 담깁니다.

색 HSL OKLCH
노랑 #ffff00 hsl(60 100% 50%) oklch(96.8% 0.211 109.8)
파랑 #0000ff hsl(240 100% 50%) oklch(45.2% 0.313 264.1)

HSL에서는 둘 다 50%였지만, OKLCH에서는 노랑이 96.8%, 파랑이 45.2%입니다. 보이는 대로 적힙니다.

그래서 OKLCH로는 이런 일이 가능합니다. 여러 색을 같은 L로 맞추면 실제로 같은 무게로 보이고, 색상(H)만 바꿔가며 팔레트를 만들어도 밝기가 흔들리지 않습니다.

/* 세 색의 밝기가 실제로 같게 보인다 */
--tag-blue:  oklch(62% 0.15 260);
--tag-green: oklch(62% 0.15 145);
--tag-red:   oklch(62% 0.15 25);

같은 일을 HSL로 하면 초록 태그만 밝게 뜹니다.

채도의 상한이 색상마다 다르다

OKLCH를 쓸 때 알아야 할 점이 하나 있습니다. C 값에 정해진 최대치가 없습니다. 정확히는 화면이 표현할 수 있는 최대 채도가 밝기와 색상에 따라 달라집니다.

밝기가 아주 높거나 낮으면 표현할 수 있는 채도가 좁아집니다. L이 95%인데 C를 0.3으로 주면 화면에 없는 색이 되고, 브라우저는 가장 가까운 색으로 잘라서 보여줍니다. 그래서 어두운 단계를 만들 때는 C도 함께 낮추는 편이 의도한 색에 가깝습니다.

무엇을 언제 쓸까

넷 다 같은 색을 적는 방법이므로 성능 차이는 없습니다. 판단 기준은 그 자리에서 무엇을 하려는가입니다.

하려는 일 쓸 표기법
색을 그냥 지정하기, 다른 사람에게 전달하기 HEX
투명도를 섞기 rgb(… / …)
한 색에서 밝기·채도만 조금 바꾸기 HSL
여러 색의 밝기를 실제로 맞추기, 팔레트 만들기 OKLCH

프로젝트를 새로 시작한다면 디자인 토큰은 OKLCH로 정의하고, 개별 값은 HEX로 두는 방식이 무난합니다. 토큰에서는 단계별 밝기를 고르게 나누는 것이 중요하고, 개별 값은 짧고 알아보기 쉬운 편이 낫기 때문입니다.

브라우저 지원은 이제 걱정할 단계가 아닙니다. OKLCH는 주요 브라우저가 모두 지원합니다. 다만 아주 오래된 환경까지 챙겨야 한다면 @supports로 대체 색을 함께 두면 됩니다.

요약

  • 네 표기법은 같은 색을 다른 축으로 쪼갠 것이다. 표현할 수 있는 색이 다른 게 아니다
  • HEX와 RGB는 같은 축(빛의 세기)이다. 16진수냐 10진수냐의 차이
  • HSL은 사람이 만지기 쉬운 축이지만, L은 지각 밝기가 아니라 계산상의 중간값이다
  • 그래서 HSL로 여러 색의 밝기를 맞추면 화면에서는 제각각으로 보인다
  • OKLCH의 L은 지각에 맞춰 설계됐다. 색상을 바꿔도 밝기가 흔들리지 않는다
  • OKLCH의 채도 상한은 밝기·색상에 따라 달라진다. 넘으면 브라우저가 잘라낸다