Skip to main content
ENKO

접근성

접근성은 따로 챙기는 체크리스트가 아니다. Senco의 모든 컴포넌트는 처음부터 접근성을 고려해 만든다. 기준은 최소 WCAG 2.2 Level AA다.

Senco 사용자 중에는 액정이 깨진 폰을 쓰는 사람, 흔들리는 차 안에서 한 손으로 앱을 여는 사람, 앱 언어가 제2·제3 언어인 사람, 송금이 한 번 실패하면 월세가 밀리는 사람이 있다. 그래서 여기서 접근성은 디자인 위에 덧붙이는 규정 준수 작업이 아니라, 제품을 쓸 수 있게 만드는 일 그 자체다.

목표 기준

모든 컴포넌트, 패턴, 페이지 템플릿은 WCAG 2.2 Level AA를 지킨다. 현실적으로 가능하면 Level AAA까지 간다. 금융 데이터에 강화 대비를 적용하고, 터치 타깃은 AAA 요구치보다도 큰 48px로 둔다.

WCAG 2.2는 2.1과 하위 호환되므로, 2.2 AA를 충족하면 2.1 AA도 충족한다. 2.2에서 새로 생긴 기준 중 Senco에 특히 중요한 것은 다음과 같다.

CriterionLevelSenco position
2.4.11 Focus Not Obscured (Minimum)AA고정 액션 바가 포커스된 필드를 가리면 안 된다
2.5.7 Dragging MovementsAA드래그가 필요한 컨트롤은 없다. 모든 슬라이더에는 숫자 입력이 있다
2.5.8 Target Size (Minimum) — 24pxAA기준보다 크다. Senco는 48px을 요구한다
3.2.6 Consistent HelpA도움말 진입점은 모든 화면에서 같은 자리에 있다
3.3.7 Redundant EntryA수취인과 주소 정보는 이전 단계에서 미리 채워진다
3.3.8 Accessible Authentication (Minimum)AA인지 기능 테스트 없음. OTP 필드는 붙여 넣기를 허용한다

송금 제품에서는 Redundant Entry와 Accessible Authentication이 실제로 중요하다. 두 단계 전에 입력한 계좌 번호를 다시 입력하게 해서는 안 된다. 붙여 넣기를 막은 OTP 필드는 시간에 쫓기는 사용자에게 여섯 자리 코드를 외우게 만든다.

색과 대비

RequirementRatioApplies to
일반 텍스트4.5:1본문, 라벨, 설명
큰 텍스트 (굵게 18.66px 이상 또는 24px 이상)3:1제목, 큰 라벨
UI 컴포넌트3:1테두리, 아이콘, 포커스 링, 폼 컨트롤
텍스트 외 대비3:1차트, 그래프, 상태 표시
  • 색만으로 의미를 전달하지 않는다. 항상 텍스트, 아이콘, 패턴을 함께 쓴다
  • 상태 배지는 색, 점, 텍스트 라벨을 함께 쓴다
  • 에러 상태는 빨간 테두리, 에러 아이콘, 설명 텍스트를 함께 쓴다

큰 텍스트의 함정

"큰 텍스트"는 굵게 18.66px 이상이거나, 굵기와 상관없이 24px 이상인 텍스트다. 18px semibold 제목은 큰 텍스트에 들지 않는다. Senco의 H4가 딱 이 경우다. 그래서 H2와 H3은 굵기 700으로 지정했고, H4는 4.5:1 기준을 그대로 지킨다. Typography 참고.

대비 검증

모든 토큰 조합은 라이트와 다크 모드에서 각각 검증한다. 전체 표는 Color와 Themes에 있다.

기준을 깔끔하게 통과하지 못한 항목이 둘 있다. 둘 다 반올림해 넘기지 않고 측정값 그대로 기록한다.

ItemMeasuredStatus
기본 상태 입력 테두리 (흰색 위 {gray-200})1.24:1허용한 예외. 필드에 항상 보이는 라벨과 48px 컨트롤 영역이 있다
채움 위의 비활성 버튼 라벨. 라이트는 {gray-300} 위 {gray-500}, 다크는 #334155 위 #7E8EA3라이트 3.76:1, 다크 3.10:1면제. SC 1.4.3은 비활성 컴포넌트를 제외한다. 그래도 기록하는 이유는 면제를 받을지가 선택의 문제이기 때문이다. Senco의 비활성 컨트롤은 포커스를 받고 비활성인 이유를 알려 주므로, 라벨도 그대로 읽힌다

