색상 시스템: RGB, HSL, OKLCH
2026년 디자인 시스템에서 OKLCH가 HSL을 대체하는 이유.
디자인 시스템들이 HSL에서 OKLCH로 이주하고 있으며, 이유는 유행이 아닙니다. HSL에는 측정 가능한 결함이 있습니다. 명도 축이 인간의 눈에 거짓말을 합니다. OKLCH는 실제 지각 실험에서 유도된 모델로 이를 고칩니다. 메커니즘을 이해하면 전환이 언제 이득인지, 날카로운 모서리가 어디인지 알 수 있습니다.
RGB와 Hex: 하드웨어 좌표
color: #ff6633;
color: rgb(255 102 51);
sRGB 삼중항은 발광체 강도를 기술하며, 채널당 8비트가 눈이 가장 민감한 곳에 쓰이도록 감마 인코딩되어 있습니다. 저장·교환 형식으로는 괜찮습니다. 디자인 조작 공간으로는 무용합니다. "같은 색상, 약간 더 어둡게"를 얻기 위해 조정할 수 있는 채널이 없습니다. RGB 채널을 조금씩 밀어 만든 파생 팔레트는 색상과 채도가 동시에 표류합니다.
HSL: 지각적 변환이 아닌 기하학적 변환
color: hsl(15 100% 60%);
HSL은 RGB 정육면체를 원통으로 재배열합니다. 색상은 각도, 채도는 반지름, 명도는 높이. 계산이 싸고 추론이 쉬워 20년간 CSS의 일꾼이었습니다. 결함: HSL 명도는 RGB 값에 대한 공식일 뿐, 지각된 밝기의 측정이 아닙니다. hsl(60 100% 50%)(노랑)과 hsl(240 100% 50%)(파랑)은 L=50%를 공유하지만, 어떤 인간 관찰자에게도 노랑이 극적으로 더 밝습니다. 결과는 팔레트에서 복리로 쌓입니다. HSL로 생성한 "균일한" 명도 램프는 눈에 띄게 고르지 않은 단계를 갖고, 고정 L에서의 색상 회전은 지각 밝기를 바꿉니다. HSL로 생성한 카테고리 색상이 차트에서 혼란스러워 보이는 이유입니다.
OKLCH: 지각 좌표
OKLCH는 Björn Ottosson이 2020년 발표하고 CSS Color Level 4에 채택된 OKLab의 원통형입니다. 현대 색채 지각 데이터에 수치적으로 적합되어, CIELAB의 파랑 색상 왜곡과 HSL의 명도 거짓말을 모두 교정합니다.
color: oklch(0.7 0.15 45);
- L — 지각 명도, 0에서 1. 같은 L의 두 색은 진짜로 똑같이 밝아 보입니다.
- C — 크로마, 0(회색)부터 위로. 실무에서 표시 가능한 최고 채도 색은 0.37 부근이 상한입니다. HSL 채도와 달리 색역에 상대적이지 않은 절대값입니다.
- H — 도 단위 색상각, 지각적으로 배치.
일상 작업을 바꾸는 속성: 축이 독립적입니다. L을 바꿔도 색상이 표류하지 않습니다. L과 C를 고정하고 H를 회전하면 결과 색 모두가 같은 시각적 무게를 갖습니다.
--brand-100: oklch(0.95 0.04 250);
--brand-300: oklch(0.80 0.10 250);
--brand-500: oklch(0.65 0.15 250);
--brand-700: oklch(0.45 0.12 250);
--brand-900: oklch(0.28 0.08 250);
색상 숫자 하나에서 나온 완전한 틴트-셰이드 스케일이고, 단계가 고르게 보입니다. HSL이 결코 제공하지 못한 것입니다. CSS Color 5의 상대 색상 문법은 파생을 선언적으로 만듭니다. oklch(from var(--brand-500) calc(l + 0.1) c h)는 "브랜드 색을 한 단계 밝게"입니다.
색역: 새로운 엣지 케이스
OKLCH는 화면이 표시할 수 있는지와 무관하게 색을 기술합니다. 높은 크로마 값은 sRGB를 초과하고 일부는 Display P3도 초과합니다. 브라우저는 가장 가까운 표시 가능 색으로 색역 매핑을 적용하지만, 그것에 의존한다는 것은 실제로 본 적 없는 색을 출시한다는 뜻입니다. 실용적 가드레일:
- 체계적 팔레트는 중간 크로마(텍스트 인접 색은 대략 0.15 이하)로 유지해 모든 단계가 sRGB에서 살아남게.
- 높은 크로마의 P3 색은 액센트용으로 남기고
@media (color-gamut: p3)뒤에 배치. - 수학을 믿지 말고 각 토큰을 변환기로 확인.
브라우저 지원은 2023년부터 에버그린 브라우저 전체에서 보편적입니다. 레거시 타깃은 OKLCH로 작성하고 PostCSS가 RGB 폴백을 내보내게 하세요. 일반 선언 뒤에 @supports (color: oklch(0 0 0)) 오버라이드를 두는 패턴은 툴링 없이도 작동합니다.
피해야 할 두 가지 실수
- 동일한 ΔL이 WCAG 준수 대비를 의미하지 않습니다. WCAG 2.x 대비율은 다른 휘도 공식을 쓰므로, OKLCH로 유도해 좋아 보이는 쌍도 검사기를 통과 못 할 수 있고 그 반대도 성립합니다(APCA가 고치려는 결함). 대비는 항상 별도로 측정하세요.
- 잘못된 공간에서의 보간. 그라디언트와
color-mix()는 일부 맥락에서 sRGB가 기본이라 보색 사이에 회색 데드존이 생깁니다.color-mix(in oklch, var(--a), var(--b))는 지각적으로 보간합니다.
기존 팔레트 마이그레이션
레거시 팔레트 변환은 기계적인 단계로 시작합니다. 각 hex 토큰을 OKLCH로 번역한 뒤 숫자를 보세요. 손으로 튜닝한 팔레트 대부분은 즉시 비일관성을 드러냅니다. 색상을 가로지르는 brand-500 토큰들이 같은 L 값을 공유하는 경우는 드물고, 왜 일부가 항상 "더 무거워" 보였는지 설명됩니다. 실용적 경로: 출시된 UI의 기존 hex 값은 그대로 두고, 단계별로 L과 C를 조화시킨 새 토큰을 OKLCH로 정의한 뒤, 화면 단위로 컴포넌트를 교체하세요. 다크 모드에서는 단순한 L 반전을 참으세요. 고크로마 색은 어두운 배경에서 번지고 진동하기 때문에, 다크 테마에는 뒤집힌 명도만이 아니라 낮춘 크로마도 필요합니다. 라이트 모드의 oklch(0.55 0.15 250)에는 공식만으로 계산된 값이 아니라 손으로 조정한 oklch(0.75 0.10 250) 같은 다크 변형이 짝지어집니다.
sdk.is/color-picker 의 컬러 피커에서 같은 색을 hex, RGB, HSL, OKLCH로 비교하세요. 크로마 값에 대한 직관을 쌓는 가장 빠른 길은 슬라이더를 끌며 숫자를 지켜보는 것입니다.