
웹 접근성(A11y): 모두를 위한 웹
시각 장애인, 마우스가 고장 난 사용자, 그리고 미래의 나를 위한 배려. `alt` 태그 하나가 만드는 큰 차이.

시각 장애인, 마우스가 고장 난 사용자, 그리고 미래의 나를 위한 배려. `alt` 태그 하나가 만드는 큰 차이.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

어느 날 마우스 배터리가 나가서, 키보드만으로 내가 만든 사이트를 써보려 했다. Tab 키를 누르는데 포커스가 어디로 갔는지 하나도 안 보였다. 세 번째 클릭부터는 대충 감으로 눌렀고, "로그인" 버튼에 도달하는 데 1분이 걸렸다. 그 순간 깨달았다. "나는 내 사이트를 쓸 수 없는 사람"이었다.
웹 접근성(Web Accessibility, 줄여서 A11y)은 먼 나라 이야기가 아니었다. Accessibility를 A와 y 사이에 11글자가 있다고 해서 A11y라 부른다는 걸 그제야 배웠다. 그리고 이건 장애인만을 위한 게 아니었다. 팔이 부러진 사람, 지하철에서 한 손으로 폰 보는 사람, 노안이 온 부모님, 그리고 무엇보다 미래의 나를 위한 것이었다.
이 글은 "웹 접근성 가이드"가 아니다. 내가 삽질하며 배운 것들, 코드로 직접 부딪히며 이해한 것들을 정리해본 노트다.
처음 사이트 만들 때, 나는 이렇게 버튼을 만들었다.
<!-- 나쁜 예: div로 만든 버튼 -->
<div class="btn" onclick="handleClick()">
클릭하세요
</div>
동작은 했다. 클릭하면 함수가 실행됐다. 그런데 키보드로 접근이 안 됐다. Tab 키를 아무리 눌러도 이 "버튼"은 포커스를 받지 않았다. 스크린 리더를 켜보니까 그냥 "클릭하세요"라고만 읽었다. 이게 버튼인지 텍스트인지 전혀 모르는 상태였다.
그리고 이미지는 이렇게 넣었다.
<!-- 나쁜 예: alt 없는 이미지 -->
<img src="/images/product.jpg">
이미지가 로딩 안 되면 빈 공간만 보였다. 스크린 리더는 "이미지"라고만 말했다. 무슨 이미지인지는 아무도 몰랐다.
이 두 가지 실수가 내 사이트를 "접근 불가능한 사이트"로 만들고 있었다. 깨달았을 때 등골이 서늘했다.
해답은 의외로 간단했다. 시맨틱 HTML, 즉 의미 있는 태그를 쓰는 것. 버튼은 <button>, 이미지는 alt 속성, 내비게이션은 <nav>, 메인 컨텐츠는 <main>. 이 원칙만 지켜도 80%는 해결됐다.
<!-- 좋은 예: button 태그 사용 -->
<button onclick="handleClick()">
클릭하세요
</button>
이렇게 바꾸니까 키보드로 Tab 키를 누르면 자동으로 포커스가 갔다. Enter 키로 클릭도 됐다. 스크린 리더는 "클릭하세요, 버튼"이라고 정확히 읽어줬다. 마법처럼 모든 게 해결됐다.
<!-- 좋은 예: alt 속성 추가 -->
<img src="/images/product.jpg" alt="빨간색 운동화 제품 사진">
이미지에 alt를 넣으니까, 이미지가 안 뜰 때도 "빨간색 운동화 제품 사진"이라는 텍스트가 보였다. 스크린 리더도 똑같이 읽어줬다. 사용자는 무슨 이미지인지 알 수 있었다.
결국 웹 접근성의 본질은 "브라우저에게 의미를 알려주는 것"이었다. div는 그냥 박스일 뿐이지만, button은 "이건 클릭할 수 있는 버튼이야"라고 브라우저에게 말해준다. 이 차이가 전부였다.
WCAG(Web Content Accessibility Guidelines)는 W3C에서 만든 웹 접근성 표준이다. 나는 이걸 "접근성 체크리스트"로 받아들였다. 크게 4가지 원칙이 있다.
이 원칙을 코드로 옮기면, 시맨틱 HTML 구조가 나온다. 페이지를 의미 단위로 나누는 것.
<!-- 좋은 예: 시맨틱 HTML 구조 -->
<header>
<nav>
<ul>
<li><a href="/">홈</a></li>
<li><a href="/about">소개</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>웹 접근성이란?</h1>
<section>
<h2>기본 개념</h2>
<p>모두가 쓸 수 있는 웹...</p>
</section>
</article>
</main>
<footer>
<p>© 2025 My Site</p>
</footer>
이렇게 나누니까 스크린 리더가 "헤더 영역입니다", "메인 콘텐츠입니다", "푸터입니다"라고 자동으로 안내했다. 사용자는 원하는 영역으로 바로 점프할 수 있었다. 마치 책의 목차처럼.
반대로 나쁜 예는 이랬다.
<!-- 나쁜 예: div만 쓴 구조 -->
<div class="header">
<div class="nav">
<div class="link">홈</div>
<div class="link">소개</div>
</div>
</div>
<div class="content">
<div class="title">웹 접근성이란?</div>
<div class="text">모두가 쓸 수 있는 웹...</div>
</div>
스크린 리더는 그냥 "div, div, div..."만 읽었다. 어디가 헤더고 어디가 본문인지 알 수 없었다. CSS로 보기에는 예쁘지만, 의미는 사라진 상태였다.
나는 이 비유가 와닿았다. 시맨틱 HTML은 건물에 표지판을 다는 것이다. "화장실", "비상구", "엘리베이터". 표지판 없으면 건물 안에서 길을 잃는다. div는 표지판 없는 빈 벽이고, header/nav/main/footer는 명확한 표지판이다.
시맨틱 HTML만으로는 부족할 때가 있다. 탭 UI, 모달, 드롭다운 같은 건 기본 HTML 태그로 표현이 안 된다. 이때 쓰는 게 ARIA(Accessible Rich Internet Applications).
ARIA는 세 가지로 나뉜다.
role="dialog", role="tab".aria-label="닫기 버튼", aria-describedby="help-text".aria-expanded="false", aria-hidden="true".모달 예시를 보자.
<!-- 좋은 예: ARIA로 모달 설명 -->
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">로그인</h2>
<form>
<label for="email">이메일</label>
<input type="email" id="email" aria-required="true">
<button type="submit">제출</button>
</form>
<button aria-label="모달 닫기" onclick="closeModal()">
×
</button>
</div>
role="dialog"는 "이건 모달이야"라고 알려준다. aria-modal="true"는 "지금 이 모달 밖은 비활성화됐어"라고 말한다. aria-labelledby="modal-title"은 "이 모달의 제목은 modal-title 요소야"라고 연결해준다. 스크린 리더는 이 정보를 읽고 "로그인 대화상자 열림"이라고 안내한다.
하지만 ARIA는 양날의 검이다. 잘못 쓰면 오히려 접근성을 망친다. "No ARIA is better than Bad ARIA"라는 격언이 있다. 예를 들어 이렇게 쓰면 안 된다.
<!-- 나쁜 예: 불필요한 ARIA -->
<button role="button" aria-label="버튼">
클릭하세요
</button>
<button>은 이미 role이 button이다. 굳이 role="button"을 또 쓸 필요가 없다. aria-label="버튼"도 중복이다. 버튼 안에 "클릭하세요"라는 텍스트가 있으니까. 이런 식으로 ARIA를 남발하면 스크린 리더가 "버튼, 버튼, 클릭하세요, 버튼"이라고 세 번 읽는다.
나는 이렇게 정리했다. ARIA는 HTML이 부족할 때만 쓴다. HTML로 해결되면 ARIA는 필요 없다.
마우스 없이 사이트를 쓴다고 상상해보자. Tab 키로 이동하고, Enter나 Space로 클릭하고, Esc로 닫는다. 이게 키보드 내비게이션이다.
가장 중요한 건 포커스 관리. 포커스는 "지금 여기 있어요"라고 알려주는 시각적 표시다. 보통 파란색 테두리로 보인다. 이걸 CSS로 지우면 안 된다.
/* 나쁜 예: 포커스 아웃라인 제거 */
button:focus {
outline: none;
}
이렇게 하면 키보드 사용자는 지금 어디에 있는지 알 수 없다. 대신 이렇게 해야 한다.
/* 좋은 예: 포커스 스타일 커스텀 */
button:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
아웃라인을 없애는 대신 더 보기 좋게 커스텀한다. 사용자는 포커스가 어디 있는지 명확히 알 수 있다.
또 중요한 건 Tab Order. Tab 키를 눌렀을 때 포커스가 논리적인 순서로 이동해야 한다. 보통은 HTML 순서대로 간다. 하지만 tabindex로 순서를 바꿀 수 있다.
<!-- 나쁜 예: tabindex로 순서 꼬기 -->
<button tabindex="3">첫 번째</button>
<button tabindex="1">두 번째</button>
<button tabindex="2">세 번째</button>
이러면 Tab 키를 눌렀을 때 "두 번째 → 세 번째 → 첫 번째" 순서로 간다. 사용자는 혼란스럽다. 가급적 tabindex는 쓰지 않고, HTML 순서를 논리적으로 짜는 게 낫다.
단, tabindex="-1"은 유용하다. 프로그래밍으로 포커스를 줄 수 있게 하되, Tab 키로는 접근 못하게 한다. 모달을 열었을 때 모달 안으로 포커스를 옮기는 데 쓴다.
// 좋은 예: 모달 열 때 포커스 이동
function openModal() {
const modal = document.getElementById('modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const firstInput = modal.querySelector('input');
firstInput.focus(); // 모달 안 첫 입력 필드로 포커스 이동
}
그리고 Skip Link도 배웠다. 긴 내비게이션을 매번 Tab으로 넘기는 건 지루하다. 페이지 맨 위에 "본문으로 바로가기" 링크를 숨겨두면, 키보드 사용자가 바로 메인 콘텐츠로 점프할 수 있다.
<!-- 좋은 예: Skip Link -->
<a href="#main-content" class="skip-link">
본문으로 바로가기
</a>
<!-- ... 긴 네비게이션 ... -->
<main id="main-content">
<h1>메인 콘텐츠</h1>
</main>
/* Skip Link는 평소에 숨김, 포커스 받으면 보임 */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
처음 Tab 키를 누르면 "본문으로 바로가기" 링크가 나타난다. Enter 치면 바로 메인 콘텐츠로 간다. 엄청 편했다.
색상 대비(Color Contrast)도 중요했다. 연한 회색 글자에 흰 배경이면, 시력이 안 좋은 사람은 읽기 힘들다. WCAG AA 기준은 4.5:1 (본문 텍스트), WCAG AAA는 7:1이다.
나는 Chrome DevTools의 Lighthouse를 돌렸다. Accessibility 점수가 70점이 나왔고, "텍스트와 배경 색상 대비가 부족합니다"라는 경고가 떴다. 회색 버튼 텍스트가 문제였다.
/* 나쁜 예: 대비 부족 */
button {
background: #e0e0e0;
color: #999999; /* 대비율 2.8:1 - 기준 미달 */
}
이걸 이렇게 바꿨다.
/* 좋은 예: 대비 충분 */
button {
background: #e0e0e0;
color: #333333; /* 대비율 7.2:1 - AAA 통과 */
}
글자가 더 진해지니까 읽기 훨씬 편했다. 나도 모르게 눈이 덜 피곤했다.
폼 접근성도 배웠다. 입력 필드에는 반드시 <label>을 연결해야 한다.
<!-- 나쁜 예: label 없는 input -->
<input type="text" placeholder="이름을 입력하세요">
placeholder만 있으면, 스크린 리더는 "텍스트 입력, 빈 칸"이라고만 읽는다. 무슨 입력인지 모른다.
<!-- 좋은 예: label 연결 -->
<label for="name">이름</label>
<input type="text" id="name" placeholder="이름을 입력하세요">
for와 id로 연결하니까, 스크린 리더는 "이름, 텍스트 입력"이라고 읽었다. 또 label을 클릭하면 input에 포커스가 갔다. 모바일에서 특히 편했다.
에러 메시지도 명확해야 한다.
<!-- 나쁜 예: 에러 메시지 불명확 -->
<input type="email" id="email">
<span style="color: red;">오류</span>
"오류"만 보여주면 뭐가 틀렸는지 모른다. 이렇게 바꿔야 한다.
<!-- 좋은 예: 명확한 에러 메시지 -->
<label for="email">이메일</label>
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">
이메일 형식이 올바르지 않습니다. 예: user@example.com
</span>
aria-invalid="true"는 "이 필드에 오류가 있어요"라고 알린다. aria-describedby는 에러 메시지를 연결한다. role="alert"는 스크린 리더가 즉시 에러를 읽게 한다. 사용자는 정확히 뭘 고쳐야 하는지 안다.
이론을 배웠으니 실제이다. 접근 가능한 드롭다운 메뉴를 만들어봤다.
<!-- 접근 가능한 드롭다운 메뉴 -->
<nav>
<ul role="menubar">
<li role="none">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="dropdown-menu"
id="menu-button"
>
메뉴
</button>
<ul role="menu" id="dropdown-menu" hidden>
<li role="none">
<a href="/profile" role="menuitem">프로필</a>
</li>
<li role="none">
<a href="/settings" role="menuitem">설정</a>
</li>
<li role="none">
<a href="/logout" role="menuitem">로그아웃</a>
</li>
</ul>
</li>
</ul>
</nav>
// 드롭다운 키보드 제어
const menuButton = document.getElementById('menu-button');
const dropdownMenu = document.getElementById('dropdown-menu');
menuButton.addEventListener('click', toggleMenu);
menuButton.addEventListener('keydown', handleKeydown);
function toggleMenu() {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true';
menuButton.setAttribute('aria-expanded', !isExpanded);
dropdownMenu.hidden = isExpanded;
if (!isExpanded) {
// 메뉴 열릴 때 첫 항목에 포커스
dropdownMenu.querySelector('[role="menuitem"]').focus();
}
}
function handleKeydown(e) {
if (e.key === 'Escape') {
toggleMenu();
menuButton.focus(); // 메뉴 닫으면 버튼으로 포커스 복귀
}
}
이렇게 만드니까:
처음으로 "이거 접근 가능하네"라는 느낌을 받았다.
만들고 나서 테스트가 필수다. 나는 세 가지 도구를 썼다.
Lighthouse 돌렸더니 처음엔 68점이었다. 주요 문제:
하나씩 고치고 다시 테스트하니까 94점이 나왔다. 완벽하진 않지만 만족스러웠다.
스크린 리더 테스트는 눈을 감고 해봤다. VoiceOver 켜고 키보드만으로 내 사이트를 써봤다. 로그인까지 가는 데 30초 걸렸다. 이전에 1분 걸렸던 것에 비하면 50% 개선이었다. 모든 버튼이 명확히 읽혔고, 폼 입력도 어렵지 않았다.
웹 접근성을 처음 배울 땐 "이거 너무 복잡한데?"라고 생각했다. ARIA 속성은 외울 게 많고, WCAG 가이드라인은 딱딱했다. 하지만 직접 해보니까 결국 핵심은 간단했다. "내가 쓸 수 없는 사이트를 남에게 주지 말자".
마우스 없이 써보면 답이 보인다. 스크린 리더 켜보면 빠진 게 보인다. Lighthouse 돌려보면 점수가 나온다. 도구는 많고, 방법은 명확하다. 문제는 실천이었다.
나는 이제 버튼을 만들 때 <button>을 쓴다. 이미지에는 alt를 넣는다. 포커스 아웃라인을 절대 지우지 않는다. 폼에는 <label>을 연결한다. 이 작은 습관들이 쌓여서, "모두가 쓸 수 있는 웹"이 만들어진다고 믿는다.
마지막으로 이 비유가 마음에 들었다. 웹 접근성은 경사로다. 휠체어 사용자를 위해 만들지만, 유모차 끄는 부모, 캐리어 끄는 여행자, 다친 사람도 다 편하다. alt 태그 하나, <button> 태그 하나가 누군가의 경사로가 된다. 그리고 언젠가는 나의 경사로가 될 거라고, 나는 그렇게 이해했다.