SK텔레콤 T world 운영 프로젝트에서 직접 적용한 웹 접근성 작업 사례를
Before / After 형식으로 정리했습니다. 단순 코드 수정을 넘어, 왜 이렇게
작성해야 하는지 근거와 함께 기록합니다.
<!-- ❌ alt가 없거나 파일명 그대로 --> <img src="banner_main.jpg"> <!-- ❌ 의미 없는 alt --> <img src="event.jpg" alt="이미지"> <!-- ❌ 장식용 이미지에 alt 없음 --> <img src="divider.png">
<!-- ✅ 이미지 내용을 구체적으로 설명 --> <img src="banner_main.jpg" alt="5G 요금제 여름 특가, 최대 50% 할인"> <!-- ✅ 맥락에 맞는 구체적 alt --> <img src="event.jpg" alt="7월 한정 요금제 출시 이벤트"> <!-- ✅ 장식용 이미지는 alt="" 빈 값 처리 --> <!-- ✅ 오래된 브라우저/보조공학기기 대응 role 추가 --> <img src="divider.png" alt="" role="presentation">
<!-- ❌ type 없음 + 레이블 없는 아이콘 버튼 --> <button class="btn-close"> <img src="icon-close.svg" alt=""> </button> <!-- ❌ 맥락 없는 "더보기" 반복 --> <button>더보기</button> <button>더보기</button>
<!-- ✅ type + aria-label 명시 --> <button type="button" class="btn-close" aria-label="팝업 닫기"> <img src="icon-close.svg" alt=""> </button> <!-- ✅ 맥락을 담은 aria-label --> <button type="button" aria-label="5G 요금제 더보기">더보기</button> <button type="button" aria-label="인터넷 요금제 더보기">더보기</button>
<!-- ❌ 포커스 이동 없음 --> <div class="modal" id="modal"> 모달 내용 <button>닫기</button> </div> <script> function openModal() { document.getElementById('modal') .style.display = 'block'; } </script>
<!-- ✅ role + aria 속성 + 포커스 이동 --> <div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 id="modal-title" tabindex="-1">제목</h2> <button type="button" aria-label="모달 닫기">닫기</button> </div> <script> function openModal() { const modal = document.getElementById('modal'); modal.style.display = 'block'; modal.querySelector('#modal-title').focus(); } </script>
<!-- ❌ span으로 레이블 처리 --> <div class="form-group"> <span class="label">이름</span> <input type="text" placeholder="이름을 입력하세요"> </div> <!-- ❌ placeholder만 있고 label 없음 --> <input type="email" placeholder="이메일 주소">
<!-- ✅ for-id로 label과 input 연결 --> <div class="form-group"> <label for="input-name">이름</label> <input type="text" id="input-name" placeholder="홍길동"> </div> <!-- ✅ label + placeholder 함께 제공 --> <label for="input-email">이메일</label> <input type="email" id="input-email" placeholder="example@email.com">
<!-- ❌ display:none → 스크린 리더도 못 읽음 --> <style> .hidden { display: none; } </style> <a href="/cart"> <img src="icon-cart.svg" alt=""> <span class="hidden">장바구니</span> </a>
<!-- ✅ visually-hidden: 화면엔 숨기고 보조기술엔 전달 --> <style> .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } </style> <a href="/cart"> <img src="icon-cart.svg" alt=""> <span class="visually-hidden">장바구니</span> </a>
<!-- ❌ hover CSS만으로 툴팁 처리 --> <div class="tooltip-wrap"> <button type="button">?</button> <span class="tooltip"> 요금제 상세 안내입니다. </span> </div> <style> .tooltip { display: none; } .tooltip-wrap:hover .tooltip { display: block; } </style>
<!-- ✅ aria-describedby + role="tooltip" --> <div class="tooltip-wrap"> <button type="button" aria-label="요금제 안내" aria-describedby="tooltip-plan"> ? </button> <span id="tooltip-plan" role="tooltip"> 요금제 상세 안내입니다. </span> </div> <style> /* hover + focus-within 모두 대응 */ .tooltip-wrap:hover .tooltip, .tooltip-wrap:focus-within .tooltip { display: block; } </style>
<!-- ❌ div/button만으로 탭 구현 --> <div class="tab-list"> <button class="tab active"> 5G 요금제 </button> <button class="tab"> LTE 요금제 </button> </div> <div class="tab-panel"> 5G 요금제 내용 </div>
<!-- ✅ ARIA role로 탭 구조 명시 --> <div role="tablist" aria-label="요금제 종류"> <button type="button" role="tab" aria-selected="true" aria-controls="panel-5g"> 5G 요금제 </button> <button type="button" role="tab" aria-selected="false" aria-controls="panel-lte"> LTE 요금제 </button> </div> <div id="panel-5g" role="tabpanel"> 5G 요금제 내용 </div>
<!-- ❌ 열림/닫힘 상태 전달 없음 --> <div class="accordion"> <button type="button" class="accordion-btn"> 자주 묻는 질문 </button> <div class="accordion-panel"> 답변 내용 </div> </div>
<!-- ✅ aria-expanded + aria-controls --> <div class="accordion"> <button type="button" class="accordion-btn" aria-expanded="false" aria-controls="faq-panel-1"> 자주 묻는 질문 </button> <div id="faq-panel-1" hidden> 답변 내용 </div> </div> <!-- JS: 클릭 시 상태 토글 --> btn.setAttribute('aria-expanded', 'true'); panel.removeAttribute('hidden');
<!-- ❌ caption, scope, thead 없음 --> <table> <tr> <td>요금제명</td> <td>월정액</td> <td>데이터</td> </tr> <tr> <td>5G 베이직</td> <td>55,000원</td> <td>10GB</td> </tr> </table>
<!-- ✅ caption + thead + scope 적용 --> <table> <caption>5G 요금제 비교표</caption> <thead> <tr> <th scope="col">요금제명</th> <th scope="col">월정액</th> <th scope="col">데이터</th> </tr> </thead> <tbody> <tr> <td>5G 베이직</td> <td>55,000원</td> <td>10GB</td> </tr> </tbody> </table>
<!-- ❌ 새 창 안내 없음 --> <a href="https://external.com" target="_blank"> 요금제 안내 바로가기 </a> <!-- ❌ 팝업과 페이지 이동이 같은 형태 --> <a href="#" onclick="openPopup()"> 자세히 보기 </a> <a href="#section-plan"> 자세히 보기 </a>
<!-- ✅ 새 창 — hidden 텍스트로 새창 안내 --> <a href="https://external.com" target="_blank"> 요금제 안내 바로가기 <span class="visually-hidden">(새 창으로 열림)</span> </a> <!-- ✅ 팝업 — button 태그로 명확히 구분 --> <button type="button" aria-haspopup="dialog"> 팝업으로 자세히 보기 </button> <!-- ✅ 페이지 이동 — 앵커 id 연결 --> <a href="#section-plan"> 요금제 자세히 보기 </a>
| # | 사례 | WCAG 항목 | 등급 | 핵심 기법 |
|---|---|---|---|---|
| 01 | 이미지 대체 텍스트 | 1.1.1 텍스트가 아닌 콘텐츠 | A | alt 속성, role="presentation" |
| 02 | 버튼 레이블 / type | 4.1.2 이름, 역할, 값 | A | aria-label, type="button" |
| 03 | 키보드 내비게이션 | 2.1.1 키보드 | A | role="dialog", aria-modal, .focus() |
| 04 | 폼 레이블 연결 | 1.3.1 정보와 관계 | A | <label for> / <input id> |
| 05 | 숨김 텍스트 | 1.3.1 정보와 관계 | A | visually-hidden (IR 기법) |
| 06 | 툴팁 | 1.3.1 정보와 관계 | A | role="tooltip", aria-describedby |
| 07 | 탭 UI | 4.1.2 이름, 역할, 값 | A | role="tablist/tab/tabpanel", aria-selected |
| 08 | 아코디언 | 4.1.2 이름, 역할, 값 | A | aria-expanded, aria-controls |
| 09 | 테이블 접근성 | 1.3.1 정보와 관계 | A | caption, scope, thead/tbody |
| 10 | 팝업 vs 페이지 이동 | 3.2.2 입력 시 변화 | A | button / a 구분, aria-haspopup |