들어가며#
“빨강과 초록으로 구분했으니까 괜찮을 거야.”
개발하다 보면 한 번쯤 이런 생각을 해보게 됩니다. 그런데 생각보다 많은 사람들이 이 두 색을 잘 구분하지 못합니다. 북유럽 혈통 기준 통계에서는 남성 약 1/12, 여성 약 1/200이 적색-녹색 색각이상을 가지고 있다고 알려져 있어요. 지역과 유전적 배경에 따라 비율은 달라질 수 있지만, 빨강-초록 구분이 어려운 사용자가 꾸준히 존재한다는 사실은 변하지 않습니다.
그리고 이게 전부가 아닙니다. 저시력 사용자, 나이가 든 사용자, 밝은 햇빛 아래서 화면을 보는 사용자들도 모두 색상 대비에 영향을 받아요.
색상 접근성은 단순히 “색맹인 사람도 볼 수 있게"하는 것이 아닙니다. 모든 사용자가 정보를 명확하게 인식하고, 색상이 정보 전달의 유일한 수단이 아니도록 설계하는 거죠.
이번 글에서는 색상 접근성의 이론부터 실무 팁, 검사 도구까지 정리했습니다. 현업에서 디자이너와 이 주제를 어떻게 다룰지도 함께 살펴볼게요.

제작: Nanobanana

제작: Nanobanana
색상 접근성이란?#
정의#
색상 접근성은 색상을 포함한 모든 사용자가 웹사이트의 정보를 정확하게 인식할 수 있도록 하는 설계 원칙입니다.
여기에는 다음이 포함됩니다:
- 색각이상 사용자: 특정 색상을 구분하기 어려운 사람들
- 저시력 사용자: 명도 대비가 낮으면 텍스트를 읽을 수 없는 사람들
- 노인: 나이가 들면서 색상 구분 능력이 저하되는 사람들
- 환경적 제약: 밝은 햇빛 아래나 저품질 모니터에서 보는 사용자들
표준과 요구사항#
색상 접근성은 WCAG 2.2에서 명확히 다룹니다:
- SC 1.4.3 (Contrast Minimum): 일반 텍스트 4.5:1, 큰 텍스트 3:1
- SC 1.4.6 (Contrast Enhanced): AAA 수준 7:1 / 4.5:1
- SC 1.4.11 (Non-text Contrast): 아이콘·입력 필드·포커스 표시 등 비텍스트 요소 3:1
- SC 2.4.11 / 2.4.12: 포커스가 다른 요소에 가려지지 않도록 보장
- SC 2.4.13 (Focus Appearance, AAA): 포커스 표시의 크기·대비 기준 제시
많은 조직과 규제가 WCAG 2.x를 참고하기 때문에, 이 기준을 이해하면 실무 적용이 훨씬 수월해집니다.
포용성 비즈니스 케이스#
색상 접근성은 법적 의무만이 아닙니다:
- 더 많은 사용자가 정보를 정확히 읽고 이해할 수 있음
- 검색 최적화 (명확한 색상 대비는 텍스트 가독성 향상)
- 브랜드 이미지 (접근성 있는 회사로의 인식)
- 사용성 개선 (모든 사용자가 더 잘 읽을 수 있음)
색각이상 이해하기#
색각이상의 유형#
1. 적색 약시 (Protanomaly): 빨강을 볼 때 회색처럼 보임
- 빨강 파장에 반응이 약함
2. 적색 맹 (Protanopia): 빨강을 완전히 못 봄
- 세상이 파랑-노랑으로만 보임
3. 녹색 약시 (Deuteranomaly): 초록을 볼 때 회색처럼 보임
- 가장 흔한 유형으로 알려짐
4. 녹색 맹 (Deuteranopia): 초록을 완전히 못 봄
5. 청색-황색 색각이상 (Tritanomaly/Tritanopia): 파랑과 노랑 구분 어려움
- 매우 드문 유형으로 알려짐

