이 글은 누구를 위한 것인가
- "접근성은 중요한데 어디서 시작해야 하나" 싶은 디자이너
- 디자인 시스템 토큰에 명암비 기준을 적용하려는 팀
- 색맹 사용자를 배려하는 팔레트 설계가 필요한 프로덕트 디자이너
들어가며
"이 회색 텍스트 읽기 힘들어요"라는 사용자 피드백의 뒤에는 명암비 기준 미달이 있다. WCAG 2.1은 법적 요건인 국가도 있고(미국 ADA, EU 접근성법), 디자인 품질 기준으로도 의미가 있다.
색상 접근성은 시각 장애인만을 위한 것이 아니다. 강한 햇빛 아래 스마트폰을 보는 모든 사용자, 노안이 있는 중장년, 저조도 환경의 모든 사람에게 해당된다.
이 글은 bluefoxdev.kr의 디자인 시스템 접근성 가이드 를 참고하고, 색상 명암비 실전 적용 관점에서 확장하여 작성했습니다.
1. WCAG 2.1 명암비 기준
[명암비 기준 요약]
텍스트 (일반):
AA (최소): 4.5 : 1
AAA (향상): 7.0 : 1
텍스트 (대형, 18pt+ 또는 Bold 14pt+):
AA: 3.0 : 1
AAA: 4.5 : 1
UI 컴포넌트 / 아이콘:
AA: 3.0 : 1
(텍스트와 무관하게 적용)
명암비 계산 공식:
비율 = (L1 + 0.05) / (L2 + 0.05)
L = 상대 휘도 (Relative Luminance)
L1 = 더 밝은 색, L2 = 더 어두운 색
예시:
흰 배경(#FFFFFF, L=1.0) + 검정 텍스트(#000000, L=0)
→ (1.0 + 0.05) / (0 + 0.05) = 21 : 1 ✅ AAA 통과
흰 배경 + 회색 텍스트(#767676, L≈0.21)
→ (1.0 + 0.05) / (0.21 + 0.05) ≈ 4.5 : 1 ✅ AA 통과 (경계)
2. 도구별 명암비 체크 방법
[Figma 플러그인]
1. Contrast (by Figma) — 내장 기능
접근: Design 패널 → Fill → 명암비 수치 표시
한계: 선택된 레이어만 체크
2. Able (무료 플러그인)
기능: 두 레이어 선택 → 명암비 + WCAG 레벨 표시
추가: 색맹 시뮬레이션 (8가지 유형)
설치: Figma Community → "Able – Friction free accessibility"
3. Color Blind (플러그인)
기능: 전체 프레임을 색맹 시뮬레이션으로 미리보기
유형: Deuteranopia(적록), Protanopia, Tritanopia(청황)
[브라우저 도구]
1. Chrome DevTools
방법: Elements → Computed → Color 클릭
→ "Contrast Ratio" 자동 표시
F12 → Rendering → Emulate vision deficiencies (색맹 시뮬)
2. axe DevTools (크롬 확장)
기능: 페이지 전체 접근성 자동 감사
명암비 오류 항목 → 개선 제안까지 제공
[웹 도구]
WebAIM Contrast Checker: webaim.org/resources/contrastchecker
Coolors Contrast Checker: coolors.co/contrast-checker
3. 디자인 토큰 명암비 사전 검증
// 디자인 토큰 명암비 자동 검증 스크립트
function hexToRgb(hex: string): [number, number, number] {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
}
function relativeLuminance(r: number, g: number, b: number): number {
const toLinear = (c: number) => {
const s = c / 255;
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
};
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
function contrastRatio(hex1: string, hex2: string): number {
const [r1, g1, b1] = hexToRgb(hex1);
const [r2, g2, b2] = hexToRgb(hex2);
const L1 = relativeLuminance(r1, g1, b1);
const L2 = relativeLuminance(r2, g2, b2);
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
function checkWCAG(foreground: string, background: string, isLargeText = false): {
ratio: number;
AA: boolean;
AAA: boolean;
} {
const ratio = contrastRatio(foreground, background);
const aaThreshold = isLargeText ? 3.0 : 4.5;
const aaaThreshold = isLargeText ? 4.5 : 7.0;
return {
ratio: Math.round(ratio * 100) / 100,
AA: ratio >= aaThreshold,
AAA: ratio >= aaaThreshold,
};
}
// 디자인 토큰 검증 실행
const tokenPairs = [
{ name: 'Primary Button', fg: '#FFFFFF', bg: '#0066FF' },
{ name: 'Body Text', fg: '#333333', bg: '#FFFFFF' },
{ name: 'Caption', fg: '#767676', bg: '#FFFFFF' },
{ name: 'Disabled Text', fg: '#AAAAAA', bg: '#FFFFFF' },
{ name: 'Error Text', fg: '#D32F2F', bg: '#FFFFFF' },
];
tokenPairs.forEach(({ name, fg, bg }) => {
const result = checkWCAG(fg, bg);
const status = result.AA ? (result.AAA ? '✅ AAA' : '✅ AA') : '❌ FAIL';
console.log(`${name}: ${result.ratio}:1 → ${status}`);
});
/*
출력 예시:
Primary Button: 5.92:1 → ✅ AA
Body Text: 12.63:1 → ✅ AAA
Caption: 4.48:1 → ❌ FAIL ← 개선 필요
Disabled Text: 2.32:1 → ❌ FAIL ← 의도적 예외 처리
Error Text: 5.81:1 → ✅ AA
*/
4. 색맹 친화 팔레트 설계
[색맹 유형별 영향]
적록색맹 (Deuteranopia/Protanopia — 가장 흔함, 남성의 8%):
문제: 빨강-초록 구분 불가
영향: 성공(초록) vs 에러(빨강) 구분 안 됨
해결: 색상 + 아이콘 + 텍스트 함께 사용
청황색맹 (Tritanopia — 드묾):
문제: 파랑-노랑 구분 불가
해결: 색상에만 의존하지 않는 설계
[색맹 안전 팔레트 원칙]
❌ 색상만으로 의미 전달:
빨간 텍스트 = 에러 (색맹 사용자에게 안 보임)
✅ 색상 + 보조 수단:
에러: 🔴 빨간 아이콘(⚠) + "오류" 텍스트 + 밑줄
색상 대신 사용 가능한 수단:
- 아이콘 (체크, 경고, 에러 각각 다른 모양)
- 패턴 (차트에서 색 대신 해치 패턴)
- 텍스트 레이블
- 도형/모양 (색 외에 형태로 구분)
[차트/그래프 색맹 안전 팔레트]
추천: Wong (2011) 8색 팔레트
#000000 (검정)
#E69F00 (주황)
#56B4E9 (하늘)
#009E73 (청록)
#F0E442 (노랑)
#0072B2 (파랑)
#D55E00 (주황적)
#CC79A7 (분홍보라)
5. 다크모드 접근성
[다크모드 명암비 함정]
라이트모드 통과 ≠ 다크모드 통과
예시:
라이트: 흰 배경(#FFF) + 파란 텍스트(#0066FF) = 5.92:1 ✅
다크: 검정 배경(#1A1A1A) + 같은 파란 텍스트(#0066FF) = 2.35:1 ❌
[다크모드 토큰 전략]
방법 1: 토큰 분리
--color-primary-light: #0066FF; /* 라이트모드 */
--color-primary-dark: #4D94FF; /* 다크모드 — 밝게 조정 */
방법 2: 상대 휘도 기반 자동 보정
// 다크모드 자동 색상 보정
function adjustForDarkMode(hex: string, targetRatio = 4.5): string {
const darkBg = '#1A1A1A'; // 어두운 배경 기준
let current = hex;
let ratio = contrastRatio(current, darkBg);
// 충분히 밝아질 때까지 밝기 증가
while (ratio < targetRatio) {
const [r, g, b] = hexToRgb(current);
const factor = 1.05;
const newR = Math.min(255, Math.round(r * factor));
const newG = Math.min(255, Math.round(g * factor));
const newB = Math.min(255, Math.round(b * factor));
current = `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
ratio = contrastRatio(current, darkBg);
if (newR === 255 && newG === 255 && newB === 255) break;
}
return current;
}
마무리
색상 접근성은 "나중에 할 일"이 아니다. 디자인 토큰을 정의하는 단계에서 명암비 검증을 자동화해두면, 이후 모든 컴포넌트가 기준을 자동으로 충족한다.
색맹 시뮬레이션은 Figma에서 1분이면 확인 가능하다. 배포 전에 한 번만 돌려봐도 의도치 않은 접근성 오류를 잡을 수 있다.