컴퓨터공학 300 주제 시리즈의 214번째 글이다. 전체 지도는 여기.

한 줄 요약

웹 접근성은 시각·청각·운동·인지 능력이나 사용 환경과 관계없이 누구나 웹 콘텐츠를 인식하고 조작하고 이해할 수 있게 만드는 일이고, 그 기준은 W3C 의 WCAG 가 검증 가능한 성공 기준으로 정해 둔다.

왜 필요한가

접근성을 “소수를 위한 추가 기능”으로 생각하기 쉽다. 실제로는 다음 사용자가 모두 해당한다.

  • 스크린 리더로 웹을 듣는 시각장애인, 확대 기능을 쓰는 저시력자
  • 손 떨림이나 부상으로 마우스 대신 키보드·스위치를 쓰는 사람
  • 동영상 소리를 켤 수 없는 지하철 안의 사용자(자막)
  • 햇빛 아래에서 휴대폰 화면을 보는 사람(대비)
  • 나이가 들어 작은 글자와 작은 버튼이 불편해진 부모님

접근성 요구는 계약·조달·법 규정으로도 들어온다. 여러 나라의 공공 기준이 WCAG 를 참조한다. 나중에 한꺼번에 고치려면 디자인 시스템부터 다시 만들어야 할 수도 있으므로, 처음부터 기본기로 넣는 것이 싸다.

핵심 개념

WCAG 의 네 원칙

WCAG 2.2 는 성공 기준을 네 원칙 아래에 묶는다. 앞 글자를 따서 POUR 라고 부른다.

원칙 뜻 대표 기준 예
인식의 용이성(Perceivable) 감각 중 하나로 인식할 수 있어야 이미지 대체 텍스트, 자막, 명도 대비
운용의 용이성(Operable) 다양한 입력으로 조작할 수 있어야 키보드 조작, 초점 표시, 충분한 시간
이해의 용이성(Understandable) 내용과 조작 방법을 이해할 수 있어야 언어 지정, 예측 가능한 동작, 오류 안내
견고성(Robust) 보조 기술이 해석할 수 있어야 이름·역할·값이 프로그램으로 판별 가능

각 성공 기준에는 A, AA, AAA 수준이 있다. 일반적인 목표치로 널리 쓰이는 것은 AA 수준이다.

자주 걸리는 기준

대체 텍스트. 정보를 전달하는 이미지에는 alt 로 그 정보를 적는다. 장식용 이미지는 alt="" 로 비워 보조 기술이 건너뛰게 한다. alt 속성 자체를 빼면 스크린 리더가 파일 이름을 읽기도 한다.

<img src="chart-q3.png" alt="3분기 매출은 2분기보다 12% 증가">
<img src="divider.svg" alt="">

명도 대비. AA 기준으로 본문 글자는 배경과 4.5:1 이상, 큰 글자는 3:1 이상이어야 한다. 대비는 두 색의 상대 휘도로 계산한다(아래 실습).

키보드 조작. 마우스로 할 수 있는 모든 일을 키보드로 할 수 있어야 한다. 초점(focus)이 어디 있는지 보여야 하고, 초점이 갇혀 빠져나오지 못하면 안 된다. outline: none 으로 초점 표시를 지우는 CSS 는 가장 흔한 위반 중 하나다.

이름·역할·값. 모든 조작 요소는 보조 기술이 알 수 있는 이름과 역할을 가져야 한다. 아이콘만 있는 버튼은 이름이 비어 있기 쉽다.

<button aria-label="검색"><svg aria-hidden="true">...</svg></button>

폼 오류. 오류가 나면 무엇이 왜 틀렸는지 글로 알려 준다. 빨간 테두리처럼 색으로만 알리면 색을 구분하지 못하는 사용자는 모른다.

터치 대상 크기. WCAG 2.2 에 새로 들어간 2.5.8 대상 크기(최소) 기준은 AA 수준에서 조작 대상이 원칙적으로 24×24 CSS 픽셀 이상이거나 충분한 간격을 갖도록 요구한다.

ARIA 는 보완재다

WAI-ARIA 는 HTML 만으로 표현하기 어려운 역할과 상태(탭, 트리, 펼침 여부 등)를 보조 기술에 알려 주는 속성 모음이다. 하지만 ARIA 는 의미만 바꾸고 동작은 추가하지 않는다. role="button" 을 붙여도 Enter 키 처리는 직접 구현해야 한다. 그래서 원칙은 이 순서다.

  1. 네이티브 HTML 요소로 되면 그것을 쓴다(202번 주제).
  2. 안 되면 W3C 의 ARIA 작성 지침(APG)에 있는 패턴대로 역할·상태·키보드 동작을 모두 구현한다.
  3. 상태가 바뀌면 속성도 바꾼다(aria-expanded="true" 등).

잘못된 ARIA 는 없는 것보다 나쁘다. 보조 기술에게 틀린 정보를 주기 때문이다.

동적 콘텐츠

단일 페이지 앱에서 화면 일부만 바뀌면 스크린 리더 사용자는 바뀐 줄 모른다.

  • 라우트가 바뀌면 새 화면의 제목(<h1>)으로 초점을 옮기거나 문서 제목을 갱신한다.
  • 모달을 열면 초점을 모달 안으로 옮기고, 닫으면 연 버튼으로 돌려준다. 최신 브라우저의 <dialog> 요소와 showModal() 은 이 동작의 상당 부분을 기본으로 제공한다.
  • “저장되었습니다” 같은 알림은 aria-live 영역으로 읽어 준다.