제작: Nanobanana
명도 대비(Luminance Contrast)#
WCAG 기준#
WCAG 2.2에서는 텍스트와 배경의 명도 대비를 숫자로 정의합니다:
| 레벨 | 일반 텍스트 | 큰 텍스트 |
|---|---|---|
| A | (요구사항 없음) | (요구사항 없음) |
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |
큰 텍스트의 정의:
- 18pt 이상, 또는
- 14pt 이상이고 굵은 글씨 (700 이상)
추가로 확인할 것 (WCAG 2.2):
- 비텍스트 대비 (SC 1.4.11): 아이콘, 테두리, 입력 필드 등 비텍스트 요소의 대비를 최소 3:1로 유지합니다.
- 포커스 가림 방지 (SC 2.4.11/2.4.12): 포커스 표시가 다른 UI에 가려지지 않도록 보장합니다.
- 포커스 표시 형태 (SC 2.4.13, AAA): 포커스 표시의 크기·대비 기준을 제시합니다. (높은 등급 목표 시 참고)
명도(Luminance) 계산#
// RGB를 명도로 변환하는 공식 (WCAG)
function getLuminance(r, g, b) {
// 0-255 범위를 0-1로 정규화
r = r / 255;
g = g / 255;
b = b / 255;
// 감마 보정
if (r <= 0.03928) {
r = r / 12.92;
} else {
r = Math.pow((r + 0.055) / 1.055, 2.4);
}
if (g <= 0.03928) {
g = g / 12.92;
} else {
g = Math.pow((g + 0.055) / 1.055, 2.4);
}
if (b <= 0.03928) {
b = b / 12.92;
} else {
b = Math.pow((b + 0.055) / 1.055, 2.4);
}
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function getContrast(foreground, background) {
const l1 = getLuminance(...foreground); // RGB 배열
const l2 = getLuminance(...background);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// 예시: 검은 텍스트 #000000 vs 흰 배경 #FFFFFF
const contrast = getContrast([0, 0, 0], [255, 255, 255]);
console.log(contrast); // 21 (아주 좋음!)
실용적인 대비 조합#
반드시 확인하세요:
/* ✅ 좋은 예 - 4.5:1 이상 */
.good-contrast {
color: #000000; /* 검은색 */
background: #FFFFFF; /* 흰색 */
/* 대비: 21:1 */
}
/* ❌ 나쁜 예 - 3:1 미만 */
.bad-contrast {
color: #777777; /* 회색 */
background: #CCCCCC; /* 밝은 회색 */
/* 대비: 약 2.79:1 */
}
/* ✅ 다크 모드 예 */
@media (prefers-color-scheme: dark) {
body {
color: #EEEEEE; /* 밝은 회색 텍스트 */
background: #1a1a1a; /* 어두운 배경 */
/* 대비: 약 15:1 */
}
}
제작: Nanobanana
도구를 이용한 검사#
- WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
- Chrome DevTools: Elements > Inspect > 색상 선택기
- Contrast Ratio 웹사이트: https://contrast-ratio.com/
색상만으로 정보 전달하면 안 된다#
문제 상황#
<!-- ❌ 나쁜 예 -->
<div>
<span style="color: red">오류</span>
<span style="color: green">성공</span>
</div>
<!-- 색각이상자는 둘 다 같은 색으로 보임 -->해결 방법#
<!-- ✅ 좋은 예 1: 텍스트 포함 -->
<div>
<span style="color: red">❌ 오류</span>
<span style="color: green">✅ 성공</span>
</div>
<!-- ✅ 좋은 예 2: 아이콘 + 텍스트 -->
<div>
<span style="color: red" aria-label="오류">
<i class="icon-error"></i> 오류 메시지
</span>
</div>
<!-- ✅ 좋은 예 3: 패턴 사용 -->
<svg width="100" height="100">
<!-- 빨강 + 대각선 패턴 -->
<rect fill="red" width="50" height="50"/>
<pattern id="diag" patternUnits="userSpaceOnUse" width="8" height="8">
<path d="M0,8 l8,-8 M-2,2 l4,-4 M6,10 l4,-4" stroke="black" stroke-width="1"/>
</pattern>
<rect fill="url(#diag)" width="50" height="50"/>
</svg>폼 입력 검증 예제#
<!-- ❌ 색상만 사용 -->
<input type="email" style="border: 2px solid red;">
<!-- ✅ 색상 + 아이콘 + 메시지 -->
<div class="form-group">
<input
type="email"
class="form-input has-error"
aria-describedby="email-error">
<svg class="error-icon" aria-hidden="true">
<use xlink:href="#icon-error"></use>
</svg>
<p id="email-error" class="error-message">
유효한 이메일 주소를 입력하세요
</p>
</div>.form-input.has-error {
border: 2px solid #d32f2f; /* 빨강 */
background-color: #ffebee; /* 연한 빨강 */
}
.error-icon {
color: #d32f2f; /* 빨강 */
}
.error-message {
color: #d32f2f;
/* 텍스트로 문제를 명확하게 설명 */
}색상 선택 가이드#
1단계: 먼저 명도로 설계#
/* 색상 없이 흑백으로 먼저 설계 */
body {
color: #333333; /* 어두운 회색 텍스트 */
background: #ffffff; /* 흰 배경 */
}
/* 이 상태에서 이미 4.5:1 이상의 대비 */2단계: 색상 추가#
/* 명도를 유지하면서 색상 추가 */
.primary {
color: #0962db; /* 파랑 - 명도 유지 */
background: #ffffff;
/* 명도 대비: 약 5.6:1 */
}
.secondary {
color: #1e7e34; /* 초록 - 명도 유지 */
background: #ffffff;
/* 명도 대비: 약 5.1:1 */
}3단계: 색맹 시뮬레이션 테스트#
// Deuteranopia (녹색 맹)를 위한 색상 변환
function simulateDeuteranopia(rgb) {
const [r, g, b] = rgb;
return [
0.625 * r + 0.375 * g,
0.7 * r + 0.3 * g,
b
];
}
// 테스트
const primary = [9, 98, 219]; // 파랑 #0962db
const simulated = simulateDeuteranopia(primary);
// 결과: [42, 36, 219] - 파랑이 약간 다르게 보이지만 구분 가능
// 참고: 실제 색각이상 시뮬레이션은 LMS 색공간 변환이 필요합니다. 이 함수는 교육용 근사치입니다.
권장 색상 팔레트#
/* 예시 팔레트 (AA 이상을 목표로 설계) */
/* 회색 톤 */
:root {
--gray-900: #111827; /* 거의 검은색 */
--gray-700: #374151; /* 어두운 회색 */
--gray-500: #6b7280; /* 중간 회색 */
--gray-100: #f3f4f6; /* 밝은 회색 */
--gray-50: #f9fafb; /* 거의 흰색 */
}
/* 기능색 (사용 맥락에 따라 대비 재검증 필요) */
:root {
--success: #065f46; /* 짙은 초록 */
--danger: #7f1d1d; /* 짙은 빨강 */
--warning: #92400e; /* 짙은 노랑/갈색 */
--info: #0c4a6e; /* 짙은 파랑 */
}
/* 흰 배경 위 사용 */
.success { color: var(--success); background: white; }
.danger { color: var(--danger); background: white; }
.warning { color: var(--warning); background: white; }
.info { color: var(--info); background: white; }
제작: Nanobanana
현업에서 자주 마주하는 상황: “이건 우리 브랜드 색이에요”#
디자이너와 함께 일하다 보면 이런 순간이 찾아옵니다.
“로고에서 따온 색상이라 바꾸기 어려워요. 이미 브랜드 가이드에 들어가 있고, 마케팅팀이랑도 맞춰둔 거라서요.”
색상 대비 문제를 제기했더니 돌아온 답변입니다. 이 상황, 낯설지 않으시죠?
브랜드 색상은 오랜 시간을 거쳐 결정된 결과물인 경우가 많습니다. 디자이너 입장에서는 “접근성 때문에 바꾸자"는 말이 단순한 수정 요청이 아니라 브랜드 아이덴티티를 건드리는 것처럼 느껴질 수 있거든요.
구체적인 상황을 가정해봅시다#
버튼 텍스트 색상이 브랜드 파랑 #4B9DFF이고, 배경은 흰색(#FFFFFF)입니다.
배경: #FFFFFF (흰색)
텍스트: #4B9DFF (밝은 파랑)
대비: 약 3.0:1 → AA 실패 (일반 텍스트 기준 4.5:1 필요)이 상황에서 “이 색은 안 됩니다"라고 말하면 대화가 막히는 경우가 많습니다. 대신 다음 단계로 접근하는 게 효과적입니다.
1단계: 데이터로 함께 확인하기
WebAIM Contrast Checker나 Chrome DevTools를 열고, 디자이너와 함께 실제 수치를 확인합니다. “규정 때문에 드리는 말씀이 아니라, 이 색상이 저시력 사용자에게 어떻게 보이는지 같이 확인해볼 수 있을까요?“라는 접근이 훨씬 잘 통합니다.
2단계: 대안 제시하기
“이 색상은 안 됩니다"보다 “이렇게 하면 브랜드 색감은 살리면서 기준도 맞출 수 있어요"가 협업의 문을 열어줍니다.
/* 기존 브랜드 색 - AA 실패 */
.btn {
color: #4B9DFF; /* 대비: 약 3.0:1 ❌ */
background: #FFFFFF;
}
/* 방법 1: 같은 계열, 더 어둡게 */
.btn {
color: #0062D1; /* 대비: 약 5.1:1 ✅ */
background: #FFFFFF;
}
/* 방법 2: 배경과 텍스트 반전 */
.btn {
color: #FFFFFF;
background: #0062D1; /* 대비: 약 5.1:1 ✅ */
}같은 파랑 계열에서 채도와 명도를 조정하면 브랜드 정체성을 크게 훼손하지 않으면서도 WCAG AA를 충족시킬 수 있는 경우가 많습니다.
3단계: 합의 내용 문서화하기
합의된 내용은 디자인 시스템이나 브랜드 가이드에 “접근성을 고려한 색상 사용 규칙"으로 함께 기록해 두는 게 좋습니다. 그래야 다음에 같은 대화를 반복하지 않아도 되거든요.
설득이 어려운 경우도 있습니다#
가끔은 현실적인 제약이 앞을 막습니다. “브랜드 가이드라인 변경은 경영진 승인이 필요하다”, “이미 인쇄물까지 나간 색상이다” 같은 상황이죠.
이럴 때 현업에서 자주 사용하는 방법들이 있습니다. 브랜드 색상을 그대로 유지하면서도 가독성을 높이는 기법들입니다.
방법 1: text-shadow로 아웃라인 효과 (이미지·그라디언트 배경에 효과적)
밝은 색상 텍스트가 복잡한 배경 위에 올라갈 때 가장 자주 쓰는 방법입니다. 지도 레이블, 히어로 섹션 타이틀에서 흔히 볼 수 있는 패턴이에요.
.hero-title {
color: #4B9DFF; /* 브랜드 파랑 */
/* 어두운 아웃라인으로 배경과의 대비를 확보 */
text-shadow:
-1px -1px 0 rgba(0, 30, 80, 0.85),
1px -1px 0 rgba(0, 30, 80, 0.85),
-1px 1px 0 rgba(0, 30, 80, 0.85),
1px 1px 0 rgba(0, 30, 80, 0.85);
}주의할 점이 있습니다. text-shadow는 WCAG 명도 대비 계산에 포함되지 않습니다. 자동화 도구는 여전히 “실패"로 표시할 수 있지만, 실제 사용자 가독성은 크게 향상됩니다. 도구 점수보다 실제 사용성을 함께 평가하는 게 좋습니다.
방법 2: text-stroke + paint-order (헤딩·디스플레이 텍스트에 효과적)
Modern CSS로 텍스트에 스트로크를 추가하는 방법입니다. paint-order를 함께 쓰면 스트로크가 텍스트 안쪽을 침범하지 않아요.
.display-heading {
color: #4B9DFF; /* 브랜드 파랑 */
-webkit-text-stroke: 3px #003380; /* 어두운 파랑 스트로크 */
paint-order: stroke fill; /* 스트로크를 fill 아래에 렌더링 */
}폰트 크기가 클수록 효과적입니다. 본문 텍스트에는 어색해 보일 수 있으니 큰 제목이나 강조 텍스트에 사용하세요.
방법 3: 텍스트 배경 칩 (UI 컴포넌트·배지에 효과적)
브랜드 색상을 장식 요소(테두리, 아이콘)에 유지하고, 텍스트에는 별도의 배경을 깔아 대비를 확보하는 방법입니다. 가장 안정적이고 WCAG 기준도 충족합니다.
.tag {
/* 브랜드 색상은 테두리로 유지 */
border: 2px solid #4B9DFF;
border-radius: 4px;
/* 텍스트는 충분한 대비 확보 */
color: #003380; /* 어두운 파랑: 흰 배경 대비 약 10:1 ✅ */
background: #FFFFFF;
padding: 4px 10px;
}
/* 또는 브랜드 색 배경 위에 흰 텍스트 */
.tag-filled {
background: #4B9DFF;
color: #FFFFFF;
/* 대비: 약 3.0:1 → 큰 텍스트 기준은 충족, 본문 기준은 아직 부족 */
/* 폰트를 14pt bold 이상으로 설정하면 AA 기준 통과 */
font-size: 1rem;
font-weight: 700;
}방법 4: 반투명 스크림 (이미지 위 텍스트에 효과적)
사진이나 그라디언트 배경 위에 텍스트가 올라가는 경우, 배경과 텍스트 사이에 반투명 레이어를 추가합니다.
.card-text-area {
/* 텍스트 영역에 반투명 어두운 배경 */
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.75) 0%,
rgba(0, 0, 0, 0) 100%
);
padding: 24px 16px 16px;
}
.card-title {
color: #FFFFFF; /* 흰 텍스트 */
/* 스크림 덕분에 충분한 대비 확보 */
}어떤 방법을 쓰든, 한 가지를 기억해 두면 좋습니다. 자동화 도구가 “실패"로 표시해도, 실제 사용자가 충분히 읽을 수 있다면 그것도 개선입니다. 접근성은 “완벽하거나 아무것도 아니거나"가 아닙니다. 할 수 있는 데서부터 시작하면 됩니다.
실무: 다크 모드와 색상#
다크 모드의 색상 대비 이슈#
/* ❌ 라이트 모드 색상을 다크 모드에 그대로 사용 */
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #0066cc; /* 라이트 모드의 파랑 */
/* 대비: 약 3.1:1 - 일반 텍스트에는 부족 */
}
}
/* ✅ 다크 모드에 맞게 조정 */
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #66b2ff; /* 밝은 파랑 */
/* 대비: 약 7.8:1 - 충분함 */
}
}테마 변수로 관리#
:root {
/* 라이트 모드 */
--text-primary: #111827;
--text-secondary: #6b7280;
--background-primary: #ffffff;
--background-secondary: #f3f4f6;
--accent-primary: #0962db;
--accent-danger: #7f1d1d;
}
@media (prefers-color-scheme: dark) {
:root {
/* 다크 모드 - 명도를 고려해 조정 */
--text-primary: #f3f4f6;
--text-secondary: #d1d5db;
--background-primary: #1a1a1a;
--background-secondary: #374151;
--accent-primary: #66b2ff;
--accent-danger: #fca5a5;
}
}
body {
color: var(--text-primary);
background: var(--background-primary);
}
.button-primary {
color: white;
background: var(--accent-primary);
}검사와 테스트#
수동 검사 체크리스트#
## 색상 접근성 체크리스트
### 명도 대비
- [ ] 텍스트와 배경 대비가 4.5:1 이상?
- [ ] 큰 텍스트(18pt+)도 3:1 이상?
- [ ] 버튼과 배경 대비도 확인했나?
### 색상 의존성
- [ ] 색상만으로 정보를 표현하지 않았나?
- [ ] 형태/패턴/텍스트로도 구분되나?
- [ ] 에러 메시지가 색상+텍스트로 표현되나?
### 색각이상 테스트
- [ ] 페이지를 색맹 시뮬레이터로 확인했나?
- [ ] 빨강-초록 구분이 중요한가?
- [ ] 다른 색상 조합도 테스트했나?
### 다크 모드
- [ ] 다크 모드에서도 대비가 충분한가?
- [ ] 색상이 자동으로 조정되나?자동화 도구#
// axe DevTools 검사 (브라우저 확장)
// https://www.deque.com/axe/devtools/
// 또는 프로그래매틱하게
async function checkContrast() {
const { axe } = window;
const results = await axe.run({
rules: ['color-contrast']
});
results.violations.forEach(violation => {
console.log('색상 대비 문제:', violation);
});
}권장 도구#

