📚 목차
[React] 접근성(A11y) 개선 - 모두가 읽을 수 있는 서비스 만들기
프론트엔드 개발을 하다 보면, 기능이 정상 동작하고 UI가 보기 좋게 렌더링되면 충분하다고 느끼기 쉽다.
하지만 어느 순간 이런 질문이 남았다.
- 이 UI는 스크린리더가 실제로 이해할 수 있을까?
- 키보드만으로도 사용자 흐름이 자연스럽게 이어질까?
- 크롤러는 이 페이지를 하나의 “문서”로 해석할 수 있을까?
보이는 것과 해석되는 것은 다르다.
화면에 잘 그려졌다고 해서, 브라우저·보조기기·검색 엔진이 같은 구조로 이해하는 것은 아니다.
그래서 시맨틱 태그와 ARIA로 UI 구조를 명확히 하고 스크린 리더(VoiceOver)로 검증해 개발하고 있는 서비스에서 접근성(a11y)를 개선했다.
이번 블로그에서는 접근성의 개념과 시맨틱 태그의 중요성, ARIA의 역할, 모달 포커스 트래핑 구현, 그리고 VoiceOver로 검증한 결과까지 자세히 정리해보려고 한다.
접근성이란?
접근성의 정의에 대해서는 Toss의 Frontend Fundamentals에서 다음과 같이 설명한다.
접근성(Accessibility, 줄여서 A11y)은 모든 사용자가 더 쉽고 편리하게 웹을 사용할 수 있도록 돕는 기본 원칙이다.
프론트엔드 개발자는 HTML 구조를 만들고 인터랙션을 정의한다. 사용자와 가장 가까이에서 웹 경험을 설계하는 만큼, 접근성의 출발점이자 핵심 역할을 맡고 있다. 그래서 프론트엔드 개발자가 접근성을 신경 쓰면 누구나 이용할 수 있는 웹을 만들 수 있다.
접근성을 지키면 장애인, 비장애인, 개발자 모두에게 효용이 있다.
장애인 사용자는 스크린 리더 같은 보조기기를 통해 웹사이트를 원활하게 이용할 수 있고, 일반 사용자는 더 빠르고 편리한 웹 경험을 할 수 있다.
또한 개발자는 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있다.
해당 글에서 접근성이 뭔지, 왜 중요한지, 어떻게 개선할 수 있는지에 대한 개념과 원칙을 자세히 설명하고 있다.
추가적으로 실제로 스크린리더로 테스트를 직접 해볼 수 있으니 한번 쯤은 읽어보는 것을 추천한다.
시맨틱 태그가 왜 중요한가?
1. 시각 장애인(스크린 리더 사용자) 관점
스크린 리더는 화면을 “보지” 않는다.
대신 HTML 구조를 읽는다.
<button>→ “버튼”<a>→ “링크”<nav>→ “내비게이션 영역”<main>→ “메인 콘텐츠”
반대로 <div>는 아무 의미도 없다.
예를 들어,
<div onClick={openModal}>열기</div>이 코드는 스크린 리더에서 "클릭 가능한 요소"로 인식되지 않는다.
결과적으로 사용자는 해당 기능을 탐색할 수 없다.
시맨틱 태그를 사용하면:
- 버튼 목록 탐색 가능
- 헤딩(h1~h6) 점프 가능
- 랜드마크(nav/main) 이동 가능
페이지가 “박스들의 나열”이 아니라 탐색 가능한 문서가 된다.
2. 일반 사용자 관점 (키보드·브라우저 기본 동작)
시맨틱 태그는 접근성뿐 아니라 브라우저의 기본 동작을 복원한다.
예를 들어,
<button>→ Enter/Space 자동 지원<a>→ 새 창 열기, 링크 복사 가능<form>+<button type="submit">→ Enter 제출 가능
하지만, 반대로 div에 onClick만 달면
반대로 div에 onClick만 달면:
- Enter 동작 없음
- 키보드 접근 불가
- 브라우저 기본 UX 상실
즉, 시맨틱 태그는 "장애인 전용 기능"이 아니라 모든 사용자의 기본 UX를 보장하는 장치다.
3. 개발자 관점 (테스트 코드·유지보수)
시맨틱 태그는 테스트 안정성을 높인다.
예를 들어,
screen.getByRole('button', { name: '로그인' });이 코드는 다음이 충족될 때만 가능하다.
- 실제
<button> - 또는 올바른 role
- 적절한 label 연결
div 기반 구조에서는,
- class 선택
- data-testid 의존
- 구조 변경 시 테스트 깨짐
그리고 시맨틱 기반 구조에서는,
- 역할 기반 선택 가능
- 리팩토링에 강함
- 테스트가 사용자 관점과 일치
즉, 시맨틱 태그는 접근성 개선이 아니라, 구조 품질과 테스트 품질을 동시에 올리는 설계 기준이다.
4. 크롤러/SEO 관점 — “해석 가능한 문서”인가?
검색 엔진도 결국 HTML을 읽는다.
스크린 리더와 마찬가지로, 구조를 기반으로 의미를 해석한다.
시맨틱 구조가 없는 경우에는,
<div>가 반복되어 콘텐츠 경계가 불명확함- 제목 계층이 깨져 페이지 주제가 흐려짐
- 내비게이션/본문 구분이 명확하지 않음
- 장식 요소와 실제 콘텐츠가 혼재됨
이 상태에서는 크롤러가 페이지의 중심 콘텐츠를 파악하는 데 추가 비용이 든다.
반대로 시맨틱 태그를 적용하면,
<main>으로 핵심 콘텐츠가 명확해지고<nav>로 탐색 영역이 분리되며<h1>~<h6>계층으로 문서 구조가 정리된다

