뒤로가기
웹 접근성, 왜 자꾸 미루게 될까

May 22, 2025

accessibilityfrontendHTML

CS 티켓 하나가 올라왔다. "스크린 리더로 사이트를 이용할 수 없습니다."

그때까지 나는 접근성이라는 단어를 들으면 "나중에 해야지" 목록에 자동으로 넣고 있었다. 그런데 그 티켓을 읽고 macOS VoiceOver를 켠 순간, 뭔가 단단히 잘못됐다는 걸 알았다. 메인 페이지에서 Tab 키를 눌렀다. VoiceOver가 읽어주는 내용은 "링크, 링크, 링크, 버튼, 링크"였다. 어떤 링크인지, 어디로 가는 버튼인지 전혀 알 수 없었다. 네비게이션 영역이 끝나고 본문이 시작되는 지점도 구분이 안 됐다. 전체 페이지가 하나의 거대한 div 덩어리였으니까.

그 CS를 올린 분이 매일 이런 상태의 웹을 쓰고 있다는 사실이 머리를 때렸다. 그 주에 접근성 개선 작업을 시작했고, 생각보다 적은 노력으로 많은 것이 바뀌었다.

semantic HTML만 제대로 써도 90%는 해결된다#

접근성 하면 ARIA 속성이나 WAI-ARIA 스펙부터 떠올리기 쉬운데, 실제로 해보니 가장 임팩트가 큰 건 HTML 태그를 용도에 맞게 쓰는 거였다.

우리 프로젝트의 GNB 코드가 이랬다.

html
<div class="nav-wrapper">
  <div class="nav-item" onclick="goTo('/home')"></div>
  <div class="nav-item" onclick="goTo('/about')">소개</div>
  <div class="nav-item" onclick="goTo('/contact')">문의</div>
</div>

스크린 리더 입장에서 이건 그냥 텍스트 3개가 나열된 거다. 네비게이션인지, 클릭할 수 있는 건지 알 방법이 없다. 키보드로는 포커스조차 가지 않는다. div에는 기본 포커스가 없으니까.

바꾼 코드는 이거다.

html
<nav aria-label="메인 네비게이션">
  <a href="/home"></a>
  <a href="/about">소개</a>
  <a href="/contact">문의</a>
</nav>

추가한 CSS? 0줄이다. 추가한 JavaScript? 0줄이다. 그런데 스크린 리더가 "네비게이션 랜드마크"로 인식하고, 각 항목을 "링크, 홈"처럼 역할과 함께 읽어준다. 키보드 Tab으로 이동도 된다.

같은 패턴이 프로젝트 곳곳에 있었다.

tsx
// Before: 스크린 리더가 이게 버튼인지 모른다
<div className="btn-primary" onClick={handleSubmit}>
  제출하기
</div>

// After: 네이티브 button은 Enter, Space 키 입력도 자동 처리
<button className="btn-primary" onClick={handleSubmit}>
  제출하기
</button>
tsx
// Before: 본문 영역의 시작점을 알 수 없다
<div className="content-area">
  <div className="article-wrapper">
    <div className="article-title">제목</div>
    <div className="article-body">본문...</div>
  </div>
</div>

// After: 스크린 리더 사용자가 main으로 바로 점프 가능
<main>
  <article>
    <h1>제목</h1>
    <p>본문...</p>
  </article>
</main>

nav, main, article, aside, header, footer — 이 태그들은 스크린 리더에서 랜드마크로 작동한다. 사용자가 페이지 구조를 파악하고 원하는 영역으로 바로 이동할 수 있게 해준다. div는 의미가 없다. 레이아웃용으로만 써야 한다.

이 작업만으로 Lighthouse accessibility 점수가 62점에서 87점으로 올라갔다.

alt 텍스트, 생각보다 까다롭다#

img 태그에 alt를 넣으라는 건 다 안다. 문제는 "뭘 넣어야 하는가"다.

html
<!-- 이렇게 쓰는 사람이 많다 -->
<img src="/banner.jpg" alt="배너" />
<img src="/profile.jpg" alt="이미지" />
<img src="/chart.png" alt="차트" />

"배너"라는 alt 텍스트는 스크린 리더 사용자에게 아무 정보도 주지 못한다. "이미지"는 더 심하다. 스크린 리더가 이미 이게 이미지라는 건 알려주기 때문에, 결과적으로 "이미지, 이미지"라고 읽히게 된다.

alt 텍스트는 그 이미지가 전달하려는 정보를 담아야 한다.

html
<!-- 제품 상세 페이지의 썸네일 -->
<img src="/product-42.jpg" alt="네이비 컬러 오버사이즈 후드 집업, 전면 지퍼 디테일" />