다크 모드 Primary 버튼 라벨은 3.23:1로 해결되지 않은 결함이었지만, Platform 계층에서 --ds-on-accent를 재매핑해 해결했다. Themes 참고.

Do / Don't
Do
You send
$0.00
CA flagCAD

Visible label above the input stays present when the user types.

Don't
$0.00
US flagUSD

Placeholder-only label disappears on focus, leaving no context.

All text and UI element pairings must meet minimum contrast ratios.

ElementMinimum ratioStandard
Normal text (<18px)4.5:1WCAG 1.4.3 AA
Large text (18px+ bold, 24px+)3:1WCAG 1.4.3 AA
UI components & icons3:1WCAG 1.4.11
Focus indicators3:1WCAG 2.4.7
Aa
Heading on white
17.74:1
PASS
Aa
Body on white
10.31:1
PASS
Aa
White on primary
5.1:1
PASS
Aa
Primary on white
5.1:1
PASS
Aa
White on destructive
5.01:1
PASS
Aa
Muted on white
5.54:1
PASS
Aa
Muted on surface
5.04:1
PASS
Aa
Placeholder text
4.5:1
PASS

키보드 내비게이션

모든 상호작용 컴포넌트는 키보드만으로 완전히 조작할 수 있어야 한다.

KeyAction
Tab다음 상호작용 요소로 포커스 이동
Shift+Tab이전 요소로 포커스 이동
Enter / Space버튼, 링크, 토글 실행
Arrow keys메뉴, 탭, 라디오 그룹 안에서 이동
Escape다이얼로그, 툴팁, 팝오버, 드롭다운 닫기
Home / End목록의 처음/마지막 항목으로 이동

복합 컴포넌트당 Tab 정지점은 하나

라디오 그룹, 탭 목록, 스테퍼, 메뉴 바는 각각 Tab 정지점이 하나다. Tab으로는 컴포넌트 사이를, 화살표 키로는 컴포넌트 안을 이동한다. 옵션이 열 개인 통화 라디오 그룹을 지나는 데 Tab을 열 번 눌러야 한다면, 조작은 가능해도 실제로는 쓸 수 없다.

포커스 관리

  • 포커스 표시는 두 가지다. 테두리가 있는 컨트롤(입력, 셀렉트)은 테두리 색을 --ds-focus로 바꾸고, 오프셋 없이 불투명도 20%의 3px 후광을 더한다. 테두리가 없는 컨트롤(버튼, 링크, 탭, 행)은 2px 오프셋을 두고 3px 실선 --ds-focus 외곽선을 그린다. 어느 쪽을 쓸지는 Tokens에 정리했다
  • 3:1을 넘어야 하는 것은 포커스된 테두리나 외곽선이며, 둘 다 흰 배경에서 5.00:1이다. 후광은 장식이라 계산에 넣지 않는다
  • :focus 대신 :focus-visible을 쓴다. 마우스로 클릭했을 때는 링이 남지 않아야 하고, Tab을 눌렀을 때는 남아야 한다
  • 포커스 순서는 시각적인 읽기 순서를 따른다 (왼쪽에서 오른쪽, 위에서 아래)
  • 대체 표시 없이 :focus-visible 외곽선을 지우지 않는다
  • 다이얼로그는 포커스를 안에 가두고, 닫힐 때 다이얼로그를 연 요소로 포커스를 돌려준다
  • 라우트가 바뀌면 포커스를 <body>가 아니라 새 페이지의 <h1>으로 옮긴다. <body>로 보내면 스크린 리더 사용자가 화면 맨 위 크롬에 덩그러니 남는다
  • 포커스된 요소가 고정 액션 바에 가려져서는 안 된다 (SC 2.4.11)

링은 --ds-focus를 기준으로 color-mix()로 만든다. 그래서 테마가 포커스 색을 바꾸면 링 색도 알맞게 따라 바뀐다. rgba 값을 하드코딩했다면 다크 모드용 토큰이 하나 더 필요했을 것이고, 두 값은 결국 어긋났을 것이다.