이는 검색 순위를 “트릭”으로 올리는 방법이 아니라,
문서를 기계가 안정적으로 해석할 수 있는 구조로 만드는 작업이다.
결과적으로, 콘텐츠 중심 신호가 명확해지고 템플릿 구조가 일관되며 Lighthouse SEO 점수도 함께 개선된다
스크린 리더 이해하기
먼저 스크린 리더의 기본적인 역할과 구성을 이해해 볼게요. 스크린 리더의 3요소는 역할, 레이블, 상태값이다.
스크린 리더는 화면에 있는 요소를 다음 순서로 읽는다.
역할(Role): 요소가 어떤 종류인지 나타낸다. (예: 버튼, 입력창, 스위치 등)레이블(Label): 컴포넌트의 이름이다. 어떤 기능인지 설명해요.상태(State): 현재 상태를 알려준다. (예: 활성화됨, 꺼짐, 선택됨 등)
예시를 들어보면,
<div role='radio' tabindex='0' aria-checked='true' aria-label='마케팅 알림'></div>스크린 리더는 순서대로 이렇게 읽는다.
-
- 마케팅 알림 (레이블)
-
- 체크상자 (역할)
-
- 선택됨 (상태)
-
- 설정을 끄거나 켜려면 이중탭 하십시오. (스크린 리더에서 자동 설명해주는 부분)
Semantic First — 태그로 역할을 복원하다
기존에는 div 태그로 무의미한 코드가 몇개 보였다.
<div onClick={openModal}>열기</div>시각적으로는 버튼이었지만, 키보드로 접근이 불가능했고, Space/Enter 동작도 없었다. 그리고 가장 중요한 스크린리더에서 버튼으로 인식되지 않는다는 문제점이 있었다.
이를 다음 기준으로 교체했다.
- 상태 변경 →
<button type="button"> - 페이지 이동 →
<a href="..."> - 입력 제출 →
<form>+<button type="submit">
이를 코드로 적용하면 아래와 같다.
<button type='button' onClick={openModal}>
열기
</button>이 변경만으로, 아래와 같은 효과가 있었다.
- 기본 키보드 동작 자동 지원
- role="button" 불필요
- 테스트에서 getByRole('button') 사용 가능
ARIA를 붙이기 전에, 기본 시맨틱 태그 자체로 의미를 전달하는 것이 가장 중요하다.
ARIA는 “의미를 보완하는 장치”다
Semantic만으로 해결되지 않는 영역이 있다.
특히 “이름”과 “상태”는 명시적으로 연결해줘야 한다.
컴포넌트가 꺼졌는지 켜졌는지, 펼쳐졌는지 접혔는지 같은 동작 변화를 스크린 리더에 알려주려면 상태 속성을 설정해야 한다.
HTML 기본 요소(<button>, <input> 등)는 일부 상태(checked, disabled)를 내장하고 있지만,
<div>처럼 커스텀 요소를 쓸 때는 aria- 속성으로 상태를 명시적으로 알려야 한다.
주의해야 할 것은 aria-label을 사용하면 기존에 화면에 보이는 텍스트가 스크린 리더에 노출되지 않게 된다.
가능하면 시각적 텍스트를 포함한 문장으로 aria-label을 작성해 주자.
aria- 상태 속성은 여러 가지가 있지만, 가장 자주 쓰이는 것은 다음과 같다.
1. aria-label — “이름 없는 버튼”을 구체화
아이콘 버튼은 시각적으로는 명확하지만,
스크린리더에게는 아무 의미도 없을 수 있다.
예를 들어, 아래와 같은 코드가 있다.
<button onClick={deleteItem}>
<TrashIcon />
</button>VoiceOver에서는
버튼
라고만 읽히기 때문에, 사용자는 이 버튼이 어떤 역할을 하는지 알 수 없다.
따라서, aria-label을 사용하여 다음과 같이 개선할 수 있다.
<button onClick={deleteItem} aria-label='게시글 삭제'>
<TrashIcon aria-hidden='true' />
</button>이제 VoiceOver는
게시글 삭제, 버튼
라고 읽어서, 사용자가 버튼의 역할을 명확히 이해할 수 있다.
인터랙션 요소에 텍스트가 없으면 aria-label를 사용하여 이름을 명시적으로 연결해주는 것이 중요하다.
추가적으로 장식 아이콘와 같이 의미없는 아이콘은 aria-hidden="true" 로 스크린리더에서 숨겨 사용자가 혼란스럽지 않도록 하는 것이 좋다.
즉, aria-label은 단순 속성이 아니라 상호작용의 의미를 완성하는 이름 지정 장치다.
2. aria-describedby - 폼 상태 연결
aria-labelledby 는 스크린 리더가 가장 먼저 읽는 속성이다.
화면에 이미 텍스트가 있고, 그것을 입력 필드의 레이블로 사용하고 싶을 때는 aria-labelledby를 사용할 수 있다.
<h2 id="address-heading">배송 주소</h2>
<input
type="text"
aria-labelledby="address-heading"
placeholder="예: 서울시 강남구"
/>이렇게 하면 스크린 리더가 입력 필드에 포커스가 갔을 때 “배송 주소”라고 읽어서, 사용자가 이 입력 필드가 무엇을 위한 것인지 명확히 알 수 있다.
3. aria-live - 중요 정보 업데이트 알림
aria-live는 현재 사용자에게 바로 전달해야하는 중요 정보가 포함된 컨텐츠 업데이트를 나타내는 상태 속성이다. 총 3가지 옵션이 있다.
polite: 현재 읽던 내용을 마친 후 업데이트된 내용을 읽는다. (기본값)assertive: 즉시 읽기 중단 후 바로 새로운 내용을 읽는다. (긴급한 정보)off: 컨텐츠 업데이트를 알리지 않는다.
// 입력 중 에러 메세지를 알림
<input type="email" value="1234" aria-describedby="error-message" />
<p id="error-message" aria-live="polite">이메일 형식이 올바르지 않습니다.</p>
// 오류 메세지를 알림
<p aria-live="assertive">인터넷 연결이 끊어졌습니다.</p>여기서 팁은
role="alert"을 사용하면 aria-live="assertive"와 똑같이 동작한다.
또한, role="status"를 사용하면 aria-live="polite"와 똑같이 동작한다.
모달 — 포커스 트래핑으로 탐색 맥락 유지
접근성에서 가장 크게 깨졌던 부분은 모달이었다.
모달이 열렸는데 Tab을 누르면 배경으로 이동한다.
이는 키보드 사용자에게 맥락 붕괴였다.
그래서 useModalFocus를 구현하여 모달이 열린 동안 Tab/Shift+Tab이 모달 내부에서 순환하도록 만들었다.
핵심 코드는 아래와 같다.
const FOCUSABLE_ELEMENTS =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';모달 내부에서 Tab으로 이동 가능한 요소(button, 링크, 입력창 등)를 한 번에 수집하기 위한 셀렉터다.
포커스 트래핑의 기준이 되는 "순환 대상 목록"을 정의한다.
1. 모달 오픈 시 첫 포커스 지정
(focusableElements[0] as HTMLElement).focus();모달이 열리면 첫 번째 포커스 가능한 요소에 즉시 focus를 준다.
사용자의 탐색 시작점을 모달 내부로 강제해 배경으로 포커스가 남지 않도록 한다.
2. Tab / Shift+Tab 순환 제어
if (event.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
event.preventDefault();
}현재 포커스가 첫 요소일 때 Shift+Tab을 누르면 마지막 요소로 이동시킨다.
모달 밖으로 포커스가 빠져나가는 것을 막고 내부에서만 순환하도록 제어한다.
결과적으로, 포커스 이탈 차단과 함께 모달 내부 탐색을 유지하고, 배경 낭독 방지하여 사용자 경험이 크게 개선되었다.
모달은 이제 단순한 overlay가 아니라 독립된 탐색 컨텍스트가 될 수 있었다.
스크린린더 검증
VoiceOver는 macOS/iOS에 내장된 스크린 리더다.
해당 스크린리더를 통하여 시맨틱 태그와 ARIA가 제대로 동작하는지 검증할 수 있다.
내가 실제로 VoiceOver(맥)에서 확인한 체크리스트는 아래다.
키보드(Tab) 관점
- Tab 순서가 시각적 흐름과 자연스러운가?
- 포커스가 빠져나가거나(배경/주소창) 끊기지 않는가?
- 모달 오픈 시 첫 포커스가 적절한 위치로 이동하는가?
- 모달 내에서 Tab/Shift+Tab이 정상 순환하는가?
- Escape로 닫히는가?
스크린 리더(VoiceOver) 관점
- 로그인 에러가 발생했을 때 즉시 읽히는가?
- 버튼/링크가 역할에 맞게 읽히는가?
- 아이콘/장식 요소가 불필요하게 읽히지 않는가?
- 모달이 “dialog”로 인지되고 제목이 함께 읽히는가?
- 모달이 열린 동안 배경을 읽으려 하지 않는가?
아래 영상들을 보면서 실제로 VoiceOver로 검증한 결과를 확인해 볼 수 있다.
로그인 페이지
개선 전
개선 후
글 작성 도메인 페이지
개선 전
개선 후
마무리

접근성 개선은 단순히 “장애인도 사용할 수 있게 하는” 작업이 아니다.
브라우저·스크린리더·크롤러가 동일하게 이해할 수 있는 구조로 맞추는 기본 품질 설계 작업이다.
시맨틱 태그와 ARIA로 역할·이름·상태를 명확히 하면,
보조기기에서는 탐색이 자연스러워지고 키보드 사용자는 기본 동작을 보장받는다.
동시에 문서 구조가 정리되면서 크롤러가 콘텐츠 중심을 명확히 파악할 수 있어 SEO 측면에서도 해석 비용이 줄어든다.
결국 접근성은 점수를 올리기 위한 체크리스트가 아니라,
사용자·테스트 코드·검색 엔진까지 같은 구조를 공유하게 만드는 설계 기준이었다.
앞으로도 UI를 구현할 때 “보이는가?”가 아니라
“해석되는가?”를 기준으로 구조를 설계해 나가고자 한다.