<!-- 데이터 시각화 차트 -->
<img src="/chart.png" alt="2025년 월별 매출 추이, 6월 최고치 2.3억 기록" />

<!-- 프로필 사진 -->
<img src="/profile.jpg" alt="조상현 프로필 사진" />

그런데 빈 alt가 정답인 경우도 있다.

html
<!-- 순수 장식용 이미지:  alt가 맞다 -->
<img src="/decorative-line.svg" alt="" />

<!-- 텍스트 옆의 아이콘: 이미 텍스트가 정보를 전달하므로 -->
<button>
  <img src="/search-icon.svg" alt="" />
  검색
</button>

alt=""를 넣으면 스크린 리더가 그 이미지를 완전히 건너뛴다. 장식용 이미지에 "장식" 같은 alt를 넣으면 오히려 노이즈가 된다. 반면 alt 속성 자체를 빼버리면 스크린 리더가 파일 이름을 읽어버린다. decorative-line.svg를 그대로 읽는 거다. 그러니 장식용이면 빈 문자열, 정보를 담고 있으면 그 정보를 서술하는 텍스트. 이 구분만 확실히 하면 된다.

outline: none 한 줄이 키보드 사용자를 차단한다#

CSS 리셋 파일을 열어보자.

css
/* 한 번쯤 봤을 거다 */
*:focus {
  outline: none;
}

이 한 줄이 키보드 네비게이션을 사실상 불가능하게 만든다. 마우스 사용자에게는 아무 영향이 없지만, 키보드로 사이트를 탐색하는 사람은 지금 포커스가 어디에 있는지 전혀 알 수 없게 된다. 눈을 감고 Tab 키를 누르는 거나 다름없다.

"포커스 링이 디자인에 안 어울린다"는 말, 디자이너에게 많이 들었다. 맞다. 크롬 기본 포커스 링이 예쁘지 않다는 데 동의한다. 하지만 지우는 게 답이 아니다.

css
/* 마우스 클릭 시에는 포커스 링 제거, 키보드 사용 시에는 표시 */
:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid #4A90D9;
  outline-offset: 2px;
  border-radius: 2px;
}

:focus-visible은 키보드로 포커스가 이동했을 때만 적용되는 pseudo-class다. 마우스 클릭으로 버튼을 누르면 포커스 링이 안 보이고, Tab 키로 넘어오면 보인다. 디자이너도 만족하고, 키보드 사용자도 만족하는 해법이다. 브라우저 지원도 이제 충분하다 — IE만 아니면 된다.

한 가지 더. 커스텀 포커스 스타일을 만들 때 색상 대비를 신경 써야 한다. 배경색과 포커스 링 색상이 비슷하면 의미가 없다.

aria-label vs aria-labelledby#

ARIA 속성은 semantic HTML로 해결이 안 될 때 쓰는 보조 수단이다. 가장 많이 쓰는 두 가지가 aria-labelaria-labelledby인데, 용도가 다르다.

aria-label: 화면에 보이는 텍스트가 없을 때 직접 레이블을 지정한다.

tsx
// 아이콘만 있는 버튼 — 시각적 텍스트가 없으므로 aria-label 필요
<button aria-label="메뉴 닫기" onClick={closeMenu}>
  <CloseIcon />
</button>

// 검색 입력 필드 — label 태그 없이 쓸 때
<input type="search" aria-label="블로그 글 검색" placeholder="검색어 입력..." />

aria-labelledby: 화면에 이미 보이는 텍스트를 레이블로 참조한다.

tsx
// 모달 다이얼로그 — 제목이 이미 화면에 있다
<div role="dialog" aria-labelledby="modal-title">
  <h2 id="modal-title">결제 확인</h2>
  <p>정말 결제하시겠습니까?</p>
</div>

// 섹션 레이블링 — 같은 "더 보기" 버튼이 여러 개일 때
<section aria-labelledby="recent-posts-heading">
  <h2 id="recent-posts-heading">최근 글</h2>
  <a href="/posts">더 보기</a>
</section>

<section aria-labelledby="popular-posts-heading">
  <h2 id="popular-posts-heading">인기 글</h2>
  <a href="/popular">더 보기</a>
</section>

판단 기준은 간단하다. 화면에 텍스트가 보이면 aria-labelledby, 안 보이면 aria-label. 둘 다 있으면 aria-labelledby가 우선한다. 그리고 네이티브 HTML로 해결할 수 있으면 ARIA를 안 쓰는 게 제일 좋다. <label htmlFor="email">이메일</label>aria-label보다 낫다.

color contrast: 눈으로 봐서는 모른다#

디자인 시안을 받아서 그대로 구현했는데, Lighthouse에서 color contrast 경고가 떴다. 확인해보니 밝은 회색 배경에 연한 회색 텍스트 — 내 모니터에서는 잘 보였다. 하지만 WCAG 기준은 명확하다.

  • 일반 텍스트: 최소 4.5:1 대비 비율
  • 큰 텍스트 (18pt 이상, 또는 14pt 볼드): 최소 3:1 대비 비율

