색상 코드 변환기 (HEX · RGB · HSL)
HEX·RGB·HSL을 서로 변환하고 대비를 확인
HEX · RGB · HSL 중 아무거나 붙여넣으세요
#6366f1rgb(99, 102, 241)99 102 241Tailwind · CSS 변수hsl(239, 84%, 67%)대비 검사 (WCAG 2.2)
이 색을 배경으로 썼을 때 얹을 수 있는 글자색
권장: 검은색 텍스트. 큰 텍스트 기준은 18.66px 굵게 또는 24px 보통 이상입니다.
HEX는 사람이 읽으라고 만든 형식이 아니다
#6366f1을 보고 “아 인디고 계열이고 살짝 밝은 편이구나”라고 읽을 수 있는 사람은 없습니다. 이건 세 바이트를 16진수로 적어 놓은 것이고, 기계가 읽기 좋으라고 만든 형식입니다. 그래서 HEX만 들고 있으면 “이 색보다 10% 어두운 색”을 만드는 일이 불가능에 가깝습니다. 각 채널을 얼마나 줄여야 하는지 알 수가 없기 때문입니다.
hsl(239, 84%, 67%)은 다릅니다. 세 숫자가 각각 색상 · 채도 · 명도라는 사람의 어휘에 대응합니다. 어두운 변형을 만들려면 마지막 숫자만 내리면 됩니다:
--brand-300: hsl(239, 84%, 78%);
--brand-500: hsl(239, 84%, 67%); /* 기준색 */
--brand-700: hsl(239, 84%, 45%); /* 색상·채도는 그대로, 명도만 */HEX로 같은 일을 하려면 #8b8df5, #6366f1, #2f34d9 같은 값을 눈으로 찍어야 하고, 이게 정말 같은 색상인지 확인할 방법이 없습니다. 팔레트를 만드는 동안만이라도 HSL로 옮겨 놓고 작업하는 게 낫다는 말은 여기서 나옵니다.
그런데 HSL의 명도는 거짓말을 한다
여기서 대부분의 글이 끝나지만, 실제로 팔레트를 만들어 본 사람은 여기서부터 이상하다는 걸 느낍니다. 명도를 50%로 고정하고 색상만 돌려서 팔레트를 만들면 어떤 색은 눈이 부시고 어떤 색은 거의 검게 보입니다.
hsl(60, 100%, 50%) /* 노랑 — 상대 휘도 0.93 */
hsl(240, 100%, 50%) /* 파랑 — 상대 휘도 0.07 */둘 다 명도가 50%라고 적혀 있는데 실제 밝기는 열두 배 넘게 차이납니다. 노랑 위에는 검은 글씨를 얹어야 하고 파랑 위에는 흰 글씨를 얹어야 합니다. 같은 명도라면 있을 수 없는 일입니다.
이유는 단순합니다. HSL의 L은 지각량이 아니라 (max + min) / 2, 즉 RGB 채널 최댓값과 최솟값의 산술 중간값일 뿐입니다. 노랑 #ffff00은 max=255, min=0이라 L=50%이고, 파랑 #0000ff도 max=255, min=0이라 똑같이 L=50%입니다. 어느 채널이 켜져 있는지는 이 공식에 들어가지도 않습니다. 사람 눈이 초록에 압도적으로 민감하고 파랑에 둔감하다는 사실을 HSL은 아예 모릅니다.
그러니 HSL은 이렇게 쓰는 게 맞습니다: 같은 색상 안에서 명도를 조절할 때는 훌륭하고(239도의 L=67%와 L=45%는 실제로 후자가 어둡습니다), 다른 색상끼리 밝기를 비교할 때는 믿으면 안 됩니다.
OKLCH가 나온 이유
이 문제를 정면으로 푼 게 oklch()이고, 지금은 주요 브라우저에 다 들어가 있는 정식 CSS 함수입니다. OKLCH의 L은 지각 균등합니다. L을 0.7로 고정하고 색상(H)만 0부터 360까지 돌리면 정말로 밝기가 같은 색들이 나옵니다. HSL로는 원리상 불가능한 일입니다.
/* 이 셋은 눈으로 봤을 때 실제로 같은 밝기다 */
oklch(70% 0.15 30) /* 붉은 계열 */
oklch(70% 0.15 150) /* 초록 계열 */
oklch(70% 0.15 260) /* 파란 계열 */이 변환기는 OKLCH를 다루지 않습니다.HEX·RGB·HSL만 오갑니다. 다만 위 도구가 보여 주는 HSL 값을 “이 색의 진짜 밝기”로 읽지는 마시라는 뜻에서 적어 둡니다. 진짜 밝기를 알고 싶다면 HSL의 L이 아니라 아래 대비 섹션의 숫자를 보는 게 맞습니다. 그 숫자는 지각을 반영한 계산에서 나온 값입니다.
대비비는 밝기 평균이 아니다
위 도구가 계산하는 대비비는 이 페이지에서 유일하게 “변환”이 아닌 값입니다. 어떻게 나온 숫자인지 알아 둘 가치가 있습니다. 흔한 오해는 이렇습니다:
// ❌ 인터넷에 널려 있는 틀린 구현
const brightness = (r + g + b) / 3;여기엔 두 개의 독립적인 오류가 있습니다.
첫째, 0~255는 빛의 양이 아닙니다. sRGB 값은 감마 인코딩되어 있습니다. 128은 255의 절반 밝기가 아니라 실제로는 약 21.6%입니다. CRT 시절의 물리적 특성과 사람 눈의 비선형성에 맞춰 값을 압축해 둔 결과인데, 덕분에 8비트로도 어두운 영역의 계조를 촘촘히 표현할 수 있습니다. 밝기를 재려면 이 인코딩을 먼저 풀어야 합니다:
const linearize = (v) => {
const c = v / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};구간이 둘로 나뉜 이유는 아주 어두운 영역에서 거듭제곱 곡선의 기울기가 0에 수렴해 수치적으로 불안정해지기 때문입니다. 그래서 0.04045 아래는 직선으로 이어 붙입니다.
둘째, 세 채널의 무게가 같지 않습니다. 사람의 망막은 초록에 압도적으로 민감합니다. 그래서 가중치가 이렇습니다:
L = 0.2126 × R + 0.7152 × G + 0.0722 × B /* 선형화된 값에 적용 */- 초록 71.52% — 순수한 초록
#00ff00의 휘도는 0.72로, 거의 흰색 수준입니다. 초록 배경에는 검은 글씨를 얹어야 합니다. - 파랑 7.22% — 순수한 파랑
#0000ff의 휘도는 0.07로, 검정에 훨씬 가깝습니다. 흰 글씨가 8.59:1로 통과하고 검은 글씨는 2.44:1로 처참하게 떨어집니다.
단순 평균을 쓰면 이 둘이 똑같이 85로 나옵니다. 초록과 파랑을 같은 밝기라고 판단하는 겁니다. 이게 왜 위험하냐면, 틀리는 방향이 대체로 통과 쪽이기 때문입니다. 실제로는 읽을 수 없는 조합에 “합격” 도장을 찍어 주는 접근성 검사기는 없느니만 못합니다.
마지막으로 공식에 붙는 + 0.05가 있습니다. (밝은쪽 + 0.05) / (어두운쪽 + 0.05)인데, 이건 화면이 완전한 검정을 내지 못하고 주변광을 반사한다는 가정입니다. 이게 없으면 검정과 아주 어두운 색의 대비가 무한대로 발산합니다. 덕분에 대비비의 최댓값은 검정 대 흰색의 21:1로 고정됩니다.
4.5:1, 3:1, 7:1이 실제로 걸리는 지점
숫자 세 개가 각각 다른 상황을 가리킵니다. 헷갈리기 쉬우니 정리하면:
- 4.5:1 — 일반 텍스트 AA. 실무의 기본선입니다. 본문, 버튼 라벨, 링크, 플레이스홀더까지 전부 여기에 걸립니다. 법적 요구사항(한국의 웹 접근성 지침 포함)이 가리키는 것도 대개 이 선입니다.
- 3:1 — 큰 텍스트 AA, 그리고 비텍스트 요소.여기서 “큰 텍스트”는 굵게 18.66px(14pt) 이상 또는 보통 굵기 24px(18pt) 이상입니다. 22px 보통 글씨는 크다고 느껴져도 기준상 큰 텍스트가 아니라 4.5:1이 필요합니다. 그리고 이 3:1은 텍스트가 아닌 것에도 적용됩니다 — 입력칸 테두리, 토글의 on/off 상태, 정보를 전달하는 아이콘이 배경과 3:1은 되어야 합니다. 디자인 리뷰에서 가장 자주 빠지는 항목이 이겁니다.
- 7:1 — 일반 텍스트 AAA. 공공 서비스나 저시력 사용자를 명시적으로 겨냥한 제품이 아니면 전면 적용이 현실적이지 않습니다. 브랜드 색 대부분이 여기서 탈락합니다.
표에 없어서 자주 놓치는 항목: AAA의 큰 텍스트 기준은 4.5:1입니다. AA의 일반 텍스트 기준과 숫자가 같아서, 본문을 4.5:1로 맞춰 놨다면 제목은 이미 AAA입니다.
경계값 근처에서 하나 조심할 것이 있습니다. #767676은 흰 배경에서 4.5427:1로 간신히 통과하지만 #777777은 4.47:1로 떨어집니다. 한 단계 차이로 갈립니다. 그래서 도구가 보여 주는 소수점 둘째 자리를 반올림하면 4.4996이 “4.50 · 불합격”이라는 모순된 화면이 됩니다. 위 도구는 이 값을 내림으로 표시합니다 — 화면의 숫자와 판정이 항상 같은 말을 하게 하기 위해서입니다.
8자리 HEX의 마지막 두 자리는 퍼센트가 아니다
#RRGGBBAA 형식은 이제 IE를 제외한 모든 브라우저에서 동작합니다. rgba()를 쓸 이유가 거의 없어졌습니다. 다만 여기 함정이 하나 있습니다.
마지막 바이트는 00~FF이지 0~100이 아닙니다. 나머지 여섯 자리와 완전히 같은 규칙인데도, 알파를 퍼센트로 생각하는 습관 때문에 실수가 잦습니다:
#6366f150 /* 의도: 50% → 실제: 0x50 = 80 / 255 ≈ 31% */
#6366f180 /* 진짜 50% (0x80 = 128) */
#6366f1ff /* 불투명 */
#6366f100 /* 완전 투명 */#6366f150은 오류가 아니라 합법적인 다른 색이라서 아무도 경고해 주지 않습니다. 시안보다 연하게 나온 이유를 한참 찾게 됩니다. 자주 쓰는 값 몇 개만 외워 두면 편합니다: 80 = 50%, cc = 80%, 1a = 10%.
참고로 알파가 붙은 색의 대비비는 계산할 수 없습니다. 반투명 색의 실제 대비는 뒤에 무엇이 깔려 있느냐에 따라 완전히 달라지기 때문입니다. 위 도구가 알파를 제외하고 계산한다고 밝히는 이유입니다 — 임의로 흰 배경을 가정해 그럴듯한 숫자를 내놓는 것보다, 계산하지 않는다고 말하는 편이 정직합니다.
자주 묻는 질문
- #63f 같은 3자리 색상은 어떻게 6자리가 되나요?
- 각 자리를 하나씩 복제합니다. #63f는 #6633ff이지 #6033ff가 아닙니다. 뒤에 0을 채우는 방식이라면 #fff가 #f0f0f0(회색)이 되어 흰색을 표현할 방법이 사라지는데, 복제하면 f가 ff(255)가 되어 각 채널의 16단계가 0~255 구간 끝까지 정확히 닿습니다. 직접 파서를 짤 때 가장 흔히 틀리는 지점입니다.
- HSL의 명도(L)가 같으면 밝기도 같은 건가요?
- 아닙니다. 이게 HSL의 가장 큰 함정입니다. hsl(60, 100%, 50%)(노랑)과 hsl(240, 100%, 50%)(파랑)은 둘 다 L=50%라고 주장하지만, 실제 상대 휘도는 노랑이 0.93, 파랑이 0.07로 열두 배 넘게 차이 납니다. HSL의 L은 사람 눈의 감도가 아니라 RGB 최댓값과 최솟값의 산술 중간값일 뿐이라서, L을 고정한 팔레트를 만들면 색상마다 밝기가 들쭉날쭉해집니다.
- 대비비는 그냥 두 색의 밝기 차이 아닌가요?
- 밝기 차이는 맞지만 '밝기'를 구하는 방법이 단순 평균이 아닙니다. sRGB의 0~255 값은 감마 인코딩된 값이라 먼저 선형화해야 하고, 그다음 R·G·B에 각각 0.2126·0.7152·0.0722를 곱해 더합니다. (R+G+B)/3으로 계산하면 초록이 밝고 파랑이 어둡다는 사실을 놓쳐서, 실제로는 읽을 수 없는 조합에 합격 판정을 내리게 됩니다.
- 4.5:1, 3:1, 7:1은 각각 언제 적용되나요?
- 4.5:1은 일반 텍스트의 AA 기준, 3:1은 큰 텍스트의 AA 기준이자 버튼 테두리·아이콘 같은 비텍스트 요소의 기준, 7:1은 일반 텍스트의 AAA 기준입니다. 여기서 '큰 텍스트'는 굵게 18.66px(14pt) 이상 또는 보통 굵기 24px(18pt) 이상을 말합니다. 참고로 AAA의 큰 텍스트 기준은 4.5:1이라 AA의 일반 텍스트 기준과 숫자가 같습니다.
- #RRGGBBAA 8자리 HEX를 써도 되나요?
- IE를 빼면 모든 브라우저가 지원하므로 실무에서 써도 됩니다. 다만 마지막 두 자리는 0~100이 아니라 00~FF입니다. 50% 투명도는 50이 아니라 80(=128)이고, 완전 불투명은 100이 아니라 FF입니다. 이걸 착각해 #6366f150이라고 쓰면 의도한 50%가 아니라 약 31% 투명도가 됩니다.
- 요즘은 OKLCH를 쓴다던데 HSL을 버려야 하나요?
- 팔레트를 만드는 작업이라면 OKLCH가 확실히 낫습니다. OKLCH의 L은 지각 균등해서 L을 고정한 채 색상만 돌리면 정말로 같은 밝기의 색들이 나오고, HSL로는 불가능한 일입니다. 다만 기존 코드베이스는 여전히 HEX와 HSL로 가득하고 디자인 도구가 뱉는 값도 대개 그쪽이라, HSL을 읽고 쓸 줄은 알아야 합니다. 이 변환기는 HEX·RGB·HSL만 다룹니다.