접근 가능한 React
모든 React 앱은 브라우저가 항상 제공해온 것과 같은 HTML로 렌더링되며, 모든 보조 기술은 컴포넌트가 생성한 DOM에서 작동합니다. React의 접근성은 대부분 그 DOM에 관한 작은 선택들의 연속입니다: 렌더링하는 요소가 무엇인지, 어떻게 이름을 얻는지, 그리고 화면이 보이지 않는 사람 입장에서 변할 때 어떻게 처리할 것인지입니다.
JSX의 세부사항 하나를 먼저 짚고 가겠습니다. React는 class를 className으로, for를 htmlFor로 이름을 바꾸지만, ARIA 속성은 하이픈을 유지합니다: aria-live, aria-label, 그리고 일반 role입니다.
의미론적 요소부터 시작합니다
<button>은 이미 많은 동작이 내장된 상태로 제공됩니다. 탭 순서에 포함되므로 키보드로 도달할 수 있습니다. Enter와 Space에서 클릭 핸들러를 실행합니다. 스크린 리더는 이를 버튼으로 표시하고 텍스트를 이름으로 읽어주며, 이는 음성 제어 소프트웨어가 대상으로 삼는 이름이기도 합니다. 브라우저가 비활성화 상태, 포커스 링, 활성화 스타일을 처리합니다.
// 브라우저가 포커스, 키보드 활성화, "버튼" 알림을 제공합니다
<button className="die" onClick={hold}>{value}</button>onClick 핸들러를 가진 <div>는 해당 목록의 한 가지 항목만 얻습니다: 클릭입니다. Tab이 이를 건너뛰고, Enter와 Space는 작동하지 않으며, 스크린 리더는 이를 상호작용 시 무엇이 일어날 가능성이 없음을 시사하는 텍스트로 읽습니다.
일반적인 해결책은 role="button"과 tabIndex={0}인데, 이는 요소를 탭 순서에 넣고 표시되는 것을 변경합니다. 하지만 동작은 여전히 부족합니다. onKeyDown 핸들러를 추가하고, Enter와 Space를 확인하고, Space에서 preventDefault()를 호출해 페이지 스크롤을 멈추고, 수동으로 작성한 비활성화 상태를 스타일링과 동기화해야 합니다. 브라우저가 이미 제공하는 것을 다시 만들기 위해 꽤 많은 코드를 작성하게 됩니다. 실제 <button>에 도달하는 것이 더 짧은 방법이며, 브라우저가 변경되어도 계속 올바르게 작동합니다.
같은 논리가 나머지 마크업 전체에 적용됩니다: 내비게이션을 위한 <a href>, 스크린 리더가 사이를 이동할 수 있는 landmark인 <nav>와 <main>, 사람들이 구조를 따라 내비게이션하는 순서대로 정렬된 제목들입니다. React 코드베이스의 대부분의 접근성 작업은 이미 그 역할을 하는 요소를 선택하는 것입니다.
변화를 알립니다
단일 페이지 앱은 제자리에서 업데이트됩니다. 스크린 리더에 무언가 일어났음을 알릴 페이지 로드가 없으므로, 화면 중간에 렌더링된 변화는 완전히 무음일 수 있습니다. 라이브 영역이 그 정보를 제공합니다: 스크린 리더가 감시하고 콘텐츠가 변할 때마다 표시하는 컨테이너입니다. 아래의 sr-only 클래스는 이를 시각적으로 숨기며, 이 장의 뒷부분에서 다루는 CSS 패턴을 사용합니다.
<div aria-live="polite" className="sr-only">
{isGameWon && <p>승리했습니다! 새 게임을 누르면 다시 시작합니다.</p>}
</div>래퍼는 매번 렌더링되며, 처음에는 비어 있고, React가 isGameWon이 바뀔 때 단락을 여기에 넣습니다. 이 순서가 사람들이 잘못 이해하는 부분입니다. aria-live를 가진 요소는 콘텐츠가 도착하기 전에 DOM에 있어야 합니다. 왜냐하면 스크린 리더는 라이브 영역을 만날 때 등록한 후 변경을 감시하기 때문입니다. 영역과 그 텍스트를 한 번의 렌더에서 함께 마운트하면 많은 스크린 리더가 아무것도 표시하지 않습니다: 전체가 일반적인 새 콘텐츠처럼 보입니다. 트리에 빈 영역을 유지하는 것은 비용이 들지 않으며 알림을 신뢰할 수 있게 만듭니다.
aria-live="polite"는 알림을 큐에 넣습니다. 스크린 리더가 현재 읽고 있는 것을 마치고, 다음 자연스러운 일시 중지에서 메시지를 전달합니다. 이는 시각적 변화 후 약간 나중에 나타날 수 있습니다. 이 지연은 의도적이며, 폴라이트가 거의 모든 것에 적합한 설정입니다.
키보드 상호작용
Tab은 포커스 가능한 요소를 통해 앞으로 이동하고, Shift+Tab은 뒤로 이동하고, Enter는 링크와 버튼을 활성화하고, Space는 버튼을 활성화하고 체크박스를 토글합니다.
탭 순서는 DOM 순서를 따르므로 JSX가 렌더링하는 순서가 사람들이 이동하는 순서입니다. CSS로 시각적으로 순서를 바꾸면 탭 순서가 화면 주위를 뛰어다니게 되고, 긍정적인 tabIndex 값은 의도적으로 같은 혼란을 유발합니다. tabIndex={-1}이 유용한 것입니다: JavaScript에서 요소를 포커스 가능하게 만들면서 탭 순서에서 벗어나게 하며, 이것이 대화상자 제목과 같은 포커스 대상이 필요한 것입니다.
두 가지 규칙이 더 있습니다. 포커스를 보이게 유지합니다: outline: none을 피하십시오. 그것을 대체하는 :focus-visible 스타일이 있지 않다면 말입니다. 그리고 탈출 경로를 유지합니다: 의도적으로 포커스를 자신 안에 보유하는 모달은 닫기를 위해 Escape가 필요하고 포커스를 트리거로 반환해야 합니다.
포커스를 의도적으로 이동합니다
UI가 형태를 변경할 때, 포커스는 어디에도 없을 수 있습니다. 누군가 버튼을 활성화하면, 버튼이 제거되거나 교체되고, 포커스가 <body>로 떨어집니다. 다음 Tab은 페이지 상단에서 시작되고, 리더가 자신의 위치를 잃었습니다.
해결책은 포커스를 합리적인 곳으로 이동하는 것이며, 이는 ref의 정당한 용도 중 하나입니다:
function NewGameButton({ isGameWon, onNewGame }) {
const buttonRef = useRef(null)
useEffect(() => {
if (isGameWon) {
buttonRef.current.focus()
}
}, [isGameWon])
return <button ref={buttonRef} onClick={onNewGame}>새 게임</button>
}effect는 React가 해당 노드를 화면에 커밋한 후에 실행되므로, 요소가 포커스를 받을 준비가 되어 있습니다. isGameWon을 확인하면 모든 렌더에서 포커스를 훔치는 것을 방지합니다.
같은 패턴은 다른 일반적인 순간들을 다룹니다: 대화상자는 열 때 포커스를 받고 닫을 때 트리거에 반환하고, 유효성 검사 실패는 첫 번째 유효하지 않은 필드로 포커스를 보내고, 행 삭제는 포커스를 그것을 대체한 행으로 이동합니다. 아래의 규칙은 한 줄입니다: 코드가 포커스를 가진 것을 제거했다면, 코드가 다음 포커스가 가는 곳을 결정합니다.
시각적으로 숨겨진 텍스트
많은 상태가 레이아웃에서 명확하고 스크린 리더에서는 무음입니다: 필드 옆의 녹색 체크 표시, 누른 것처럼 보이는 주사위, 있는 위치에서 명확하게 읽히는 숫자입니다. 시각적으로 숨겨진 텍스트는 페이지를 듣고 있는 누구에게나 그것을 설명합니다.
관례는 sr-only라는 클래스입니다. React나 브라우저에 의미가 없습니다: 일반 클래스 이름이며, 이 CSS 규칙들이 작업을 수행합니다.
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}요소는 접근성 트리에 남아 있으면서 시각적 공간을 차지하지 않습니다. display: none과 visibility: hidden은 그것을 트리에서도 제거하여 모두로부터 숨기게 됩니다.
아이콘 전용 버튼이 일반적인 경우입니다. aria-label을 제공하거나 실제 텍스트를 내부에 넣고 시각적으로 숨깁니다:
<button onClick={onClose}>
<XIcon aria-hidden="true" />
<span className="sr-only">닫기</span>
</button>aria-hidden="true"는 장식용 SVG를 알림에서 유지하고, 숨겨진 스팬이 이름을 제공합니다. aria-label에 대한 한 가지 주의: 이는 대화형 요소와 명시적 역할을 가진 모든 것에 접근 가능한 이름을 설정하며, 브라우저는 역할 없는 일반 <div> 또는 <span>에서 이를 자주 무시합니다. 버튼, 링크, 입력, 그리고 레이블이 있는 landmark에 유지합니다.
소스를 읽는 것은 이 중 어느 것이든 거기까지만 갈 것입니다. Cmd+F5로 VoiceOver를 켜서 자신의 앱을 듣고, 브라우저에서 axe DevTools를 실행하여 누락된 레이블과 명명되지 않은 컨트롤을 자동으로 잡습니다.
button에 도달하고, 모든 입력 다음에 실제 label을 사용하십시오. 이 요소들은 키보드 지원과 스크린 리더가 읽을 수 있는 이름을 무료로 제공합니다. 페이지를 듣고 있는 사람이 그렇지 않으면 놓칠 수 있는 화면의 무언가가 변경되면, 짧은 문장을 aria-live="polite"가 있는 <div> 안에 넣고, 변경이 감지되도록 페이지에서 시작하여 그 div를 유지합니다. 다음: 기본을 넘어, 기초를 넘어서 무엇이 있는지의 지도입니다.

