본문으로 건너뛰기

© 2026 Molayo

Smashing헤드라인2026. 05. 29. 01:32

알고리즘 테마 엔진: `contrast-color()`를 이용한 자가 수정형 컬러 시스템 구축

요약

웹 접근성 문제를 해결하기 위해 JavaScript 라이브러리 대신 CSS의 `contrast-color()` 함수를 사용하는 방법을 소개합니다. 이 함수는 브라우저가 스타일 계산 단계에서 자동으로 대비가 높은 색상을 선택하게 하여, 런타임 테마 변경 시에도 즉각적이고 효율적인 색상 적응을 지원합니다.

핵심 포인트

  • 기존 JS 라이브러리 의존 방식의 한계와 웹 접근성 저하 문제 지적
  • CSS `contrast-color()`를 통한 선언적이고 효율적인 대비 색상 계산
  • 런타임 테마 교체 시 별도의 재계산이나 이벤트 리스너 불필요
  • 현재 버전은 black 또는 white 색상 반환만 지원

HTTP Archive Web Almanac은 수년 동안 컬러 대비(color contrast) 실패 사례를 추적해 왔습니다. 하지만 수치는 거의 변하지 않았습니다. 디자인 시스템 도구, 접근성 린터(accessibility linters), 그리고 읽기 쉬운 텍스트 색상을 계산하는 데 전념하는 수많은 JavaScript 라이브러리가 지난 5년 동안 등장했음에도 불구하고, 2025년에도 웹사이트의 70%가 기본적인 WCAG 대비 검사를 통과하지 못하고 있습니다. WebAIM Million은 훨씬 더 암울한 상황을 보여줍니다. 2026년 기준으로 저대비 텍스트로 분류된 홈페이지는 83.9%로, 2025년의 79.1%에서 오히려 증가했습니다. 한 벤치마크에서는 매년 몇 퍼센트 포인트 정도 개선되는 듯 보이지만, 다른 벤치마크에서는 실제로 상황이 _악화_되고 있습니다. 이것은 진보가 아닙니다. 이토록 근본적인 문제를 해결하기 위해 런타임 JavaScript에 의존하는 방식이 오픈 웹(open web) 전체에 확장될 수 없음을 증명하는 것입니다. 우리에게 필요했던 것은 더 나은 라이브러리가 아니라, 더 나은 CSS였습니다.

contrast-color() 함수가 바로 그 더 나은 CSS입니다. 단 하나의 선언만으로 충분합니다. 브라우저는 페이지를 페인트(paint)하기 전, 스타일 계산(style computation) 단계에서 대비 수학 연산을 실행하여 적절한 텍스트 색상을 제공합니다. 라이브러리도, 빌드 단계(build step)도, 하이드레이션 플래시(hydration flash)도 필요 없습니다.

참고: 이전 기사나 명세 초안에서 color-contrast()라고 불리는 것을 보셨다면 — 그 이름은 변경되었으며, 이전 구문은 더 이상 어떤 브라우저에서도 작동하지 않습니다.

기능 (및 한계)

Level 5 버전은 간단합니다. 색상을 입력하면, 입력값과 대비가 더 높은 black 또는 white를 반환합니다.

