17장 접근성과 반응형
접근성은 WCAG 2.2 AA를 기준으로, 동작은 primitive가, 보이는 표현은 디자인 시스템이, 이름은 화면 작성자가 맡습니다. 반응형은 변경 수준에 맞는 실제 화면을 정해진 폭에서 열고, 넘침과 가림을 숫자로 기록해 확인합니다.
왜 필요한가
AI가 만든 화면은 그럴듯해 보여도 키보드로 닿지 않거나, 아이콘 버튼에 이름이 없거나, 좁은 휴대폰에서 가로로 밀릴 수 있습니다. 스크린샷 한 장으로는 이런 결함을 놓칩니다. 「충분히 크게」「적당히 진하게」 같은 기준은 사람마다 다르게 읽힙니다. 그래서 Leysys는 기준을 표준과 숫자로 정하고, 누가 무엇을 책임지는지 나눕니다. 접근성 규약은 판단 기준을 이렇게 적습니다. 「30대 경리가 빠르게 처리하면서, 50대 관리소장이 햇빛 아래 스마트폰으로 봐도 문제없는가?」
원칙 / 모델
기준과 책임
기준은 WCAG(Web Content Accessibility Guidelines, W3C 웹 콘텐츠 접근성 지침) 2.2 AA이고, 가능하면 AAA를 따릅니다. 국내 기준 KWCAG 2.2와도 맞춥니다. 책임은 세 곳으로 나눕니다. primitive는 동작·키보드·ARIA만 맡는 모양 없는 부품(15장)이고, ARIA는 보조기술에 역할과 상태를 알리는 HTML 속성입니다.
| 영역 | 담당 |
|---|---|
| 키보드 처리, 대화상자의 포커스 가두기, ARIA, 낭독 알림 | primitive (Reka UI, Drawer는 vaul-vue) |
| 포커스 링, 색 대비, 상호작용 대상 크기, 움직임 줄이기 | 디자인 시스템 |
| 접근 가능한 이름(accessible name) | 화면 작성자 |
이 분담은 규약 한 곳에만 적습니다. 각 컴포넌트 문서의 Accessibility 섹션은 W3C WAI-ARIA 패턴 링크, 키보드 동작 표, ARIA 속성 표와 그 컴포넌트에만 해당하는 주의점만 둡니다.
UI를 바꿀 때 확인하는 네 가지
검증 원칙 P-A11Y(4장)는 UI 변경마다 다음을 요구합니다.
- 접근 가능한 이름 — 화면 낭독기가 읽을 이름이 모든 컨트롤에 있습니다. 시맨틱 HTML을 먼저 쓰고, ARIA는 HTML로 해결되지 않을 때만 보탭니다. 아이콘만 있는 버튼은
aria-label이나 화면에 숨긴 텍스트가 필수입니다. - 포커스 순서 — 보이는 순서와 DOM 순서가 같습니다.
tabindex는0과-1만 씁니다. - 키보드 함정 0 — 키보드만으로 들어가고, 쓰고, 빠져나올 수 있습니다.
- ARIA 연결 — label과 설명이 실제로 컨트롤에 연결됩니다.
포커스 링
포커스 링은 브라우저가 포커스 표시가 필요하다고 판단할 때 맞는 :focus-visible 선택자에서 같은 토큰으로 그립니다. EDS는 --e-focus-ring-width: 2px, --e-focus-ring-offset: 2px이고 색은 포커스 테두리 색입니다. MDS도 --m-focus-ring-*에 같은 두께와 간격을 둡니다. 2px은 WCAG 2.4.13(포커스 모양, AAA)의 두께 기준에 맞춘 값입니다. 모양은 버튼처럼 바깥에 그리는 링, 달력 칸처럼 안쪽(-2px)에 그리는 링, 입력 테두리, 슬라이더 손잡이 둘레의 빛 번짐(glow), 날짜 입력 조각처럼 배경으로 보이는 표시의 다섯 갈래뿐입니다. 실제로 포커스를 받는 요소(focus owner)는 목록으로 관리하고, 감사 스크립트는 EDS·MDS의 목록과 CSS가 어긋나면 실패로 보고합니다.
상호작용 대상 크기
상호작용 대상은 24×24 CSS px 이상을 권장합니다. WCAG 2.5.8의 이 기준에는 간격·대체 수단 등 예외가 있어, 예외 없는 하한으로 단정하지 않습니다. EDS 높이 스케일에서는 xs(24px) 이상이 이 크기를 만족합니다.
- 컴포넌트의 공개 size가 곧 대상 크기입니다. 보이지 않는 클릭 영역을 넓히거나
min-height·padding을 덧대지 않고, 더 큰 size나 다른 컴포넌트를 고릅니다. - size는 포인터 종류가 아니라 화면의 맥락·밀도·위계로 고릅니다. 44px(AAA)을 모든 컨트롤에 강제하지 않습니다.
- 상호작용 요소 사이는 8px 이상 띄웁니다.
대비와 움직임
컬러 모델(9장)에서는 단계(step) 차이만 보고 대비를 가늠할 수 있습니다. 50~950 단계 안의 두 색이 5단계 떨어지면 3:1 이상(최소 3.24), 6단계면 4.5:1 이상(최소 4.88), 7단계면 7:1 이상(최소 7.33)입니다. 흰색·검정 배경은 이 계산에 들어가지 않습니다. 다만 기본 테마의 모든 색 조합이 이 차이를 지키지는 않습니다.
OS에서 움직임 줄이기(prefers-reduced-motion: reduce)를 켜면 EDS와 MDS가 각각 모든 duration 토큰을 0으로 만듭니다. 컴포넌트는 transition 시간을 직접 쓰지 않고 이 토큰을 참조하므로 움직임이 함께 사라집니다. 5초 넘게 자동 재생되는 콘텐츠에는 일시정지·정지·숨기기 수단을 둡니다.
실제 예
hover card 대신 클릭 카드
변경 이력(16장)에서 행위 주체 이름을 누르면 사용자 카드가 열립니다. 마우스를 올리면 열리는 hover card는 쓰지 않았습니다. Radix와 Reka의 Hover Card 문서는 키보드 사용자가 내용에 닿을 수 없다고 안내하고, Base UI Preview Card 문서는 터치·키보드·화면 낭독기로 닿을 수 없다고 적습니다. 그래서 버튼과 네이티브 Popover API로 만들었습니다.
vue
<button type="button" :popovertarget="id" aria-haspopup="dialog">{{ name }}</button>
<div :id="id" popover="auto" role="dialog" :aria-labelledby="`${id}-name`">…</div>클릭·Enter·Space·탭으로 열리고, 시트나 대화상자 안에서도 최상위 레이어에 뜨며, 바깥 클릭과 Esc로 닫힙니다. 위치는 CSS anchor positioning이 정하므로 위치 계산 스크립트가 없습니다. 네이티브 API를 먼저 쓰는 이유는 14장에서 다룹니다.
native label-wrap
자기 label을 가진 입력은 <label> 안에 컨트롤을 넣습니다. label과 컨트롤이 암묵적으로 연결되므로 id를 만들어 잇지 않아도 됩니다.
vue
<label class="e-checkbox e-checkbox-md">
<ECheckbox v-model="checked" />
<span class="e-checkbox-label">Receive newsletter</span>
</label>가려지는 포커스를 숫자로 찾은 일
WCAG 2.4.11은 포커스 표시가 다른 콘텐츠에 가려지지 않기를 요구합니다. 목록의 조회 조건 행은 좁으면 가로로 스크롤되며, Tab으로 반쯤 가려진 컨트롤에 들어가면 네이티브 scrollIntoView로 그 컨트롤을 보여 줍니다. 처음에 둔 포커스 여유 4px은 링 두께 2px과 간격 2px의 합과 같아 남는 공간이 없었습니다. 오른쪽에서 왼쪽으로 쓰는 화면의 소수점 스크롤 끝에서 링이 0.5625px 잘리는 것을 재현했고, 여유를 다음 간격 단계인 8px로 늘렸습니다. 좌표를 직접 계산하거나 Tab 키를 가로채지 않았습니다.
반응형 화면 검증
뷰포트
| 범주 | 뷰포트 |
|---|---|
| 기본 대표 | 412×915 · 768×1024 · 1440×900 |
| 모바일 | 360×800 · 412×915 · 448×998 |
| 태블릿 | 768×1024 · 1024×768 |
| 전환 경계 | 1023×900 |
| 데스크톱 | 1280×800 · 1440×900 · 1920×1080 |
EDS와 MDS는 1024px 이상에서 좌측 메뉴를 고정하고, 그 미만에서는 같은 메뉴를 시트로 엽니다(목적지가 3~5개인 모바일 우선 앱은 시트 대신 하단 탭을 쓸 수 있습니다). 1024는 메뉴 폭 256px과 본문 최소 폭 768px의 합입니다. 1023×900은 데스크톱 대표가 아니라 메뉴가 숨는 바로 아래 폭이며, 1024×768과 함께 보면 전환의 양쪽을 확인합니다. 360~448px 사이에는 기기별 breakpoint(화면 폭 전환점)를 만들지 않습니다.
변경 수준
검증 범위는 바뀐 파일 수가 아니라 화면 영향 수준으로 정합니다.
| 수준 | 대상 | 뷰포트 |
|---|---|---|
| L0 | 문서·테스트·주석·보이지 않는 로직 | 브라우저 생략 |
| L1 | 문구·메뉴명·버튼명·아이콘 | 기본 대표 3개 |
| L2 | 모바일 전용 | 모바일 3개 |
| L3 | 태블릿 전용 | 태블릿 2개, 전환에 영향이 있으면 경계 추가 |
| L4 | 컴포넌트·레이아웃 | 412·768·1023·1024·1440, 영향 따라 추가 |
| L5 | 전역 셸·디자인 시스템 | 9개 전체 |
태블릿에서는 hover 없이도 모든 기능에 닿는지 함께 봅니다. CI(지속적 통합)는 CSS·토큰·registry·패키지 공개 표면이 바뀌면 L5, Vue 화면 파일이 바뀌면 L4를 최소로 씁니다. 경로만으로는 문구 변경과 레이아웃 변경을 구분할 수 없기 때문입니다.
숫자로 기록하기
스크린샷과 함께 DOM 수치를 잽니다. 페이지 가로 넘침은 document.documentElement.scrollWidth <= document.documentElement.clientWidth로 봅니다. 텍스트는 요소의 scrollWidth·scrollHeight가 보이는 크기를 넘지 않아야 하며, 의도한 말줄임과 내부 스크롤은 제외합니다. fixed·sticky 요소의 겹침은 스크롤 영역에 잘린 뒤 실제로 그려진 영역끼리 비교합니다. 결과에는 route(화면 주소), 변경 수준, 뷰포트와 각 수치를 적고 「PASS」 한 단어로 대신하지 않습니다. 실행하지 않은 항목은 「N-A」와 이유를 적습니다.
경계와 예외
- 대화상자는 열려 있는 동안 포커스를 안에 가두지만 Esc나 닫기·취소 버튼으로 나올 수 있습니다(Esc를 막는 확인 대화상자의 잠금 모드도 취소 버튼은 남습니다). 「키보드 함정 0」은 빠져나올 길이 항상 있다는 뜻입니다.
- 24px은 권장이지 법적 하한의 단정이 아니며, MDS에도 모바일 전용 최소 크기를 따로 두지 않습니다.
- 기본 테마는 모든 색 조합에서 WCAG 2.2 AA 대비를 보장하지 않습니다. 인증이 필요한 제품은 강조 글자와 흰 글자 면을 600 단계에 연결하는 등 제품에서 대응합니다.
- 디자인 규칙과 다른 배치는 알림이지만, 실행으로 재현된 접근성 실패는 배포를 막습니다.
정리
- 기준은 WCAG 2.2 AA이고, 동작·표현·이름의 책임을 primitive·디자인 시스템·작성자가 나눠 맡습니다.
- UI를 바꾸면 이름, 포커스 순서, 키보드 함정, ARIA 연결을 확인합니다.
- 대상은 24px 이상을 권장하고, 공개 size가 곧 대상 크기입니다.
- hover로만 열리는 UI 대신 키보드와 터치로 닿는 클릭 UI를 씁니다.
- 반응형은 변경 수준에 맞는 뷰포트에서 실제 route를 열고, 넘침과 겹침을 숫자로 남깁니다.
정본
leysys-designdocs/specs/A11Y-CONVENTION.md— 접근성 기준, 책임 분담, 포커스·대상 크기·움직임leysys-designdocs/specs/MDS-FOCUS-VISIBLE-INVENTORY-2026-07-12.md— MDS focus owner 목록과 감사leysys-designdocs/process/RESPONSIVE-VIEWPORT-VALIDATION-STANDARD.md— 뷰포트, 변경 수준, 수치 기록leysys-designdocs/decisions/TOUCH-TARGET-24PX-RECOMMENDATION-2026-08-12.md— 24px 권장leysys-designdocs/decisions/ICON-ONLY-CONTROL-SELECTION-AND-TARGET-SIZE-2026-09-22.md— 공개 size가 곧 대상 크기leysys-designdocs/decisions/RECORD-HISTORY-2026-09-24.md— 클릭으로 여는 사용자 카드의 근거leysys-designdocs/decisions/LAYOUT-NAVIGATION-BREAKPOINT-AND-CONTENT-WIDTH-2026-09-27.md— 1024px 전환점