구체적으로, 우리가 쓰던 placeholder 색상이 문제였다.

css
/* Before: 대비 비율 2.3:1 — WCAG 미달 */
input::placeholder {
  color: #C0C0C0; /* 밝은 배경에 밝은 회색 */
}

/* After: 대비 비율 4.6:1 — WCAG AA 통과 */
input::placeholder {
  color: #767676;
}

Chrome DevTools에서 바로 확인할 수 있다. 요소를 선택하고 color 값 옆의 색상 피커를 열면 Contrast ratio가 표시된다. AA 기준을 통과하면 초록색 체크, 아니면 경고가 뜬다.

디자이너에게 이 기준을 공유했을 때 반응이 좋았다. "그냥 예쁘게"가 아니라 "4.5:1 이상"이라는 숫자가 있으니 논의가 명확해졌다. Figma에서도 Stark 같은 플러그인으로 디자인 단계에서 대비를 체크할 수 있다.

실전 도구 두 가지#

접근성 개선을 시작할 때 어디서부터 손대야 할지 모르겠다면, 도구의 힘을 빌려야 한다.

axe DevTools Chrome 확장을 설치하고, DevTools에서 axe 탭을 열어 페이지를 스캔하면 된다. 문제를 심각도별로 분류해주고, 각 이슈마다 어떤 요소가 문제인지, 어떻게 고쳐야 하는지까지 알려준다. 내가 처음 우리 메인 페이지를 돌렸을 때 critical 이슈가 23개 나왔다. 대부분이 이미지 alt 누락과 버튼 레이블 부재였고, 고치는 데 2시간이 안 걸렸다.

CI에 자동화하고 싶다면 @axe-core/react를 개발 환경에 넣을 수 있다.

tsx
// 개발 환경에서만 접근성 위반을 콘솔에 경고로 표시
if (process.env.NODE_ENV === 'development') {
  import('@axe-core/react').then((axe) => {
    axe.default(React, ReactDOM, 1000);
  });
}

이걸 넣으면 개발하면서 실시간으로 접근성 위반 사항이 콘솔에 뜬다. 새 컴포넌트를 만들 때마다 자연스럽게 체크하게 된다.

Lighthouse accessibility audit는 Chrome DevTools의 Lighthouse 탭에서 Accessibility 카테고리만 선택해서 돌리면 된다. axe 엔진 기반이라 결과가 비슷하지만, 점수로 보여주니까 진행 상황을 추적하기 편하다. PR마다 Lighthouse CI를 돌려서 접근성 점수가 떨어지면 머지를 막는 팀도 있다.

비즈니스 관점에서 한마디만#

접근성은 "착한 일"이기만 한 게 아니다. semantic HTML을 쓰면 검색 엔진이 페이지 구조를 더 잘 이해한다. nav, main, article 같은 태그는 Google 크롤러에게도 의미 있는 시그널이다. alt 텍스트는 이미지 검색 트래픽에 직접 영향을 준다. SEO와 접근성은 상당 부분 겹친다.

법적 측면도 있다. 미국에서는 ADA(Americans with Disabilities Act) 기반 웹 접근성 소송이 매년 늘고 있고, 한국도 장애인차별금지법에 웹 접근성 의무가 포함되어 있다. 공공기관은 이미 의무 대상이고, 민간 기업으로 확대되는 추세다.

오늘 당장 할 수 있는 것#

접근성에서 가장 위험한 마인드셋은 "제대로 하려면 시간이 너무 많이 든다"다. 그래서 계속 미루게 된다. 전체를 완벽하게 만들 필요 없다. 오늘 하나만 고치면 된다.

지금 바로 할 수 있는 체크리스트를 하나 남긴다.

  1. CSS 리셋에서 outline: none 찾아서 :focus-visible 패턴으로 교체
  2. divonClick이 붙어있는 곳을 검색해서 button이나 a로 변경
  3. img 태그에 alt가 빈 채로 빠져있는 곳 찾아서 적절한 텍스트 추가
  4. axe DevTools 설치하고 메인 페이지 한 번 스캔

우리 팀은 이 네 가지를 하루 만에 끝냈다. Lighthouse 점수가 62에서 91로 올라갔고, 그 시각장애인 유저에게 개선 사항을 안내했을 때 감사하다는 답변을 받았다. 기능 하나 추가한 것보다 기분이 좋았다.

접근성은 특별한 기술이 아니다. HTML을 원래 용도대로 쓰고, 키보드로 한 번 테스트해보고, 도구를 돌려보는 것. 그게 전부다.