Portfolio 01 · Accessibility Cases

웹 접근성
개선 사례

SK텔레콤 T world 운영 프로젝트에서 직접 적용한 웹 접근성 작업 사례를
Before / After 형식으로 정리했습니다. 단순 코드 수정을 넘어, 왜 이렇게 작성해야 하는지 근거와 함께 기록합니다.

10
개선 사례
WCAG
2.1 기준 적용
2년+
실무 운영 경험

Case 01
이미지 대체 텍스트 (alt)
의미 있는 이미지에 적절한 alt 속성을 제공하지 않으면 스크린 리더 사용자가 이미지 정보를 전혀 받을 수 없습니다.
WCAG 1.1.1 — 텍스트가 아닌 콘텐츠 (Level A)
Before — 문제 있는 코드
<!-- ❌ alt가 없거나 파일명 그대로 -->
<img src="banner_main.jpg">

<!-- ❌ 의미 없는 alt -->
<img src="event.jpg" alt="이미지">

<!-- ❌ 장식용 이미지에 alt 없음 -->
<img src="divider.png">
After — 개선된 코드
<!-- ✅ 이미지 내용을 구체적으로 설명 -->
<img src="banner_main.jpg"
     alt="5G 요금제 여름 특가, 최대 50% 할인">

<!-- ✅ 맥락에 맞는 구체적 alt -->
<img src="event.jpg"
     alt="7월 한정 요금제 출시 이벤트">

<!-- ✅ 장식용 이미지는 alt="" 빈 값 처리 -->
<!-- ✅ 오래된 브라우저/보조공학기기 대응 role 추가 -->
<img src="divider.png"
     alt="" role="presentation">
💡 개선 포인트
스크린 리더는 alt 값을 그대로 읽습니다. "이미지"라고만 쓰면 정보가 전달되지 않고, 장식용 이미지에 alt가 없으면 파일명을 읽어 노이즈가 됩니다. 의미 있는 이미지는 내용을 설명하고, 장식용은 alt=""로 처리하는 것이 원칙입니다.
Case 02
버튼 레이블 및 type 속성 — aria-label / type="button"
아이콘 버튼이나 맥락 없는 텍스트 버튼은 스크린 리더에 기능을 명확히 전달해야 합니다. 또한 type 속성이 없으면 폼 내부에서 의도치 않은 submit이 발생할 수 있습니다.
WCAG 4.1.2 — 이름, 역할, 값 (Level A)
Before — 문제 있는 코드
<!-- ❌ type 없음 + 레이블 없는 아이콘 버튼 -->
<button class="btn-close">
  <img src="icon-close.svg" alt="">
</button>

<!-- ❌ 맥락 없는 "더보기" 반복 -->
<button>더보기</button>
<button>더보기</button>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
type="button"을 명시하지 않으면 폼 내부에서 submit으로 동작할 수 있습니다. aria-label은 "더보기"가 반복될 때 각각 어떤 항목인지 맥락을 전달합니다.
Case 03
키보드 내비게이션 — 포커스 처리
모달, 레이어 팝업 등 JS로 동작하는 UI에서 포커스가 적절히 이동되지 않으면 키보드 사용자는 해당 기능을 사용할 수 없습니다.
WCAG 2.1.1 — 키보드 (Level A)
Before — 문제 있는 코드
<!-- ❌ 포커스 이동 없음 -->
<div class="modal" id="modal">
  모달 내용
  <button>닫기</button>
</div>

<script>
function openModal() {
  document.getElementById('modal')
    .style.display = 'block';
}
</script>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
모달이 열릴 때 포커스가 이동되지 않으면 키보드 사용자는 모달 뒤 배경 콘텐츠를 계속 탐색하게 됩니다. role="dialog"와 aria-modal="true"로 보조 기술에 모달임을 알리고, 열릴 때 포커스를 내부로 이동시켜야 합니다.
Case 04
폼 레이블 연결 — for / id
입력 필드와 레이블이 연결되지 않으면 스크린 리더가 어떤 필드인지 안내하지 못하고, 레이블 클릭 시 필드가 활성화되지 않습니다.
WCAG 1.3.1 — 정보와 관계 (Level A)
Before — 문제 있는 코드
<!-- ❌ span으로 레이블 처리 -->
<div class="form-group">
  <span class="label">이름</span>
  <input type="text"
         placeholder="이름을 입력하세요">
</div>

<!-- ❌ placeholder만 있고 label 없음 -->
<input type="email"
       placeholder="이메일 주소">
After — 개선된 코드
<!-- ✅ 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">
💡 개선 포인트
placeholder는 입력하면 사라지기 때문에 레이블을 대체할 수 없습니다. <label for>와 <input id>를 연결하면 스크린 리더가 필드 진입 시 레이블을 읽어주고, 레이블 클릭 시 해당 필드로 포커스가 이동합니다.
Case 05
숨김 텍스트 — visually-hidden (IR 기법)
화면에는 보이지 않지만 스크린 리더에 전달해야 하는 텍스트를 display:none으로 처리하면 보조 기술에서도 읽히지 않습니다.
WCAG 1.3.1 — 정보와 관계 (Level A)
Before — 문제 있는 코드
<!-- ❌ display:none → 스크린 리더도 못 읽음 -->
<style>
  .hidden { display: none; }
