01 — Color Spaces (sRGB·P3·OKLCH)
한 줄 답: 색은 삼차원 좌표이고, 어떤 축으로 좌표를 잡느냐가 색공간이다. sRGB·HSL은 1996년 CRT 시대의 좌표계로 perceptually non-uniform — 같은 L이 hue마다 밝기가 다르다. OKLCH는 2020년 Björn Ottosson이 사람 시각계에 맞게 다시 설계한 좌표계로, 같은 L이면 진짜 같은 밝기다. CSS Color 4는 이 새 좌표계 위에
color-mix()·relative color·P3 광역색을 얹어 2024년부터의 모던 색 시스템을 완성했다.
Why — 왜 sRGB·HSL을 떠나는가
sRGB가 좁다
sRGB는 1996년 평균적 CRT 모니터의 색역을 표준화한 것이다. 2017년 iPhone X부터 모든 애플 기기는 P3 색공간(약 35% 더 넓음)을 지원한다. sRGB만 쓰면 디스플레이가 가진 색의 1/3을 못 쓰는 셈.
HSL이 거짓말한다
hsl(0 100% 50%) (빨강)과 hsl(120 100% 50%) (초록)을 나란히 놓으면 — 초록이 훨씬 밝다. L=50%인데도. HSL의 L은 RGB의 평균이지 사람이 인지하는 밝기가 아니기 때문이다.
/* HSL의 함정 */
.danger { color: hsl(0 100% 50%); } /* 빨강 — 보통 밝기 */
.success { color: hsl(120 100% 50%); } /* 초록 — 눈에 띄게 밝음 */
.info { color: hsl(240 100% 50%); } /* 파랑 — 너무 어두움 */
/* 셋 다 "L=50%"이지만 인지 밝기는 다 다르다 */다크모드를 만들 때 hsl(... 50%)을 일괄 30%로 떨어뜨리면 어떤 색은 적당하고 어떤 색은 안 보인다. 이게 HSL 다크모드의 함정이다.
OKLCH가 풀어준다
.danger { color: oklch(60% 0.25 25); } /* 빨강 */
.success { color: oklch(60% 0.20 140); } /* 초록 */
.info { color: oklch(60% 0.20 250); } /* 파랑 */
/* 셋 다 L=60% — 사람 눈에 진짜로 같은 밝기 */L=60%이면 hue가 뭐든 지각 밝기가 같다. 다크모드는 모든 색의 L을 일괄 떨어뜨리는 1줄 변환으로 작동한다.
How — OKLCH가 어떻게 다른가
색공간 분류
| 색공간 | 좌표 | 강점 | 약점 |
|---|---|---|---|
rgb() | R/G/B 0~255 | 직관, 호환 | perceptually non-uniform, sRGB 한정 |
hsl() | H/S/L | 직관적 색상환 | L이 hue마다 다른 밝기 (함정) |
hwb() | H/W/B | 화이트·블랙 혼합 비율 | 여전히 sRGB |
lab()·lch() | L/a/b 또는 L/C/H (CIE) | 디바이스 독립 | LCH의 hue 보간이 약간 어색 |
oklab()·oklch() | L/a/b 또는 L/C/H (Ottosson) | perceptually uniform, hue 보간 깨끗 | 새로움 (2020) |
color(display-p3 ...) | R/G/B (P3) | 광역 색역 | 디스플레이 필요 |
color(rec2020 ...) | R/G/B (Rec2020) | HDR | HDR 디스플레이 + 브라우저 지원 진행 중 |
OKLCH의 세 숫자
oklch(70% 0.15 200)
/* L C H */- L (Lightness): 0% ~ 100% 또는 0 ~ 1. 사람 눈의 밝기. 0=완전 검정, 100%=완전 흰색.
- C (Chroma): 0 ~ ~0.4. 채도. 0=회색, 클수록 선명. 맥시멈은 hue마다 다름 — 빨강은 0.37까지, 노랑은 0.21까지 가능.
- H (Hue): 0 ~ 360 (deg, turn 등). 색상환 각도. 0=빨강, 120=초록, 240=파랑.
채도 C가 너무 크면 sRGB 색역을 넘어가 gamut clipping된다. P3 디스플레이에선 더 넓게 살아남는다.
변환 비교
| 값 | sRGB rgb() | HSL | OKLCH |
|---|---|---|---|
| Tailwind blue-500 | rgb(59 130 246) | hsl(217 91% 60%) | oklch(62% 0.21 254) |
| Tailwind red-500 | rgb(239 68 68) | hsl(0 84% 60%) | oklch(63% 0.24 25) |
| Tailwind green-500 | rgb(34 197 94) | hsl(142 71% 45%) | oklch(72% 0.18 145) |
세 색의 HSL L은 60·60·45 — 직관에 어긋남. 초록이 가장 어두워 보이는데 L이 가장 낮음. OKLCH L은 62·63·72 — 실제 인지 밝기 순서와 일치.
What — 핵심 기능 4가지
1) color-mix() — 색을 코드로 합성
/* in <colorspace>, color1 [pct], color2 [pct] */
color-mix(in oklch, #4a90e2 30%, white);
color-mix(in oklch, var(--brand) 50%, var(--surface));
/* hue 보간 방향 제어 */
color-mix(in oklch longer hue, red, blue); /* 색상환 먼 길 */
color-mix(in oklch shorter hue, red, blue); /* 짧은 길 (기본) */디자인 토큰 패턴 — 1개 brand에서 4단계 자동 파생:
:root {
--brand: oklch(62% 0.21 254);
--brand-50: color-mix(in oklch, var(--brand) 10%, white);
--brand-100: color-mix(in oklch, var(--brand) 20%, white);
--brand-500: var(--brand);
--brand-700: color-mix(in oklch, var(--brand) 70%, black);
--brand-900: color-mix(in oklch, var(--brand) 40%, black);
}in srgb로 섞으면 중간이 진흙색이 된다. in oklch가 디자인 토큰의 기본값.
2) Relative Color Syntax — 기존 색에서 파생
/* rgb(from <color> <r> <g> <b> [/ <alpha>]) */
.btn {
--c: oklch(62% 0.21 254);
background: var(--c);
border: oklch(from var(--c) calc(l - 0.1) c h); /* 10% 어둡게 */
box-shadow: 0 4px 8px oklch(from var(--c) l c h / 0.3);
}
/* sRGB도 가능 */
.fade {
background: rgb(from var(--brand) r g b / 0.5);
}color-mix가 섞기라면 relative color는 분해 후 재조립. 채도만, hue만 바꾸기 좋다.
3) P3 광역색 — color(display-p3 ...)
.vivid {
/* sRGB에 없는 빨강 */
background: color(display-p3 1 0 0);
}
/* fallback 패턴 — 사실상 표준 */
.brand {
background: oklch(62% 0.21 254); /* sRGB-safe */
}
@supports (color: color(display-p3 1 1 1)) {
.brand { background: color(display-p3 0.3 0.5 1); } /* P3 */
}
/* media query로 디스플레이 감지 */
@media (color-gamut: p3) {
.brand { background: color(display-p3 0.3 0.5 1); }
}
@media (color-gamut: rec2020) {
.brand { background: color(rec2020 0.3 0.5 1); }
}color-gamut은 디스플레이가 지원하는지, @supports (color: ...)은 브라우저가 파싱할 수 있는지를 본다. 둘 다 같이 쓰는 게 안전하다.
4) HDR — color(rec2020 ...)와 dynamic-range-limit
CSS Color 6 (2024 ED)에서 등장. Apple Vision Pro, 최신 iPhone, HDR TV가 타겟.
.hdr-highlight {
/* Rec2020 색공간 + HDR 강조 */
background: color(rec2020 1.5 0.8 0.3); /* 1.0을 넘는 값 → HDR */
}
/* HDR 끄기 */
.tone-mapped {
dynamic-range-limit: standard;
}2026년 현재 실험적 — Safari TP에서 일부 동작. 디자인 시스템에 도입하기엔 아직 이르다.
What-if — 잘못 다루면
1) HSL 다크모드 = 위계 파괴
/* 안 됨 */
:root {
--primary: hsl(220 80% 50%);
--success: hsl(140 70% 50%);
}
.dark {
--primary: hsl(220 80% 30%);
--success: hsl(140 70% 30%); /* 너무 어두워서 안 보임 */
}OKLCH로:
:root {
--primary: oklch(62% 0.20 254);
--success: oklch(62% 0.18 140);
}
.dark {
--primary: oklch(50% 0.20 254);
--success: oklch(50% 0.18 140); /* 같은 위계 유지 */
}2) sRGB에서 보간하면 진흙색
/* 진흙색 중간 */
background: linear-gradient(red, blue); /* sRGB 기본 보간 */
/* 깨끗한 보라 중간 */
background: linear-gradient(in oklch, red, blue);자세한 이유는 02-gradients 참고.
3) Gamut clipping — 보이지 않는 색
/* oklch C가 너무 큼 — sRGB 디스플레이에선 잘림 */
color: oklch(70% 0.5 25); /* 실제 표시: 0.37 근처로 잘림 */브라우저는 gamut mapping 알고리즘으로 가장 가까운 표시 가능 색으로 옮긴다. 디자인 시스템에서 항상 P3로 보이게 하려면 C를 0.15 미만으로 잡는 게 안전.
4) currentColor와 relative color 혼동
/* 자식이 부모 텍스트 색의 50% 투명 — 옳음 */
.icon {
background: rgb(from currentColor r g b / 0.5);
}currentColor는 연산 시점에 부모의 color로 평가된다. relative color와 조합하면 상속받은 색의 변형을 자식이 자동으로 가질 수 있다 — 디자인 토큰의 비밀 무기.
Insight — Björn Ottosson과 OKLab의 탄생
“색공간 30년의 빈자리”
CIE Lab(1976)이 디바이스 독립적 perceptually uniform을 표방하며 나왔지만, 실제 사용해보면 hue 보간이 어색하다 — 파랑→빨강을 섞으면 보라가 아니라 자주색이 된다. 그래서 디지털 디자인은 30년간 RGB·HSL을 떠나지 못했다.
2020년, 스웨덴의 엔지니어 Björn Ottosson이 블로그에 “A perceptual color space for image processing” 을 올렸다. 그는 Lab의 변환 행렬을 현대 디스플레이의 색역에 맞게 다시 튜닝했고, hue 보간을 깨끗하게 만들었다. 이게 OKLab이다.
2022년 CSS Color 4가 oklch()·oklab()을 표준화했고, 2023년 Tailwind v4가 모든 컬러 팔레트를 OKLCH로 재정의했다. 2024년에는 Open Props·Radix Colors도 OKLCH로 전환했다. “30년 묵은 HSL의 함정” 이 5년 만에 해결된 셈.
흥미로운 반전은, Ottosson의 알고리즘은 너무 단순해서 학계가 받아들이는 데 시간이 걸렸다는 점이다. 복잡한 색채과학 논문보다 블로그 글 한 편이 산업의 표준을 바꿨다.
요약 + Mermaid
- 색은 공간 + 모델 + 합성의 3축이다.
- OKLCH가 perceptually uniform — 같은 L이면 진짜 같은 밝기.
color-mix(in oklch, ...)는 1색에서 N단계 토큰을 코드로 파생.- Relative color (
rgb(from var(--c) r g b / 0.5))는 분해 후 재조립. - P3는
@media (color-gamut: p3)+@supports (color: color(display-p3 ...))조합으로. - 디자인 시스템의 기본값은 OKLCH가 사실상 표준이 됐다 (Tailwind v4, Open Props, Radix).
다음: 02-gradients — gradient의 interpolation method도 결국 같은 색공간 문제다.