파괴적이고 되돌릴 수 없는 액션

송금 확인은 페이지 아무 곳에서나 Enter를 누른다고 실행되지 않는다. 주요 액션 버튼에 포커스가 있어야만 실행된다. 제품에서 유일하게 되돌릴 수 없는 액션이라 일부러 마찰을 둔 것이다.

  • Visible focus indicator on all interactive elements
  • Focus ring meets 3:1 contrast against adjacent colors
  • Custom focus styles consistent across the system
  • Never remove outline without providing an alternative
Do— Visible focus ring

3:1 contrast, 3px ring, visible on the focused element

Don't— No focus indicator

Keyboard users cannot see where they are

  • All interactive elements reachable via Tab
  • Logical focus order matching visual order
  • Escape key closes modals and overlays
  • Arrow keys navigate within composite widgets
  • Enter/Space activates buttons and controls

Tab index

Use tabIndex to control keyboard focus order.

ValueBehaviorWhen to use
tabIndex="0"Follows natural DOM orderCustom interactive elements (div with onClick)
tabIndex="-1"Focusable via JS, skipped by TabOff-screen elements, programmatic focus (modals, error summaries)
tabIndex="1+"Forces priority orderAvoid — breaks natural flow, confuses users
1
Devon
Tab ↓
2
Kim
Tab ↓
3
88 Queens Quay W
Tab ↓
4
Tab NextShift+Tab PreviousEnter ActivateEsc Close

시맨틱 HTML

  • 가능하면 네이티브 HTML 요소를 쓴다 (<button>, <input>, <nav>, <main>)
  • 상호작용 요소를 <div>나 <span>으로 만들지 않는다
  • 제목은 위계 순서대로 쓴다 (h1 → h2 → h3, 단계를 건너뛰지 않는다)
  • 랜드마크 요소를 쓴다: <header>, <nav>, <main>, <footer>, <aside>
  • 목록은 스타일을 입힌 div 대신 <ul>/<ol>로 만든다
  • <h1>은 페이지마다 하나만 두고, 제품 이름이 아니라 페이지 이름을 쓴다

네이티브 <button>을 쓰면 키보드 실행, 포커스 관리, 비활성 상태, 폼 연동, 올바른 스크린 리더 역할이 기본으로 따라온다. <div role="button">으로 만들면 이걸 전부 직접 구현해야 하고, 실제로는 꼭 하나씩 빠뜨린다.

ARIA 패턴

ARIA는 네이티브 HTML 시맨틱으로 부족할 때만 쓴다.

PatternWhen to use
aria-label아이콘만 있는 버튼, 라벨이 없는 컨트롤
aria-describedby에러 메시지, 힌트 텍스트, 보충 정보
aria-live동적 콘텐츠 갱신 (토스트, 로딩 상태)
aria-current="page"활성 내비게이션 항목, 브레드크럼의 현재 페이지
aria-expanded아코디언 트리거, 드롭다운 토글
aria-invalid="true"검증 에러가 있는 폼 입력
role="alert"에러 요약, 중요한 알림

알림 우선순위

LevelUsed forExample
aria-live="polite"읽던 내용이 끝난 뒤에 알려도 되는 상태"Rate updated", "3 results"
aria-live="assertive" / role="alert"사용자가 지금 바로 대응해야 하는 일송금 실패, 세션 곧 만료

Assertive는 스크린 리더가 읽고 있던 내용을 끊고 들어온다. 환율 갱신에 쓰면 30초마다 사용자가 듣던 문장을 중간에 자르게 된다. Senco에서 assertive는 실패와 보안 이벤트에만 쓴다.

disabled가 아니라 aria-disabled

disabled 버튼은 탭 순서에서 빠진다. 그래서 키보드로 폼 끝까지 Tab을 눌러 내려간 사용자는 버튼을 찾지 못하고, 이유도 알 수 없다. Senco는 aria-disabled="true"를 쓰고 클릭 핸들러만 막는다. 컨트롤은 계속 포커스를 받고, aria-describedby로 무엇이 빠졌는지 알려 줄 수 있다.

첫 번째 규칙

