Mu Hun
Engineering based on digital a11y.
웹 접근성, 모든 사람의 콘텐츠 접근을 지향하는 기술
김무훈, A11YKR 커뮤니티
발표 슬라이드 QR
컴퓨터 공학 전공, 4학년 재학 중
약 3년 간 스타트업에서 프런트엔드 서비스 개발을 맡았습니다.
모두를 위한 접근성 지원과 웹 표준 동향, 오픈소스 참여에 주목하며
웹 프런트엔드 엔지니어링에 대한 관심을 가지고 있습니다.
⬆️ URL: https://muhun.kim
웹 접근성이 정확히 무엇이고, 어떻게 지원을 하는게 좋을지...
평소 고려했던 웹 접근성의 가치와 접근성 준수의 경험을 담았습니다.
🤔
웹의 힘은 보편성에 있습니다. 장애와 관계없이 모두의 접근은 필수적인 측면입니다.
– Tim Berners-Lee, 웹의 창시자, Accessibility | Our mission | W3C
디지털 접근성이란 사용자의 정신적 또는 신체적 능력에 관계없이 웹사이트, 앱과 의미 있고 동등한 방식으로 상호작용할 수 있도록 제품을 설계하고 빌드하는 것을 의미합니다. — 접근성 원칙 - 디지털 접근성은 어떻게 측정되나요? | web.dev
웹의 힘 = 보편성, 모든 사람이 콘텐츠를 쉽게 인식하고 활용할 권리 지향 🌐✨
성공기준: "이 성공 기준의 의도는 텍스트가 아닌 콘텐츠로 전달하는 정보를 대체 텍스트를 사용하여 접근 가능하게 만드는 것이다."
— 텍스트가 아닌 콘텐츠 | WCAG 2 이해, A11YKR Docs
"보안을 소홀히 하거나, 나중에 고려하겠다": 『접근성을 지원한다는 착각 | 뱅크샐러드』 게시글의 첫 문단의 내용을 인용했습니다.
어떻게 보일지 = 심미성 VS 무엇을 = 정보 접근성
열차 항목을 티켓 형태로 재디자인하면서 화면에 표시되는 개수가 6개에서 3개로, 절반으로 줄어듦
효율성 감소: 탐색에 할애하는 시간이 증가
비교 기능 저하: 여러 선택지를 같이 비교하기가
어려워짐
첫번째 사진: 코레일 앱 "코레일톡" 예매 화면,
두번째 사진: 리뉴얼 된 웹 서비스 예매 화면
모든 사람이 콘텐츠를 쉽게 인지 · 조작 · 이해할 수 있는
보편성을 지향하는 접근성 원칙을 POUR라고 부름
인지 가능, 작동 가능, 이해 가능, 견고함은 모두 서로 연결됩니다.
WAI-ARIA 🧭
시맨틱 마크업을 잘 설계하면 접근성이 보장
그러나 서드파티 UI 구현이 필요한 경우,
WAI-ARIA로 접근성을 관리해야 함 📐
W3C WAI에서 만든 ARIA 작성 방법 가이드(APG)
자주 쓰이는 여러 UI 패턴의 사례가 접근성을 염두하여 잘 소개됨
= Web Accessibility Initiative – Accessible Rich Internet Applications
장애를 갖고 있는 사용자가 일반인과 동등한 웹 앱의 사용 경험을 제공하는 것이 목표
목표 달성을 위해 ARIA는 HTML 어트리뷰트를 확장하여 재정의
😵💫 이것만 기억해두세요,
ARIA = {역할, 상태, 속성}
ARIA는 웹 콘텐츠와 웹 애플리케이션을 만드는 방법을 정의하는
역할(Roles)과 상태(States), 속성(Properties)의 집합입니다.
— ARIA - Accessibility | MDN
대부분의 HTML 시멘틱 태그는 이미 고유한 역할 존재
HTML 시멘틱 태그에 없는 UI 패턴은 역할 재정의하여 표현
🎚 스위치 UI ➡️ <button role="switch" aria-checked="false/true">레이블</button>
<input type="search"
value="신발 브랜드"
aria-control="search-result"
aria-expanded="false">
<ul role="listbox"
id="search-result">
<li role="option">
스케처*
</li>
<li role="option">
뉴발란*
</li>
<li role="option">
나이*
</li>
</ul>
HTML에 존재하지 않는 다양한 상태 제공, 두가지 예시:
더 많은 ARIA 상태 목록은 "상태 탭" | WAI-ARIA 1.2 Cheat Sheet - DigitalA11Y 참고
🧭 aria-controls → 버튼(제어 트리거)↔ 리스트/팝업 제어
🏷️ aria-labelledby → 레이블 연결, 💬 aria-describedby → 설명 연결
🎯 aria-activedescendant → 현재 활성화된 하위 항목 명시,
주로 리스트 박스 UI 패턴에서 사용
🔔 aria-live → 시간에 민감 - 타이머, 채팅 로그, 주가 정보 등
🪜 aria-level → 계층 구조(H1, H2, H3 등 헤딩/트리 UI)
더 많은 관계 어트리뷰트 목록은 "6.6.4 Relationship Attributes, WAI-ARIA 1.2" 명세 참조
🚫 aria-disabled / aria-readonly → 비활성화 또는 읽기 전용
✳️ aria-required → 필수 입력 필드
📝 aria-placeholder → 입력 플레이스홀더
👁️🗨️ aria-hidden → 스크린 리더 등 보조 도구에서 장식용 UI를 숨기기 위해 사용
<h2 id="brand-label">신발 브랜드</h2>
<div role="combobox"
aria-expanded="true"
aria-labelledby="brand-label"
aria-controls="brand-list"
>
<input type="search"
value="나이*"
aria-activedescendant="opt-nik*"
aria-autocomplete="list"
/>
</div>
<ul role="listbox" id="brand-list">
<li role="option" id="opt-nik*" aria-selected="true">나이*</li>
<li role="option" id="opt-newbalanc*" aria-selected="false">뉴발란*</li>
<li role="option" id="opt-skecher*" aria-selected="false">스케처*</li>
</ul>
<input />의 역할을 "combobox"로 재정의하는 것은 유효하지 않아, 별도의 <div role="combobox" /> 로 감쌌습니다.
스크린리더 사용 데모
옵션 이동 시 실제로 포커스가 이동하지 않고, aria-activedescendant 로 AT에게 가상 포커스 정보를 제공
어디까지나 기본 = 시멘틱 마크업,
WAI-ARIA = 보완 수단
- <div role="button">주문하기
+ <button>주문하기</button>
+ <div role="tab">
- <h2 role="tab">제목 탭</h2>
+ <h2>제목 탭</h2>
+ </div>
- <div role="my-role">
No ARIA is better than Bad ARIA — Read Me First | APG | WAI | W3C
위에서 ARIA를 언제-어떻게 사용하고, 사용하면 안 되는 상황에 대해 잘 설명함
조금 더 친절한 가이드 — WAI-ARIA를 배울 때 정리해 두고 싶은 것 (일본어)
일본어지만 브라우저 번역을 통해 충분히 읽으실 수 있습니다.
아래 항목은 접근성 지원에 대한 일반적인 오해를 두 명제로 정리한 것
웹 접근성은 장애 사용자만을 위한 특수한 사용자 경험을 지칭하는 기술 분야이다.
여러 가지 오해가 있지만, 접근성을 준수하는 것은 충분히 제시간에 달성할 수 있는 목표입니다.
경험상 접근성 인터페이스를 별도로 고려하지 않고, 일반 UI와 함께 설계하는 방식을 따르면 됩니다.
저는 지난 2023년에 펜슬컴퍼니에서 “글리프“라는 콘텐츠 투고
웹 서비스의 출시 준비 3개월 전부터 프런트엔드 개발에 참여,
적절히 서비스에 웹 접근성 기술이 지원되도록 노력을 했습니다.
접근성 개선 및 ARIA 활용에 대한 제안
개발 내역은 모두 오픈소스로 열람할 수 있습니다.
재직했던 회사에서 공개 허가를 받고
공유드리는 대화 이력입니다.
<Tooltip enabled={invalidPrice} placement="top">
<span slot="message" aria-live="assertive"
id={errorTooltipId}>
{errorMessages.join('<br/>')}
</span>
<input
name="price"
aria-invalid={invalidPrice}
aria-describeby={errorTooltipId}
inputmode="numeric"
</Tooltip>
<style>
input[name="price"][aria-invalid="true"] {
border: 1px solid red;
background-color: rgb(255, 0, 0, 0.5);
}
</style>
팝업 형태의 포인트 설정 UI
<Tooltip enabled={invalidPrice} placement="top">
<span slot="message" aria-live="assertive"
id={errorTooltipId}>
{errorMessages.join('<br/>')}
</span>
<input
name="price"
+ class="aria-[invalid='true']:(border-error-900 bg-error-50)"
aria-invalid={invalidPrice}
aria-describeby={errorTooltipId}
inputmode="numeric"
</Tooltip>
오픈소스, penxle/withglyph#1296에서 확인 가능
실제로는 Tailwind 환경에서 작업했습니다.
Tailwind는 일부 ARIA 상태에 대해 예약된 variants로 기본 지원합니다.
지원하지 않은 ARIA 상태의 경우,
aria-[invalid="true"] 형태의 표기를 손수 써야 함
button.expanded {
background-color: lightgray;
& > i.chevron-icon {
transform: rotate(0.5turn);
}
}
button > i.chevron-icon {
transition: transform 0.5s;
}
ul {
position: absolute;
}
닫힌 상태
열린 상태
<Toolbar>
<button type="button"
aria-expanded={isFontListOpen}
class={isFontListOpen ? 'expanded' : ''}
onclick={() => isFontListOpen = !isFontListOpen}
>
프리텐다드 <i class="chevron-icon" />
</button>
<ul role="listbox" hidden={!isFontListOpen}>
<li role="option"
data-value="프리텐다드"
onclick={onSelectFontValue}
>
프리텐다드
<i class="selected-icon" />
</li>
// 기타 다른 옵션...
</ul>
</Toolbar>
보조 기술과 일반 사용자 각각 별도의 인터페이스 구현
단일 마크업을 보조 기술과 일반 사용자 양측에 사용
닫힌 상태
열린 상태
- button.expanded {
+ button[aria-expanded='true'] {
background-color: lightgray;
& > i.chevron-icon {
transform: rotate(0.5turn);
}
}
+ button[aria-expanded='true'] + ul[role='listbox'] {
+ display: block;
+ }
<Toolbar>
<button type="button"
aria-expanded={isFontListOpen}
- class={isFontListOpen ? 'expanded' : ''}
onclick={() => isFontListOpen = !isFontListOpen}
>
프리텐다드 <i class="chevron-icon" />
</button>
- <ul role="listbox" hidden={!isFontListOpen}>
+ <ul role="listbox">
<li role="option"
data-value="프리텐다드"
onclick={onSelectFontValue}
>
프리텐다드
<i class="selected-icon" />
</li>
// 기타 다른 옵션...
</ul>
</Toolbar>
(번역) 구닥다리 공룡을 위한 오늘날의 CSS — Steemit
원본 일러스트 출처 - Dinosaur Comics
CSS 명세에 따르면, 클래스네임은 HTML과 달리
의미를 담고 있지 않습니다. 따라서 클래스네임은 HTML의 의미를 대체할 수 없습니다.
"CSS는 클래스 어트리뷰트에 강력한 기능을 부여하여, 고유한 역할이 거의 없는 엘리먼트(예: HTML의 <div> 및 <span>)를 기반으로 나만의 “문서 언어"를 설계하고 “class” 어트리뷰트를 통해 스타일을 지정할 수 있습니다. 하지만 문서 언어의 구조적 요소는 일반적으로 정해진 의미가 있지만, 사용자가 임의로 정의한 클래스는 그렇지 않을 수 있습니다. 따라서 이러한 관행을 피하는 것이 좋습니다."
— “Class Selectors” Selectors Level 3 | W3C
li[role='option']
> i.selected-icon {
visually: hidden;
}
li[role='option'][aria-selected='true']
> i.selected-icon {
/* 선택되면 ✅ 아이콘 표시 */
visually: visible;
}
열린 상태
<Toolbar>
<button type="button"
aria-expanded={isFontListOpen}
onclick={() => isFontListOpen = !isFontListOpen}
>
프리텐다드 <i class="chevron-icon" />
</button>
<ul role="listbox">
<li role="option"
+ aria-selected={true}
data-value="프리텐다드"
onclick={onSelectFontValue}
>
프리텐다드
<i class="selected-icon" />
</li>
// 기타 다른 옵션...
</ul>
</Toolbar>
ARIA 표준은 시각적 스타일링 목적으로 만들어진 것이 아님,
세가지 웹 표준 명세 HTML, CSS, WAI-ARIA 에서
ARIA를 활용한 의미론적 CSS 작성은 찾을 수 없습니다.
⇥ Tab
, ⇧⇥ Shift+Tab
→ 입력 간 이동
아이템 사이 초점 이동: 방향키로 제한
Tab
키 입력 시: 다른 입력으로 포커스 이동
운영체제에서 보이는 일반적인 GUI 운용 방식을 따른 것
⇥ Tab,
⇠⇡⇣⇢ 방향키
펜슬컴퍼니의 또 다른 제품 "리더블"의 사이트 생성 단계 페이지
복합적인 UI 위젯은 탭 순서에 초점 가능한 하위 요소 1개만 포함
Tab
, Shift+Tab
→ 위젯 간 이동
아이템 사이 초점 이동: 방향키로 제한
선택 항목만 tabindex="0", 나머지 -1 (aka. "Roving tabindex")
Tab
키 입력 시: 탐색 중인 위젯을 벗어나 다른 입력으로 포커스 이동
운영체제에서 보이는 일반적인 GUI 운용 방식을 따른 것
관련하여 키보드 제어 컨설팅을 제공한 오픈소스 PR
penxle/readable#1128
일반적인 GUI 운용 방식: "but they are strongly advised to use the same key bindings as similar components
in common GUI operating systems as demonstrated in APG Patterns." — Keyboard Navigation Inside Components | APG
<h2 id="brand-label">신발 브랜드</h2>
<div role="combobox"
aria-expanded="true"
aria-labelledby="brand-label"
aria-controls="brand-list"
>
<input type="search"
value="나이*"
aria-activedescendant="opt-nik*"
aria-autocomplete="list"
/>
<ul role="listbox" id="brand-list">
<li role="option" id="opt-nik*"
tabindex="0"
aria-selected="true">나이*</li>
<li role="option" id="opt-newbalanc*" tabindex="-1"
aria-selected="false">뉴발란*</li>
<li role="option" id="opt-skecher*" tabindex="-1"
aria-selected="false">스케처*</li>
</ul>
</div>
⇥ Tab
, ⇧⇥ Shift+Tab
다음 항목으로 이동하거나, 이전 항목으로 돌아가기
적용 가능한 UI 패턴: 툴바, 콤보박스, 그리드, 메뉴, 라디오 그룹, 탭, 트리 뷰 입니다.
더 자세한 정보는 "기본 키보드 탐색 규칙, 키보드 인터페이스 개발 | ARIA 작성 방법 가이드"를 참고
접근성 표준에 맞춘 UI를 매번 직접 구현하는 것은 중복되고, 많은 시간이 소요 🤯
이를 위해 Headless UI라는 툴킷이 등장, 스타일링을 제외한 UI 로직과 접근성 지원만을 제공
Ark UI: Chakra UI 팀에서 스스로 사용하고자 만든 도구
여러 배포판(React, Solid, Vue, Svelte)을 동시에 제공 중
React Aria: Adobe에서 관리, 다양한 UI 패턴의 React 구현을 Headless UI로 제공
Melt UI: Svelte 사용자 커뮤니티에서 운영하는 Headless UI 도구
접근성 지원을 고려하지 않더라도, 자주 사용되는 UI 패턴의 보일러플레이트 구현과
의존성 주입(Dependency Injection)을 유연하게 지원하므로 적극 사용을 추천
관계성 명시는 보조 기술의 접근성 향상만 아니라,
UI 테스트를 구성할 때 유용한 힌트로 쓸 수 있음
import { render } from '@testing-library/svelte'
import Select from './Select.svelte'
const defaultSelectValue = '프리텐다드'
const rendered = render(Select)
// getByRole API를 이용하여 원하는 두 엘리먼트 쿼리하기
const triggerButton = rendered.getByRole('button')
const listbox = rendered.getByRole('listbox')
// triggerButton & listbox 관계성 일치하는지 확인
expect(triggerButton).toHaveAttribute('aria-controls', listbox.id)
expect(triggerButton).toHaveAttribute(
'aria-activedescendant',
makeOptionIdByFontValue(defaultSelectValue)
)
Playwright와 testing-library UI 테스트 도구에서
접근성 트리를 바탕으로 한 getByRole
쿼리 사용을
우선적으로 권장
Playwright: "To make tests resilient, we recommend prioritizing user-facing attributes and explicit contracts such as page.getByRole()." — Locating elemets, Locators
testing-library: https://testing-library.com/docs/queries/about/#priority
name: 쇼핑
role: region
children:
- name: shoppingbox
role: internal frame
children:
- name: shoppingbox
role: document
children:
- name:
role: generic
children:
- name:
role: generic
children:
- name:
role: tablist
children:
- name: 쇼핑
role: tab
description: 쇼핑은 광고영역입니다.
children:
- name: 쇼핑
role: text leaf
- name: 맨즈
role: tab
description: 맨즈는 광고영역입니다.
children:
- name: 맨즈
role: text leaf
- name: 원쁠딜
role: tab
children:
- name: 원쁠딜
role: text leaf
- name: 쇼핑라이브
role: tab
children:
- name: 쇼핑라이브
role: text leaf
- name:
role: generic
children:
- name:
role: generic
children:
- name: '1'
role: text leaf
- name:
role: generic
- name:
role: generic
- name: "/"
role: text leaf
- name: '6'
role: text leaf
- name: 이전 페이지
role: button
children:
- name:
role: generic
- name: 다음 페이지
role: button
children:
- name:
role: generic
- name:
role: tabpanel
children:
- name:
role: tablist
- name:
role: list
네이버 포털 쇼핑 섹션을 개발자 도구의 접근성 패널로 살펴보면,
“region(쇼핑) → tablist → tab…” 등으로 구성된 접근성 트리를 볼 수 있습니다.
키보드 제어 및 기타 상호작용 모두 접근성 트리 기반 운용
A11YKR 커뮤니티 멤버 탐정토끼님의
접근성 트리를 응용한 국제화 테스트 자동화 공유 트윗
접근성 트리를 UI 테스트 관점에서 바라본다면,
접근 가능한 정보의 변화만을 추적할 수 있습니다.
import { test, expect } from '@playwright/test';
test('Banner contains expected elements', async ({ page }) => {
await page.goto('https://playwright.dev/');
const banner = page.getByRole('banner');
await expect(banner.getByRole('heading', { level: 1 })).toHaveText(
/Playwright enables reliable end-to-end/
);
await expect(banner.getByRole('link').nth(0));
.toHaveText('Get started');
await expect(banner.getByRole('link').nth(1))
.toHaveText('Star microsoft/playwright on GitHub');
await expect(banner.getByRole('link').nth(2)).toHaveText(
/[\d]+k\+ stargazers on GitHub/
);
});
getByRole('link').nth(2).toHaveText(...)
검증을 항목마다 반복합니다. 🥱
import { test, expect } from '@playwright/test';
test('Banner contains expected elements', async ({ page }) => {
await page.goto('https://playwright.dev/');
const banner = page.getByRole('banner');
await expect(page.getByRole('banner')).toMatchAriaSnapshot(`
- banner:
- heading /Playwright enables reliable end-to-end/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- link /[\\d]+k\\+ stargazers on GitHub/
`);
});
접근성 트리를 YAML 형식으로 표현하여 스냅샷 테스팅을 지원합니다. 😇
공식 소개 겸 데모 영상: Getting started with ARIA Snapshots - 유튜브
다만, 이러한 활용은 다양한 브라우저와 보조기술(AT) 사이의 지원이 맞물려야 유의미함
기계가 읽을 수 있는 문서 🤖 (Machine-Readable Document)
모두가 쉽게 접근할 수 있는 웹 페이지는 기계에게도 동일하게 적용
SEO뿐만 아니라, LLM에도 정보 색인이 더 원활
"효율적인 소프트웨어 개발팀은 이러한 투쟁에서 정면으로 맞서 싸운다. 소프트웨어를 안전하게 지켜야 할 책임이 있기 때문이다."
— 『클린 아키텍처』 중에서
여기서 언급한 "투쟁"은 대립이 아닌 더 나은 방향으로 나아가기 위한 노력
위 삽화는 마이클 F. 지앙그레코(Michael F. Giangreco)라는
특수교육 전문가가 2002년에 제작한 유명한 자료입니다.
AOA11Y 접근성 오픈 아카데미: 정보접근성 인식 개선을 위한 유튜브 채널. 한국지능정보사회진흥원(NIA)에서 운영하며, 항공사 접근성 사례집 등의 다양한 접근성 사례를 제공합니다.
alt
속성의 필요성과 작성 방법을 설명한 국내 블로그 포스트. 시각 콘텐츠에 대한 대체 텍스트 제공 취지를 이해하는 데 도움이 됩니다.➡️ https://a11ykr.github.io "다 함께 접근성 배웁시다"
발표 자료 자문과 슬라이드 검수에 A11YKR 커뮤니티의 많은 도움을 받았습니다. 🙇
By Mu Hun