.button {
  background-color: var(--brand-color);
  color: contrast-color(var(--brand-color));
...

--brand-color를 네온 그린(neon green)으로 바꾸면 텍스트는 검은색이 됩니다. 미드나잇 네이비(midnight navy)로 바꾸면 텍스트는 흰색이 됩니다. JavaScript를 통해 런타임에 테마를 교체하면 텍스트가 즉시 적응합니다. 이벤트 리스너(event listeners)나 재계산(recalculation)이 필요 없습니다.

현재 버전에 대해 알아두어야 할 몇 가지 사항은 다음과 같습니다:

  • 숫자가 아닌 <color>를 반환합니다. 실제 색상 값(black 또는 white)을 얻게 되므로, CSS에서 색상을 허용하는 곳이라면 어디든 사용할 수 있습니다.
  • 현재로서는 검은색(black) 또는 흰색(white)만 지원합니다. 후보 색상 목록(Candidate color lists)과 목표 대비율(target ratios)은 Level 6에서 구현될 예정입니다.
  • 키워드는 사용할 수 없습니다. 이전 블로그 포스트에서 max를 보셨다면, 그것은 명세(spec)에서 제거되었습니다. 이를 사용하면 선언이 조용히 깨지게 됩니다.
  • 위에서 언급했듯이, 이 함수는 초기 초안에서 color-contrast()라고 불렸습니다. 그 이름은 이제 사용되지 않습니다 — CSSWG는 CSS 함수는 반환하는 값에 따라 이름이 붙어야 한다는 관례를 따르기 위해 이름을 변경했습니다. color-mix()는 색상을 반환합니다. contrast-color()도 색상을 반환합니다. 이전의 color-contrast()라는 이름은 대비 비율(4.5와 같은 숫자)을 반환하는 것처럼 들려 오해의 소지가 있었습니다. color-contrast() 구문을 보여주는 2021~2023년 사이의 모든 튜토리얼은 현재 브라우저에서 작동하지 않을 것입니다.

명세의 분리: Level 5 대 Level 6

이 함수는 두 개의 명세에 걸쳐 존재합니다. 이는 이례적인 일이며 이해할 가치가 있습니다.

CSS Color Level 5는 오늘날 브라우저가 제공하는 기능을 정의합니다. 하나의 색상을 입력하면 검은색 또는 흰색이 출력됩니다. 알고리즘은 의도적으로 “UA-defined(사용자 에이전트 정의)”로 표시되어 있으며, 이는 브라우저가 내부적으로 어떤 수학적 계산을 사용할지 결정함을 의미합니다. 현재 모든 엔진은 WCAG 2.x 상대 휘도(relative luminance)를 사용합니다. 하지만 이 “UA-defined”라는 라벨은 우연이 아닙니다. 이는 계획된 탈출구(escape hatch)입니다.

이 맥락에서 APCA (Accessible Perceptual Contrast Algorithm)가 자주 언급되는 것을 보게 될 것입니다. APCA는 글꼴 두께, 공간 주파수(spatial frequency), 주변 밝기 등을 고려하여 인간의 눈이 실제로 대비를 어떻게 인지하는지를 모델링하며, 이는 WCAG 2.x 공식보다 진정으로 개선된 방식입니다. Level 5 명세에 *“WCAG 2.x 사용”*을 고정하지 않음으로써, 브라우저 제조사들은 기존 코드를 깨뜨리지 않고도 나중에 APCA로 교체할 수 있습니다. 만약 명세가 wcag2() 키워드를 기본값으로 포함하여 출시되었다면, 이를 사용하는 모든 사이트는 영구적으로 오래된 계산 방식에 갇히게 되었을 것입니다.

하지만 APCA의 미래는 과장된 홍보만큼 확실하지 않습니다. Adrian Roselli의 “2026년 4월 기준 WCAG3 대비(WCAG3 Contrast as of April 2026)”는 현재 상황을 명확하게 설명합니다. APCA는 워킹 그룹(Working Group)의 충분한 지지를 얻지 못해 2023년 중반에 WCAG 3 작업 초안(working draft)에서 제외되었습니다. 현재 WCAG 3 명세(spec)는 대비 알고리즘이 _“아직 결정되지 않았다(yet to be determined)”_고 명시하고 있으며, 표준 자체도 2030년 또는 그 이후에나 확정될 수 있습니다. 또한 Roselli는 2024년 5월에 Chromium 이슈를 제기하며, DevTools에서 “Advanced Perceptual Contrast Algorithm(APCA)” 실험용 플래그를 완전히 제거해 달라고 요청했습니다. 그는 해당 구현이 구식이며, 개발자들에게 APCA가 실제보다 더 진척되었거나 더 공식적인 것처럼 오해를 불러일으킬 위험이 있다고 주장했습니다. 해당 이슈는 여전히 해결되지 않은 상태(open)로 남아 있습니다.

이 모든 것이 APCA가 끝났음을 의미하지는 않습니다. 그 이면의 연구는 동료 검토(peer-reviewed)를 거친 실질적인 내용이며, 제작자는 APCA 가이드라인을 통과하는 색상들이 대다수의 경우 WCAG 2의 최소 기준을 크게 상회한다는 점을 언급한 바 있습니다. 하지만 현재로서는 APCA가 WCAG 2.x를 대체할 알고리즘이 될 것이라는 보장이 없으며, 이러한 불확실성은 contrast-color()에 있어 매우 중요합니다. 만약 다른 알고리즘이 승리하거나 WCAG 3가 완전히 새로운 것을 채택하게 된다면, “UA-defined(User Agent 정의)”라는 라벨 덕분에 브라우저가 코드를 깨뜨리지 않고도 적응할 수 있기 때문입니다. 이는 또한 Level 6 기능들 — 후보 색상 목록(candidate color lists), 목표 대비율(target ratios), tbd-fg/tbd-bg 키워드 — 이 모두 현재 형태로는 실현될 수도 있고 그렇지 않을 수도 있는 알고리즘을 중심으로 설계되었음을 의미합니다.

CSS Color Level 6는 확장된 구문인 후보 색상 목록과 목표 대비율을 추가합니다:

/* Level 6 미래 구문 — 아직 출시되지 않음 */
color: contrast-color(var(--bg) tbd-bg wcag2(aa), #1a1a2e, #e2e8f0, #fbbf24);

브라우저는 왼쪽에서 오른쪽으로 각 후보를 평가하여 4.5:1 AA 임계값(threshold)을 충족하는 첫 번째 색상을 선택합니다. tbd-fgtbd-bg 키워드는 기준 색상이 전경색(foreground)인지 배경색(background)인지를 나타내며, 이는 APCA와 같은 방향성 대비 모델(directional contrast models)에서 중요합니다. 이 모든 것은 Working Draft 단계이며, APCA의 불확실한 상태를 고려하면 더욱 그러합니다. 지금은 Level 5 버전을 사용하세요.

브라우저 지원 (Browser Support)

이 기능은 대부분의 새로운 CSS 기능보다 상태가 좋습니다. 세 가지 주요 엔진 모두 안정적인 릴리스(stable releases)에 이를 포함하여 출시했습니다: Chrome 147 (2026년 4월), Firefox 146, 그리고 Safari 26.0. 이 기능은 2026년 4월에 Baseline Newly Available 상태에 도달했습니다. 전체 버전 매트릭스는 caniuse에서 확인하세요. 세 엔진 모두 contrast-color()에 대한 Web Platform Tests를 통과했으며, 이는 예외 케이스(예: 동점 처리 로직(tie-breaking logic), 색 공간 변환(color space conversion), 구문 파싱(syntax parsing))가 브라우저 간에 동일하게 동작함을 의미합니다.

caniuse의 원시 전역 지원 비율(raw global support percentage)은 낮아 보일 수 있지만, 이는 주로 기업용 브라우저와 업데이트를 전혀 하지 않는 사용자들을 반영한 것입니다. 이 글을 읽고 있다면, 귀하의 브라우저는 거의 확실히 이미 이를 지원하고 있을 것입니다.

@supports를 사용하면 점진적 향상(Progressive enhancement)을 간단하게 구현할 수 있습니다:

.card {
  background: var(--bg);
  color: #fff;
...

구형 브라우저에서는 가독성을 위해 어두운 그림자가 있는 흰색 텍스트가 표시됩니다. 지원하는 브라우저에서는 네이티브 계산(native calculation)이 적용됩니다. 그 누구도 깨진 텍스트를 보게 되지 않습니다.

주의해야 할 점이 하나 있습니다. 자동화된 접근성 스캐너(Lighthouse, Axe 등)는 text-shadow를 평가할 수 없습니다. 이들은 오직 계산된 colorbackground-color 사이의 대비만을 확인합니다. 따라서 그림자(shadow) 덕분에 사람의 눈에는 텍스트가 완벽하게 읽히더라도, 폴백(fallback) 설정은 CI/CD 파이프라인에서 여전히 대비 실패로 표시될 것입니다. 만약 팀에서 자동화된 접근성(a11y) 검사를 실행한다면, 해당 특정 규칙을 허용 목록(allowlist)에 추가하거나 해당 경고가 왜 오탐(false positive)인지 설명하는 주석을 달아야 할 수도 있습니다.

PostCSS에 관한 참고 사항:

빌드 타임에 contrast-color()를 평가하는 플러그인(@csstools/postcss-contrast-color-function)이 있습니다. 이 플러그인은 contrast-color(#ff0000)와 같은 정적 색상(static colors)에는 작동합니다. 하지만 커스텀 속성(custom property)인 contrast-color(var(--bg))를 사용하는 순간, 플러그인은 런타임 값(runtime values)에 접근할 수 없기 때문에 도움이 되지 않습니다. 만약 여러분의 테마가 동적(dynamic)이라면(이 작업을 수행하는 목적 그 자체이기도 합니다), 폴리필(polyfill)은 건너뛰고 @supports에 의존하세요.

주의 사항 (The Gotchas)

지각적(Perceptual) 또는 AAA 준수를 보장하지는 않음

이 부분에서 사람들이 실수할 수 있습니다: "대비 함수를 사용했으니, 이제 내 사이트는 접근성 검사를 통과하겠지?"

수학적으로는? 대개 그렇습니다. 특정 "중간 톤(mid-tone)" 배경색의 경우, 검은색과 흰색 모두 표준 WCAG 4.5:1 AA 비율을 통과하지 못한다는 지속적인 미신이 있습니다. 이는 수학적으로 틀린 말입니다. WCAG 2.x 상대 휘도(relative luminance) 공식에 따르면, 순수한 검은색과 순수한 흰색이 모두 AA를 통과하지 못하는 배경색은 절대 존재하지 않습니다. 하나(또는 둘 다)는 항상 통과하게 됩니다.

#2277d3(중간 파란색)를 예로 들어보겠습니다. 이 색상은 검은색과 흰색이 실제로 모두 AA를 통과하는(둘 다 약 4.58:1에 도달함) 수학적 경계선에 걸쳐 있습니다. contrast-color()는 그중 수학적으로 아주 미세하게라도 우위에 있는 색상을 여러분에게 제공할 것입니다.

하지만 여기에 실제적인 함정(gotcha)이 있습니다. WCAG 2.x의 수학적 계산에는 인지적 사각지대(perceptual blind spots)가 존재한다는 사실이 알려져 있습니다. 동일한 #2277d3 색상에 검은색 텍스트를 사용하면 수학적으로는 AA 등급을 통과하지만, 사람의 눈에는 읽기가 매우 어려울 수 있습니다. contrast-color()수학적 준수(compliance)를 제공하며, 이는 자동화된 감사(automated audits)에는 매우 유용하지만, 그것이 항상 인지적 접근성(perceptual accessibility)과 일치하는 것은 아닙니다. (이것이 바로 APCA가 존재하는 이유이며, 브라우저가 나중에 알고리즘을 교체할 수 있도록 스펙이 설계된 이유이기도 합니다.)

게다가 더 엄격한 WCAG AAA 표준(7.0:1)을 목표로 한다면, 실제적인 데드 존(dead zone)이 존재합니다. 휘도(luminance)가 대략 10%에서 30% 사이인 배경의 경우, 검은색도 흰색도 7:1을 달성할 수 없습니다. 그런 경우 contrast-color()는 여러분을 구원할 수 없으며, 그저

그 때문에 흰색에서 검은색으로 페이드(fade)되는 동안의 시각적 동작이 심하게 왜곡됩니다. 텍스트가 중간 지점에서 즉시 전환되지 않습니다. 애니메이션의 대부분 동안 검은색을 유지하다가, 배경이 극도로 어두워지는 전환의 맨 마지막 순간에만 흰색으로 급격히 바뀝니다. 이는 매우 부자연스럽고 늦게 발생하는 하드 컷(hard cut)입니다.

transition-behavior: allow-discrete가 이를 해결해 줄 것이라고 생각할 수도 있습니다. 하지만 그렇지 않습니다. allow-discrete는 이 부자연스러운 시각적 경험을 해결하지 못하는데, 이는 이 속성이 이진 출력(binary output)을 보간(interpolate)할 수 없기 때문입니다. 단지 하드 스냅(hard snap)이 발생하는 타이밍을 애니메이션 지속 시간의 50% 지점으로 옮겨줄 뿐입니다. 부드러운 텍스트 색상 전환이 필요하다면, color-mix()를 레이어링하거나 크로스페이드(crossfade)를 직접 관리해야 합니다.

무승부는 흰색의 승리

만약 배경이 검은색과 흰색 모두 동일한 대비비(contrast ratio)를 생성하는 완벽한 중간 회색(middle gray)인 경우, 스펙에는 하드코딩된 타이브레이커(tiebreaker)가 정의되어 있습니다: 흰색이 승리합니다. 실제 적용 시 큰 문제는 아니지만, 회색 팔레트를 디버깅하는 중에 텍스트가 예상대로 작동하지 않는다면 알아둘 가치가 있습니다.

그라데이션과 이미지는 제외

이 함수는 단일 <color> 값을 받습니다. 그라데이션이나 url()을 전달할 수는 없습니다. contrast-color(linear-gradient(...))는 구문 오류(parse error)입니다. 배경이 사진이거나 복잡한 그라데이션인 경우, 오버레이 텍스트를 위해 여전히 JavaScript를 사용하거나 수동으로 색상을 선택해야 합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Smashing Magazine의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0