잘못된 ARIA보다 ARIA가 없는 편이 낫다. role을 잘못 붙이면 올바른 네이티브 시맨틱을 덮어써서, 요소가 실제와 다른 것으로 읽힌다. 컴포넌트에 넣은 ARIA 속성은 하나하나 한 문장으로 이유를 댈 수 있어야 한다.

  • Use semantic HTML first, ARIA second
  • Apply ARIA roles and landmarks to define page regions
  • Use aria-label, aria-describedby, and aria-live where needed
  • Never use ARIA to override semantic meaning
<nav>
<header>
<main>
role="status"

Landmarks let assistive technology users jump between page regions.

스크린 리더 테스트

모든 컴포넌트는 스크린 리더 두 가지 이상으로 테스트한다.

PlatformScreen readerBrowser
macOSVoiceOverSafari
WindowsNVDAFirefox or Chrome
MobileTalkBack (Android)Chrome
MobileVoiceOver (iOS)Safari

Chrome의 TalkBack은 표 맨 아래에 있지만 우선순위는 가장 높다. Senco 사용자 대부분이 쓰는 기기 환경이기 때문이다.

확인할 것

  • 모든 상호작용 요소에 접근 가능한 이름이 있다
  • 폼 에러가 나타나면 바로 읽어 준다
  • 페이지를 이동하면 페이지 제목이 바뀐다
  • 동적 콘텐츠가 바뀌면 aria-live로 알린다
  • 이미지에 의미 있는 대체 텍스트가 있다 (장식용이면 aria-hidden)
  • 금액을 자연스럽게 읽는다. "2 5 0 U S D"가 아니라 "250 dollars"
  • 대기 중인 송금의 상태가 바뀌면, 사용자가 다시 찾아 들어가지 않아도 알려 준다

자동 검사는 최소한일 뿐이다

자동화 도구가 잡는 WCAG 이슈는 대략 3분의 1이고, 여기서 가장 중요한 이슈는 하나도 잡지 못한다. 포커스 순서가 자연스러운지, 에러 메시지가 해결 방법을 알려 주는지, 상태 안내가 필요한 순간에 나오는지 같은 것들이다. 자동 검사는 풀 리퀘스트를, 수동 테스트는 릴리스를 통과시키는 조건이다.

  • Meaningful alt text for images
  • Hidden descriptive text for icon-only actions
  • Live regions for dynamic content updates
  • Form labels and error messages announced correctly
Do— Descriptive labels
Visa ****7788
Expires 11/27
Edit
RBC ****4400
Chequing
Edit
“Visa ending 7788, selected, Expires 11/27”
“Edit, Visa ending 7788, button”
Don't— Missing labels
Visa ****7788
Expires 11/27
Edit
RBC ****4400
Chequing
Edit
“radio button”
“button”

Alt text for images

Do— Describe method + state
USDT ****9c1d
Tether
Edit
BTC ****k8xf
Bitcoin
Edit
alt="USDT wallet ending 9c1d, selected"
Don't— Generic label
USDT ****9c1d
Tether
Edit
BTC ****k8xf
Bitcoin
Edit
alt="icon"
alt="wallet"
Do— Flag + recipient context
South Korea flag
Min-ji Kim
Mar 28 · Sent
-$250.00 CAD
Completed
United Kingdom flag
James Wilson
Mar 25 · Received
+$80.00 CAD
Pending
alt="South Korean flag — Min-ji Kim, sent $250 CAD"
Don't— No flag context
Min-ji Kim
Mar 28 · Sent
-$250.00 CAD
Completed
James Wilson
Mar 25 · Received
+$80.00 CAD
Pending
alt="flag"
alt=""

본문으로 건너뛰기