제작: Nanobanana
- WebAIM Contrast Checker: 두 색상의 대비 검사
- Color Blindness Simulator: 색맹 시뮬레이션
- axe DevTools: 자동화된 접근성 검사
- Lighthouse: Chrome DevTools 내장 검사
자세한 사용방법은 추후 포스트에서 다뤄보겠습니다.
자주 실수하는 패턴#
실수 1: 회색 텍스트로 부가 정보 표현#
/* ❌ 회색 텍스트는 저시력 사용자가 못 읽을 수 있음 */
.secondary-text {
color: #999999; /* 회색 */
background: #ffffff;
/* 대비: 약 2.85:1 - AA 미달 */
}
/* ✅ 어두운 회색 사용 */
.secondary-text {
color: #666666; /* 어두운 회색 */
background: #ffffff;
/* 대비: 약 5.74:1 - AA 만족 */
}실수 2: 포커스 스타일을 너무 옅게 설정#
/* ❌ 포커스 아웃라인이 옅음 */
button:focus {
outline: 1px solid #cccccc;
outline-offset: 1px;
}
/* ✅ 명확한 포커스 스타일 */
button:focus-visible {
outline: 3px solid #0962db;
outline-offset: 2px;
}실수 3: 라이트/다크 모드 색상을 동일하게 사용#
/* ❌ 둘 다 파랑 #0066cc 사용 */
.primary-color {
color: #0066cc;
}
/* ✅ 모드별로 조정 */
:root {
--primary: #0066cc;
}
@media (prefers-color-scheme: dark) {
:root {
--primary: #66b2ff;
}
}
.primary-color {
color: var(--primary);
}정리하며#
색상 접근성은 모든 설계자와 개발자가 알아야 할 기본 원칙입니다. 그리고 디자이너와의 협업에서 알 수 있듯, 이건 혼자서 해결하는 문제가 아니라 팀이 함께 만들어가는 품질이기도 하죠.
체크리스트로 정리하면 이렇습니다.
- ✅ 명도 대비 검사 (4.5:1 이상)
- ✅ 색상만으로 정보 표현 금지
- ✅ 색맹 시뮬레이터로 테스트
- ✅ 다크 모드에서도 대비 확인
- ✅ 포커스 스타일이 명확
- ✅ 자동화 도구로 검사
이 항목들을 하나씩 챙기다 보면, 단지 색각이상 사용자만이 아니라 저시력 사용자, 노인, 그리고 평범한 사용자까지 모두가 더 쉽게 쓸 수 있는 웹이 됩니다. 작은 색상 하나가 누군가의 경험을 바꿀 수 있다는 것, 기억해 두셨으면 좋겠습니다.