</style>

<a href="/cart">
  <img src="icon-cart.svg" alt="">
  <span class="hidden">장바구니</span>
</a>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
display:none은 스크린 리더에서도 콘텐츠를 숨깁니다. visually-hidden은 시각적으로 보이지 않지만 DOM에 존재해 보조 기술이 읽을 수 있습니다. 아이콘 링크, 건너뛰기 링크 등에 필수로 사용합니다.
Case 06
툴팁 — role="tooltip" / aria-describedby
마우스 hover로만 표시되는 툴팁은 키보드 사용자나 스크린 리더 사용자에게 전달되지 않습니다.
WCAG 1.3.1 — 정보와 관계 (Level A)
Before — 문제 있는 코드
<!-- ❌ 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>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
hover로만 표시되는 툴팁은 키보드 포커스 시에도 표시되어야 합니다. aria-describedby로 버튼과 툴팁을 연결하면 스크린 리더가 버튼 포커스 시 툴팁 내용을 함께 읽어줍니다.
Case 07
탭 UI — role="tablist" / aria-selected
탭 UI를 div나 button으로만 구현하면 스크린 리더가 탭 구조임을 인식하지 못합니다. ARIA role로 탭 구조를 명확히 전달해야 합니다.
WCAG 4.1.2 — 이름, 역할, 값 (Level A)
Before — 문제 있는 코드
<!-- ❌ div/button만으로 탭 구현 -->
<div class="tab-list">
  <button class="tab active">
    5G 요금제
  </button>
  <button class="tab">
    LTE 요금제
  </button>
</div>
<div class="tab-panel">
  5G 요금제 내용
</div>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
role="tablist/tab/tabpanel"을 함께 사용하면 스크린 리더가 "탭 1/2, 선택됨"과 같이 구조를 안내합니다. aria-selected로 현재 선택된 탭을, aria-controls로 연결된 패널을 명시합니다.
Case 08
아코디언 — aria-expanded 상태 전달
아코디언 버튼의 열림/닫힘 상태가 시각적으로만 표현되면 스크린 리더 사용자는 현재 상태를 알 수 없습니다.
WCAG 4.1.2 — 이름, 역할, 값 (Level A)
Before — 문제 있는 코드
<!-- ❌ 열림/닫힘 상태 전달 없음 -->
<div class="accordion">
  <button type="button"
          class="accordion-btn">
    자주 묻는 질문
  </button>
  <div class="accordion-panel">
    답변 내용
  </div>
</div>
After — 개선된 코드
<!-- ✅ 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');
💡 개선 포인트
aria-expanded="false/true"로 열림/닫힘 상태를 보조 기술에 전달합니다. 패널은 hidden 속성으로 제어하면 스크린 리더도 닫힌 상태에서는 내용을 읽지 않습니다.
Case 09
테이블 접근성 — caption / scope / thead
데이터 테이블에 제목과 헤더 정보가 없으면 스크린 리더가 각 셀이 어떤 항목에 해당하는지 파악할 수 없습니다.
WCAG 1.3.1 — 정보와 관계 (Level A)
Before — 문제 있는 코드
<!-- ❌ 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>
After — 개선된 코드
<!-- ✅ 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>
💡 개선 포인트
caption은 테이블 제목을 스크린 리더에 전달하고, scope="col/row"는 헤더 셀이 어느 방향의 데이터를 설명하는지 명시합니다. thead/tbody 구분으로 구조도 명확해집니다.
Case 10
팝업 vs 페이지 이동 링크 구분
새 창 또는 팝업으로 열리는 링크를 사전에 안내하지 않으면 스크린 리더 사용자가 예상치 못한 컨텍스트 전환을 경험하게 됩니다.
WCAG 3.2.2 — 입력 시 변화 (Level A)
Before — 문제 있는 코드
<!-- ❌ 새 창 안내 없음 -->
<a href="https://external.com"
   target="_blank">
  요금제 안내 바로가기
</a>

<!-- ❌ 팝업과 페이지 이동이 같은 형태 -->
<a href="#"
   onclick="openPopup()">
  자세히 보기
</a>
<a href="#section-plan">
  자세히 보기
</a>
After — 개선된 코드
<!-- ✅ 새 창 — 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>
💡 개선 포인트
페이지 이동은 <a href>, 팝업 호출은 <button>으로 구분하는 것이 원칙입니다. 새 창은 visually-hidden으로 사전 안내하고, aria-haspopup="dialog"로 팝업 호출임을 명시합니다.

개선 사례 요약

웹 접근성 개선 사례 요약 — WCAG 항목 및 핵심 기법
# 사례 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
← 포트폴리오로 돌아가기