건너뛰기 링크는 숨어 있다가 키보드 포커스를 받으면 나타나는 앵커다. 반복되는 내비게이션을 건너뛰고 본문으로 바로 이동하게 해 준다.

  • 건너뛰기 링크는 페이지에서 가장 먼저 포커스를 받는 요소여야 한다
  • 평소에는 화면에서 숨기고, :focus일 때 보여 준다
  • 대상은 id가 있는 랜드마크로 한다 (예: #ds-main-content)
  • 고정 내비게이션이 있는 페이지에는 모두 건너뛰기 링크를 둔다
  • 대상은 포커스를 받을 수 있어야 한다(tabindex="-1"). 그렇지 않으면 포커스가 앵커에 남고, 다음 Tab에서 다시 내비게이션으로 돌아간다
Skip to content
Default — hidden
On Tab — visible
Skip to main content
Do— First focusable element on the page
<body>
  <a href="#main" class="ds-skip-link">
    Skip to main content
  </a>
  <nav>...</nav>
  <main id="main">...</main>
Don't— Skip link buried after navigation
<body>
  <nav>...</nav>
  <a href="#main">
    Skip to main content
  </a>
  <main id="main">...</main>

모션과 애니메이션

  • prefers-reduced-motion 설정을 따른다
  • 동작 줄이기가 켜져 있으면 꼭 필요하지 않은 애니메이션은 끈다
  • 스피너는 회전만 멈추고 화면에는 남는다. 로딩 중이라는 사실은 텍스트 라벨이 알려 준다
  • 1초에 세 번 넘게 깜빡이는 요소는 없다 (SC 2.3.1)
  • 상태 변화를 모션으로만 전달하지 않는다

Platform 계층에서 전역으로 처리한다. Motion 참고.

터치 타깃

  • 최소 터치 타깃: 48×48 CSS 픽셀 (WCAG 2.5.5 AAA와 2.5.8 AA보다 크다)
  • 타깃 사이 최소 간격: 8px
  • 눈에 보이는 요소는 더 작아도 되지만, 탭 영역은 48×48px이어야 한다

체크리스트

새 컴포넌트나 패턴을 만들 때마다 이 체크리스트를 확인한다. 체크하지 않은 항목이나 기록하지 않은 예외가 있으면 출시하지 않는다.

  • 색 대비가 두 테마 모두에서 텍스트 4.5:1, UI 3:1을 통과한다
  • 키보드로 조작할 수 있다 (Tab, Enter, Escape, 화살표 키). 복합 컴포넌트는 Tab 정지점이 하나다
  • 포커스 표시(3px, 20%)가 보이고, 대비 기준을 충족하며, :focus-visible을 쓴다
  • 포커스된 요소가 고정 크롬에 가려지지 않는다
  • 스크린 리더가 이름, 역할, 상태를 읽어 준다
  • 에러 상태를 색만이 아니라 텍스트와 아이콘으로도 보여 준다
  • 에러 메시지가 문제가 있다는 사실만이 아니라 해결 방법을 알려 준다
  • 터치 타깃이 최소 48×48px이고, 타깃 사이 간격이 8px이다
  • prefers-reduced-motion을 따르고, 모션으로만 전달하는 상태가 없다
  • 시맨틱 HTML을 썼다 (div로 만든 버튼 없음)
  • ARIA 속성이 정확하고 꼭 필요한 만큼만 있다. disabled 대신 aria-disabled를 쓴다
  • VoiceOver와 NVDA로 테스트했다. 송금 경로에 있는 화면은 TalkBack으로도 테스트했다
  • 320px 너비와 200% 확대에서 가로 스크롤 없이 텍스트가 줄바꿈된다
  • 브라우저 글자 크기 200%에서도 잘리지 않고 동작한다

책임

RoleOwns
디자인대비, 타깃 크기, 레이아웃상의 포커스 순서, 해결 방법을 알려 주는 에러 카피
엔지니어링시맨틱 마크업, 키보드 동작, ARIA, 라이브 영역, 포커스 관리
QA스크린 리더 테스트, 확대와 리플로우, 두 테마, 위 체크리스트

QA에서 돌아온 접근성 실패는 한데 묶어 등록하지 않고 원인별로 나눈다. 대비 실패는 어떤 기준인지 적어 디자인으로, 빠진 라이브 영역은 엔지니어링으로 보낸다.

알려진 공백

ItemStatus
forced-colors / Windows 고대비구현도 테스트도 안 됨
기본 상태 입력 테두리근거를 남기고 허용한 예외
CI의 자동 접근성 검사설정 안 됨
컴포넌트별 접근성 상태 표공개 안 됨. Carbon처럼 variant별로 보고하는 작업이 남아 있다
인지 접근성 리뷰다국어를 쓰고 시간에 쫓기는 사용자층인데도 정식으로 진행한 적 없음