01 — Color Space for Design Systems
이 문서가 답하는 질문: 디자인 시스템이 색공간을 고른다는 건 무엇을 결정하는 것인가? 왜 Tailwind v4·Radix 3·Open Props가 모두 OKLCH로 갈아탔는가? 한 줄 답 (Pyramid Top): 색공간 선택은 “같은 L 숫자가 정말 같은 밝기를 의미하는가” 라는 1개 질문을 푸는 것이다. OKLCH는 그렇고, HSL·sRGB는 아니다. 디자인 시스템은 연산으로 색을 파생해야 하기 때문에, 수학이 사람 눈과 일치하지 않는 좌표계 위에서는 시스템이 무너진다.
Why — 왜 색공간이 디자인 시스템의 문제인가
디자인 시스템은 “색을 만든다”
이게 핵심이다. 디자인 시스템은 색을 사용하지 않는다. 파생한다.
| 단순 사용 (CSS) | 시스템 (Design System) |
|---|---|
background: #3370b8; | background: var(--color-primary-9); |
hover 시 #2a5d96로 바꿈 | hover 시 공식에 따라 자동 파생 |
| 다크모드는 색을 교체 | 다크모드는 같은 공식의 다른 L 슬라이스 |
| step 50/100/…/900을 디자이너가 하나하나 골라줌 | step 1~12를 수식으로 계산 |
이 파생 작업이 색공간 선택을 결정짓는다. 사람 눈의 인지와 좌표계의 수학이 일치해야, 같은 수식이 모든 hue에서 같은 시각 결과를 만든다.
sRGB·HSL이 시스템에서 깨지는 4가지 경우
| 경우 | sRGB/HSL 결과 | OKLCH 결과 |
|---|---|---|
L=50% 빨강/초록/파랑 | 초록이 가장 밝아 보임 | 셋 다 같은 밝기 |
hover = L - 5% 일괄 적용 | 어떤 색은 너무 어둡고, 어떤 색은 변화 못 느낌 | 모든 색에서 같은 시각적 깊이 |
linear-gradient(red, blue) | 중간에 진흙색 | 중간에 깨끗한 보라 |
| dark mode swap | 명도 위계 깨짐 | 같은 L 차이가 유지됨 |
How — 4개의 색공간 비교
sRGB (1996)
CRT 모니터의 색역. R/G/B 각 채널이 0~255. 디바이스 의존 — 같은 #3370b8이 모니터마다 약간 다르게 보인다. 30년간 웹의 기본.
color: rgb(51 112 184); /* sRGB */
color: #3370b8; /* sRGB hex */HSL (CSS 1)
sRGB를 사람이 더 이해하기 쉽게 Hue/Saturation/Lightness로 재좌표화. 단, L은 RGB 평균이지 인지 밝기가 아니다.
color: hsl(214 56% 46%); /* 같은 색의 HSL 표현 */LCH (CIE 1976 기반, CSS Color 4)
CIE Lab → LCH. 디바이스 독립 + perceptually uniform. 단, hue 보간이 어색하다 (파랑↔빨강이 자주색을 지난다).
color: lch(48% 38 280); /* CIE LCH */OKLCH (2020, Björn Ottosson)
LCH의 변환 행렬을 현대 디스플레이에 맞게 재튜닝. hue 보간이 깨끗하고, sRGB·P3 양쪽에서 일관된 결과.
color: oklch(54% 0.11 254); /* OKLCH */4-축 비교표
| 색공간 | 좌표 | 디바이스 독립 | perceptually uniform | hue 보간 깨끗 | P3 지원 | gamut 검사 |
|---|---|---|---|---|---|---|
| sRGB | R/G/B | ❌ | ❌ | ❌ | ❌ | 항상 sRGB 내 |
| HSL | H/S/L | ❌ | ❌ | ❌ | ❌ | 항상 sRGB 내 |
| Lab/LCH | L/a/b·L/C/H | ✅ | ✅ | △ | ✅ | 가능 |
| OKLCH | L/C/H | ✅ | ✅ | ✅ | ✅ | 1급 |
| display-p3 | R/G/B | ✅ (P3 색역) | ❌ | ❌ | ✅ | 항상 P3 내 |
What — 디자인 시스템에서 실제 사용
OKLCH 값 읽는 법
oklch(0.62 0.21 254)
/* L C H */- L (Lightness): 0~1 또는 0%~100%. 사람 눈의 밝기. 절대값.
- C (Chroma): 0~0.4 정도. 채도. 맥시멈은 hue마다 다름 — 파랑은 0.31, 노랑은 0.21까지.
- H (Hue): 0~360deg. 색상환 각도. 0=빨강, 90=노랑, 180=청록, 270=보라.
Tailwind v4의 OKLCH 팔레트 (실제 값)
Tailwind v4가 사실상 표준이라 그대로 인용:
/* blue */
--color-blue-50: oklch(0.97 0.014 254.604);
--color-blue-100: oklch(0.932 0.032 255.585);
--color-blue-200: oklch(0.882 0.059 254.128);
--color-blue-300: oklch(0.809 0.105 251.813);
--color-blue-400: oklch(0.707 0.165 254.624);
--color-blue-500: oklch(0.623 0.214 259.815); /* base */
--color-blue-600: oklch(0.546 0.245 262.881);
--color-blue-700: oklch(0.488 0.243 264.376);
--color-blue-800: oklch(0.424 0.199 265.638);
--color-blue-900: oklch(0.379 0.146 265.522);
--color-blue-950: oklch(0.282 0.091 267.935);규칙성을 보면 — L이 monotonically 감소(0.97 → 0.28), C는 base에서 최대(0.214), H는 거의 유지(254~266). 이게 디자인 시스템 ramp의 시그니처다.
sRGB Fallback 페어 (실무 패턴)
브라우저가 oklch를 모를 때(매우 구형) 또는 코드 처리에 hex가 필요할 때:
:root {
/* sRGB fallback */
--color-primary-9: #3370b8;
/* 모던 브라우저는 이걸 덮어씀 */
--color-primary-9: oklch(0.546 0.245 262.881);
}@supports 분기보다 cascade override가 더 깔끔하다. 모르는 브라우저는 두 번째 줄을 무시하고 첫 줄을 쓴다.
Gamut Out-of-bounds 감지
OKLCH 값이 sRGB로 표시할 수 없으면 브라우저가 gamut mapping을 한다 (가장 가까운 표시 가능 색으로 옮김). 토큰 정의 시점에 이를 검증해야 한다.
// 빌드 타임 검증 (culori 라이브러리)
import { displayable, oklch } from 'culori';
const c = oklch({ l: 0.7, c: 0.5, h: 25 });
if (!displayable(c)) {
throw new Error(`OOG: ${JSON.stringify(c)}`);
}P3 디스플레이는 sRGB보다 ~35% 넓다. 진짜 출력 디바이스가 무엇이냐에 따라 안전한 C의 상한이 다르다.
| 디바이스 | 안전한 max C (대략) |
|---|---|
| 구형 IPS sRGB | 0.13 ~ 0.15 |
| 최신 IPS sRGB | 0.18 ~ 0.20 |
| P3 (모든 Apple 2017+) | 0.25 ~ 0.30 |
| Rec2020 (HDR) | 0.35+ |
실제 디자인 시스템에서의 채택
| 시스템 | 색공간 (2024+) | 비고 |
|---|---|---|
| Tailwind v4 | OKLCH (전 팔레트) | 2024년 초 갈아탐 |
| Radix Colors 3 | OKLCH + P3 페어 | 둘 다 제공 |
| Open Props | OKLCH (ramp) | 1.7+ |
| Material 3 | HCT (Google 자체) | HCT≈OKLCH의 친척 |
| Adobe Spectrum | sRGB → OKLCH 진행 중 | 2025 진행 |
| Apple HIG | display-p3 RGB | OKLCH 미사용 |
Material 3의 HCT는 Google이 OKLCH보다 자기 디바이스 더 잘 맞춤이라 주장하며 만든 사촌이다. 결국 같은 철학(perceptually uniform).
What-if — 잘못 다루면
1) oklch()를 hsl()처럼 쓰기
/* 안 됨 — L=50%면 다 같은 밝기여야 하는데 */
.danger { color: oklch(50% 0.25 25); }
.success { color: oklch(50% 0.25 140); } /* C=0.25는 초록에서 sRGB OOG */각 hue의 max C가 다르다는 사실을 무시하면 어떤 색은 잘려서 결국 맥시멈에서 같아 보이는 결과가 된다.
2) sRGB hex만 두고 다크모드 만들기
:root { --primary: #3370b8; }
.dark { --primary: #1a3a5c; } /* 디자이너 감으로 한 값 */라이트→다크에서 step 의미가 사라진다. OKLCH면 oklch(0.546 0.245 262) → oklch(0.62 0.214 259)처럼 L 차이를 보존할 수 있다. → 04장
3) P3 색을 sRGB로 렌더링하면서 눈치 못 챔
background: oklch(0.7 0.32 25); /* C=0.32, sRGB OOG */P3 모니터에선 선명한 빨강. sRGB 모니터에선 조용히 잘린 약한 빨강. 두 모니터에서 같은 디자인을 본 적 없는 디자이너는 이 차이를 모른다. 빌드 타임 gamut 검사가 답.
4) color-mix(in srgb, ...)로 토큰 파생
/* 안 됨 — 진흙색 중간 */
--brand-hover: color-mix(in srgb, var(--brand), black 10%);
/* 옳음 */
--brand-hover: color-mix(in oklch, var(--brand), black 10%);05장에서 자세히.
Insight — Ottosson의 블로그 글 한 편이 산업을 바꿨다
“30년 묵은 좌표계의 빈자리”
CIE Lab(1976)이 디바이스 독립적 perceptually uniform을 표방하며 등장했지만, hue 보간이 어색하다는 약점으로 디지털 디자인 영역에선 외면받았다. 디자이너는 PhotoshopHSB·HSL을 못 떠났다.
2020년 12월, 스웨덴 게임 엔지니어 Björn Ottosson이 자기 블로그에 “A perceptual color space for image processing” 을 올렸다. 핵심 아이디어는 단순했다 — Lab의 행렬을 현대 디스플레이의 측정값으로 재추정하면 깨끗해진다. 이게 OKLab이고, 극좌표 버전이 OKLCH.
2022년 CSS Color 4가 oklch()·oklab()을 표준화. 2023년 9월 Firefox 113이 마지막으로 지원 추가 → 모든 메이저 브라우저 안정화. 2024년 1분기에 Tailwind v4 alpha · Radix 3 · Open Props 1.7이 같은 분기에 OKLCH로 전환. 한 명의 블로그 글이 5년 만에 산업의 사실상 표준이 됐다.
흥미로운 반전은, Ottosson이 학계 출신이 아니라는 점이다. 그는 게임 개발자였고, 블로그가 논문보다 빠른 배포 경로임을 증명했다. 지금 그는 Adobe에서 일한다.
요약
- 디자인 시스템의 색은 파생된다 → 좌표계가 사람 눈과 일치해야 한다.
- OKLCH가 그 조건을 만족하는 사실상 표준 (2024 Tailwind v4 / Radix 3 / Open Props).
- 좌표는
L (0~1) / C (0~0.4) / H (0~360deg). L이 절대 밝기. - max C는 hue마다 다름 → 빌드 타임 gamut 검사로 P3/sRGB 안전성 검증.
- sRGB fallback은 cascade override 패턴으로 단순화.
다음: 02-color-scale-12-step — 그 좌표 위에 step 1~12의 의미를 박는 법.