들어가기 전에..! 요즘 프론트엔드 채용 공고를 살펴보면 "접근성"이라는 키워드를 많이 볼 수 있습니다.
하지만 제가 접근성을 생각하면서 개발한 것들을 떠올려보면 meta 태그 입력하기, 시멘틱 구조 잡기, aria-label, alt 정도였던 것 같습니다.
최근 웹앱 개발을 하면서 디자인 시스템 문서를 보다 보니 aria-* 속성이 많은 컴포넌트에 적용되어 있었고, 스크린리더와 관련된다는 것 정도는 알고 있었지만, 솔직히 실무 우선순위에서는 늘 뒤로 밀려왔던 것이 사실입니다.
웹 접근성(Web Accessibility)이란, 사람들의 능력이 어느 정도 제한되어 있더라도 가능한 사람이 동일한 방식으로 웹을 사용할 수 있도록 보장하는 것입니다.
웹 접근성의 기초는 결국 "시맨틱 HTML"입니다.
시맨틱 HTML은 태그 자체에 의미가 담겨 있는 마크업을 뜻하며 단순히 <div>와 <span>만 쓰는 게 아니라 콘텐츠의 역할과 의미에 맞는 태그를 사용하는 것을 말합니다.
<section> vs <article> vs <div>세 개의 태그들은 콘텐츠 영역을 구분하는데 사용됩니다.
div: 의미없는 컨테이너, 콘텐츠에 의미를 부여하지 않습니다. 단순히 스타일링이나 레이아웃을 위한 그룹화에 사용되며 다른 시맨틱 태그를 쓸 수 없을 때 사용합니다.section: 주제별 콘텐츠 그룹입니다. 웹 페이지 내 주요 주제의 일부 영역이고 단독으로는 의미가 불완전할 수 있습니다.article: 독립적인 콘텐츠입니다. 완전히 독립적으로 배포 가능한 콘텐츠 입니다. 다른 페이지에 복사해도 의미가 온전히 유지됩니다.article 안에 section을 넣을 수 있고, section 안에 article을 넣을 수도 있습니다.
<!-- div -->
<div class="container">
<div class="wrapper">
<!-- 단순히 레이아웃을 위한 박스 -->
</div>
</div>
<!-- section -->
<section>
<h2>최신 뉴스</h2>
<p>오늘의 뉴스 내용...</p>
<p>이 섹션은 전체 페이지의 한 부분입니다.</p>
</section>
<!-- article -->
<article>
<h2>React 19의 새로운 기능</h2>
<p>작성자: 홍길동</p>
<p>React 19에서는...</p>
</article>
<button> vs <a>클릭 가능한 요소지만 역할과 동작이 다릅니다.
button: 액션을 수행할 때 사용합니다. Space와 Enter 키로 활성화됩니다.a: 다른 페이지나 위치로 이동할 때 사용합니다. Enter 키로 활성화됩니다.<!-- button -->
<button onClick={handleDelete}>삭제</button>
<!-- a -->
<a href="/products">상품 보기</a>
스크린 리더가 "버튼입니다" vs "링크입니다"를 다르게 안내합니다.
사용자가 예상하는 동작이 다릅니다. (링크는 우클릭 시 새탭 열기가 가능합니다.)
<b> vs <strong>둘 다 화면에서 굵은 글씨로 나타나지만, 다른 의미를 가지고 있습니다.
b: 시각적 스타일만 나타냅니다. 단순하게 굵게 보이게 합니다.strong: 중요도를 나타냅니다. 스크린 리더가 강조해서 읽습니다.<!-- b: 시각적 스타일만 -->
<p>이 텍스트는 <b>굵게</b> 표시됩니다.</p>
<!-- strong: 의미적 강조 -->
<p>
<strong>주의:</strong> 이 제품은 <em>반드시</em>
냉장 보관해야 합니다.
</p>
<i> vs <em>i: 시각적 스타일만 나타냅니다. 단순하게 기울임체로 보이게 합니다.em: 강세를 나타냅니다. 스크린리더가 억양을 달리해서 읽습니다.<!-- i: 시각적 스타일만 -->
<p>이 텍스트는 <i>기울임</i> 표시됩니다.</p>
<!-- em: 의미적 강조 -->
<p>이것은 <em>강조하고 싶은</em> 부분입니다.</p>
<img> vs <figure> +<figcaption>이미지를 표현하는 방법이지만 설명을 제공하는 방식이 다릅니다.
img: 단순 이미지입니다. alt 속성으로 대체 텍스트를 제공합니다.figure + figcaption: 이미지와 캡션을 그룹화합니다. 스크린리더가 이미지와 설명을 연결해서 읽어줍니다.<!-- img: 단순 이미지 -->
<img src="chart.png" alt="2024년 월별 매출 그래프" />
<!-- 스크린 리더: "2024년 월별 매출 그래프, 이미지" -->
<!-- figure: 이미지 + 캡션 -->
<figure>
<img src="chart.png" alt="월별 매출 그래프" />
<figcaption>2024년 매출은 전년 대비 20% 증가했습니다.</figcaption>
</figure>
ARIA(Accessible Rich Internet Applications)는 HTML만으로는 표현하기 어려운 UI 상태나 구조를 보조기술(스크린리더)에게 전달하기 위한 속성 집합입니다.
원칙: "No ARIA is better than Bad ARIA"
잘못된 ARIA는 접근성을 오히려 해칠 수 있습니다.
HTML이 이미 제공하는 기본 의미(네이티브 시맨틱)가 항상 우선입니다.
aria-label: 요소에 접근 가능한 이름 제공 (텍스트가 없거나 시각적으로 숨겨야 할 때)aria-labelledby: 다른 요소의 텍스트를 이름으로 연결aria-describedby: 상세 설명 제공, 주로 입력 폼 안내 페이지aria-expanded: 아코디언, 드롭다운 메뉴가 열림/닫힘 상태인지 표시aria-checked: 체크박스, 토글 스위치 상태 표시aria-selected: 탭, 리스트박스 등 선택 상태 표시role="dialog": 모달 대화 상자 의미role="alert": 즉시 알려야 하는 경고 메세지role="tablist", role="tab", role="tabpanel": 탭 UI 구조 정의aria-controls: 이 요소가 제어하는 요소가 무엇인지 연결aria-owns: DOM 구조가 다르지만 논리적으로 자식 관계를 연결aria-labelledby: 요소 제목(title) 연결aria-live="polite": 새 메세지를 조용히 알려줌aria-live="assertive": 즉시 끼어들어 알려줌 (주의 필요)aria-atomic: 전체 메세지를 다시 읽을지 여부aria-relevant: 어떤 변경을 감지할지사용자가 클릭, 탭, 키보드 조작을 해야 하는 UI라면 button 또는 a 같은 시맨틱 요소를 반드시 사용해야 합니다.
❌ 나쁜 예 - div를 버튼처럼 사용
<div role="button" tabindex="0" onclick="submitForm()">
제출
</div>
✅ 좋은 예 - 네이티브 버튼 사용
<button type="submit">제출</button>
왜 이렇게 해야 할까?
HTML5 요소가 이미 역할(role)을 내장하고 있다면 굳이 ARIA를 덧붙이지 않습니다.
❌ 나쁜 예 - 의미 중복
<nav role="navigation">...</nav>
✅ 좋은 예 - 기본 의미 그대로
<nav>...</nav>
사용해야 하는 이유
<nav>를 이미 navigation landmark로 인식한다.텍스트 없는 버튼은 스크린리더에게 "버튼"으로만 읽힙니다.
목적(닫기/삭제/열기)을 명확히 알려야 합니다.
❌ 나쁜 예 - 이름 없는 버튼
<button>
<svg>...</svg>
</button>
✅ 좋은 예 - aria-label 추가
<button aria-label="닫기">
<svg aria-hidden="true">...</svg>
</button>
✅ 또는 - sr-only 텍스트 사용
<button>
<span class="sr-only">닫기</span>
<svg aria-hidden="true">...</svg>
</button>
UI가 열렸는지/닫혔는지 상태를 aria-expanded로 나타내고, 어떤 요소를 제어하는지 aria-controls로 연결해야 한다.
❌ 나쁜 예 - 상태 정보 없음
<div class="accordion-header" onclick="toggle()">
FAQ 제목
</div>
<div class="accordion-content">내용…</div>
✅ 좋은 예 - 상태 + 관계 표현
<button aria-expanded="false" aria-controls="answer1" id="question1">
FAQ 제목
</button>
<div id="answer1" role="region" aria-labelledby="question1" hidden>
내용...
</div>
UX를 위해 디자인적 요소가 들어가더라도 스크린리더가 굳이 읽을 필요가 없는 콘텐츠는 alt="" 또는 aria-hidden="true"를 사용해 숨겨야 한다.
❌ 나쁜 예 - 불필요한 alt
<img src="pattern.png" alt="배경 패턴 이미지" />
✅ 좋은 예 - 빈 alt로 숨기기
<img src="pattern.png" alt="" aria-hidden="true" />
UI가 자동으로 갱신되면 스크린리더에게 변화가 전달되지 않을 수 있으므로 aria-live로 메세지 영역을 지정해야 한다.
❌ 나쁜 예 - 변화는 있지만 알림 없음
<div id="status">저장되었습니다</div>
✅ 좋은 예 - live region 적용
<div aria-live="polite" id="status"></div>
// JS로 메세지 업데이트
document.getElementById("status").textContent = "저장되었습니다";
이번에 접근성을 공부하면서 느낀점은 접근성이 특별하거나 거창한 것이 아니라는 것입니다.
접근성은 특정 사용자만을 위한 기능이 아니라 모든 사용자를 고려하는 더 나은 개발 습관이며 장기적으로는 개발자에게도 유지보수성과 확장성을 가져다 주는 선택이라고 느꼈습니다!
덕분에 요즘 핫한 시맨틱 태그에 대해 다시 한 번 상기시키는 내용이였습니다~! 스크린 리더 부분은 잘 몰랐는데 항상 좋은 내용 작성해주셔서 도움이 많이됩니다. 감사합니다.