자동 검사와 수동 검사

자동 검사 도구(axe, Lighthouse 등)는 대비 부족, alt 누락, 라벨 없는 입력칸 같은 기계적인 문제를 빠르게 잡는다. 하지만 대체 텍스트의 내용이 적절한지, 키보드 순서가 자연스러운지, 오류 메시지가 이해되는지는 사람이 확인해야 한다. 최소한 다음 두 가지는 직접 해 본다.

  • 마우스를 치우고 Tab, Shift+Tab, Enter, Space, Esc, 방향키만으로 핵심 흐름(가입, 결제)을 끝까지 해 본다.
  • 운영체제 내장 스크린 리더(macOS VoiceOver, Windows 내레이터 등)를 켜고 같은 흐름을 들어 본다.

직접 해 보기

WCAG 의 상대 휘도 정의로 명도 대비를 계산한다. sRGB 각 채널을 선형화한 뒤 가중합으로 휘도를 구하고, (밝은 쪽 + 0.05) / (어두운 쪽 + 0.05) 로 대비를 낸다.

def channel(c8):
    c = c8 / 255
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4

def luminance(hex_color):
    h = hex_color.lstrip("#")
    r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
    return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)

def contrast(fg, bg):
    l1, l2 = sorted((luminance(fg), luminance(bg)), reverse=True)
    return (l1 + 0.05) / (l2 + 0.05)

def grade(ratio, large=False):
    aa, aaa = (3, 4.5) if large else (4.5, 7)
    return "AAA" if ratio >= aaa else "AA" if ratio >= aa else "실패"

pairs = [("#000000", "#FFFFFF"), ("#777777", "#FFFFFF"), ("#767676", "#FFFFFF"),
         ("#FFFFFF", "#1E88E5"), ("#9E9E9E", "#FFFFFF")]
print(f"{'글자':8} {'배경':8} {'대비':>6}  본문  큰글자")
for fg, bg in pairs:
    r = contrast(fg, bg)
    print(f"{fg:8} {bg:8} {r:6.2f}  {grade(r):4}  {grade(r, large=True)}")

실행 결과:

글자       배경           대비  본문  큰글자
#000000  #FFFFFF   21.00  AAA   AAA
#777777  #FFFFFF    4.48  실패    AA
#767676  #FFFFFF    4.54  AA    AAA
#FFFFFF  #1E88E5    3.68  실패    AA
#9E9E9E  #FFFFFF    2.68  실패    실패

눈으로는 거의 구분되지 않는 #777777 과 #767676 이 기준선을 사이에 두고 갈린다. 흰 바탕의 회색 안내 문구, 파란 버튼 위의 흰 글씨처럼 디자인에서 흔한 조합이 본문 기준을 통과하지 못하는 경우가 많다. 대비 최댓값은 검정과 흰색의 21:1 이다. 디자인 토큰을 정할 때 이 계산을 스크립트로 돌려 두면, 새 색을 추가할 때마다 자동으로 걸러 낼 수 있다.

현업에서는

  • 디자인 시스템에서 해결한다: 버튼·입력칸·모달·탭 같은 기반 컴포넌트가 접근성을 갖추면 모든 화면이 따라온다. 반대로 기반 컴포넌트가 틀리면 수백 화면이 한꺼번에 틀린다.
  • CI 에 자동 검사: E2E 테스트에 axe 같은 검사 도구를 붙여 새 위반이 들어오면 실패시킨다. 자동 검사가 못 잡는 항목은 릴리스 체크리스트로 둔다.
  • 초점 표시 되살리기: 디자인 요청으로 초점 테두리를 지웠다면 :focus-visible 로 키보드 사용 시에만 보이게 바꾸는 절충이 있다.
  • 사내 도구도 예외가 아니다: 홈랩이나 사내 관리 화면도 언젠가 손을 다친 동료가 키보드만으로 쓰게 된다. 표는 <table>, 버튼은 <button> 으로만 만들어도 대부분의 문제가 생기지 않는다.

확인 문제

  1. WCAG 의 네 원칙을 쓰라.
  2. 장식용 이미지와 정보 이미지의 alt 처리는 어떻게 다른가.
  3. AA 수준의 본문 글자 명도 대비 기준은? 큰 글자는?
  4. role="button" 을 붙인 <div> 가 여전히 접근성 문제가 있는 이유는?
  5. SPA 에서 모달을 열고 닫을 때 초점은 어떻게 다뤄야 하는가.

풀이

  1. 인식의 용이성, 운용의 용이성, 이해의 용이성, 견고성(POUR).
  2. 장식용은 alt="" 로 비워 건너뛰게 하고, 정보 이미지는 그 이미지가 전하는 정보를 alt 에 적는다.
  3. 본문 4.5:1 이상, 큰 글자 3:1 이상.
  4. ARIA 는 역할 정보만 바꿀 뿐 동작을 추가하지 않는다. 포커스 가능(tabindex)과 Enter·Space 키 처리를 직접 구현해야 한다.
  5. 열 때 모달 안으로 초점을 옮기고 모달 안에 머물게 하며, 닫을 때는 모달을 연 요소로 초점을 되돌린다.

더 읽을거리 (References)