웹 접근성 최상의 실천사례

빠른 요약

나쁜 ARIA보다는 ARIA가 없는 것이 더 나은 것을 고려하여, aria-*, role, 및 tabindex를 사용하기 전에 다음 권장 사항을 검토하세요. 웹 접근성을 지원하는 의미 있는 HTML을 사용하고, 이상적으로는 스크린 리더 및 브라우저의 관련 조합으로 테스트하세요.

WebAIM의 상위 백만 홈페이지에 대한 접근성 분석에서 “ARIA가 감지 가능한 오류와 상관 관계가 있다”고 발견되었습니다. ARIA의 오용이 증가된 오류의 큰 원인일 가능성이 높으므로, 의심스러울 경우 aria-*, role, 및 tabindex를 사용하지 말고 의미 있는 HTML을 사용하세요.

macOS에서 키보드 탐색 기능 활성화

기본적으로 macOS에서는 tab 키가 텍스트 상자 및 디렉터리만으로 제한됩니다. 전체 키보드 탐색을 활성화하려면:

  1. 시스템 환경설정을 엽니다.
  2. 키보드를 선택합니다.
  3. 단축키 탭을 엽니다.
  4. 설정에서 컨트롤 사이의 포커스를 이동하는 데 키보드 탐색 사용을 활성화합니다.

여기에서 브라우저별 키보드 탐색 기능을 활성화하는 방법에 대해 더 읽을 수 있습니다.

빠른 체크리스트

문서 개요 제공

스크린 리더 사용자가 콘텐츠를 탐색하는 데 사용하는 주요 메커니즘은 제목입니다. 따라서 페이지의 제목 구조는 좋은 차례로 콘텐츠를 탐색할 수 있도록 만들어져야 합니다. 다음을 보장해야 합니다:

  • 페이지에는 h1 요소가 하나만 있습니다.
  • 제목 수준이 건너뛰지 않습니다.
  • 제목 수준이 올바르게 중첩되어 있습니다.

스크린 리더용 접근 가능한 이름 제공

접근 가능한 이름으로 표시된 마크업을 제공하려면 모든 다음 사항이 충족되어야 합니다:

  • 입력에는 연관된 label이 있습니다.
  • 버튼 및 링크에는 가시 텍스트 또는 가시 텍스트가 없을 때는 aria-label이 있습니다(예: 내용이 없는 아이콘 버튼의 경우).
  • 이미지에는 alt 속성이 있습니다.
  • fieldset에는 첫 번째 자식으로 legend가 있습니다.
  • figure에는 첫 번째 자식으로 figcaption가 있습니다.
  • table에는 첫 번째 자식으로 caption이 있습니다.

체크박스 및 라디오 입력의 그룹은 fieldset 내에 묶여 있어야 하며, legend를 가지고 있어야 합니다. legend는 체크박스 및 라디오 입력 그룹에 라벨을 제공합니다.

만약 label, 자식 텍스트, 또는 자식 요소가 시각적으로 표시되지 않는 경우 .gl-sr-only를 사용하여 화면 리더를 제외한 모든 것에서 해당 요소를 숨깁니다.

접근 가능한 이름을 제공하는 예제

다음 하위 섹션에는 접근 가능한 이름으로 HTML 요소를 렌더링하는 마크업의 예제가 포함되어 있습니다.

GlFormGroup을 사용할 때:

  • label prop만 전달하면 label 값을 포함하는 legend가 있는 fieldset이 렌더링됩니다.
  • labellabel-for prop을 모두 전달하면 동일한 label-for ID를 가진 폼 입력을 가리키는 label이 렌더링됩니다.

접근 가능한 이름을 가진 텍스트 입력

GlFormGroup를 사용할 때, label prop만으로는 입력에 접근 가능한 이름이 제공되지 않습니다. 입력에 접근 가능한 이름을 제공하려면 label-for prop을 함께 제공해야 합니다.

텍스트 입력 예시:

<!-- 라벨이 있는 입력 -->
<gl-form-group :label="__('이슈 제목')" label-for="issue-title">
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

<!-- 숨겨진 라벨이 있는 입력 -->
<gl-form-group :label="__('이슈 제목')" label-for="issue-title" label-sr-only>
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

textarea 예시:

<!-- 라벨이 있는 텍스트 영역 -->
<gl-form-group :label="__('이슈 설명')" label-for="issue-description">
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

<!-- 숨겨진 라벨이 있는 텍스트 영역 -->
<gl-form-group :label="__('이슈 설명')" label-for="issue-description" label-sr-only>
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

또는 일반 label 요소를 사용할 수 있습니다:

<!-- `label`을 사용하여 라벨이 있는 입력 -->
<label for="issue-title">{{ __('이슈 제목') }}</label>
<gl-form-input id="issue-title" v-model="title" />

<!-- `label`을 사용하여 숨겨진 라벨이 있는 입력 -->
<label for="issue-title" class="gl-sr-only">{{ __('이슈 제목') }}</label>
<gl-form-input id="issue-title" v-model="title" />

접근 가능한 이름을 가진 선택 입력

선택 입력 예시:

<!-- 라벨이 있는 선택 입력 -->
<gl-form-group :label="__('이슈 상태')" label-for="issue-status">
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

<!-- 숨겨진 라벨이 있는 선택 입력 -->
<gl-form-group :label="__('이슈 상태')" label-for="issue-status" label-sr-only>
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

접근 가능한 이름을 가진 체크박스 입력

단일 체크박스:

<!-- 라벨이 있는 단일 체크박스 -->
<gl-form-checkbox v-model="status" value="task-complete">
  {{ __('작업 완료') }}
</gl-form-checkbox>

<!-- 숨겨진 라벨이 있는 단일 체크박스 -->
<gl-form-checkbox v-model="status" value="task-complete">
  <span class="gl-sr-only">{{ __('작업 완료') }}</span>
</gl-form-checkbox>

여러 체크박스:

<!-- fieldset으로 그룹화된 여러 라벨이 있는 체크박스 -->
<gl-form-group :label="__('작업 디렉터리')">
  <gl-form-checkbox value="task-1">{{ __('작업 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('작업 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('작업 디렉터리')">
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>

<!-- fieldset으로 그룹화된 여러 라벨이 있는 체크박스(숨겨진 legend와 함께) -->
<gl-form-group :label="__('작업 디렉터리')" label-sr-only>
  <gl-form-checkbox value="task-1">{{ __('작업 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('작업 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('작업 디렉터리')" label-sr-only>
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>
#### 접근 가능한 이름을 가진 라디오 입력란

단일 라디오 입력란:

```html
<!-- 라벨이 있는 단일 라디오 -->
<gl-form-radio v-model="status" value="opened">
  {{ __('Opened') }}
</gl-form-radio>

<!-- 숨겨진 라벨이 있는 단일 라디오 -->
<gl-form-radio v-model="status" value="opened">
  <span class="gl-sr-only">{{ __('Opened') }}</span>
</gl-form-radio>

다중 라디오 입력란:

<!-- fieldset 내에 그룹화된 여러 라벨이 있는 라디오 입력란 -->
<gl-form-group :label="__('이슈 상태')">
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('이슈 상태')">
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

<!-- 숨겨진 legend가 있는 fieldset 내에 그룹화된 여러 라벨이 있는 라디오 입력란 -->
<gl-form-group :label="__('이슈 상태')" label-sr-only>
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- 또는 -->
<gl-form-group :label="__('이슈 상태')" label-sr-only>
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

접근 가능한 이름을 가진 파일 입력란

파일 입력 예시:

<!-- 라벨이 있는 파일 입력란 -->
<label for="attach-file">{{ __('파일 첨부') }}</label>
<input id="attach-file" type="file" />

<!-- 숨겨진 라벨이 있는 파일 입력란 -->
<label for="attach-file" class="gl-sr-only">{{ __('파일 첨부') }}</label>
<input id="attach-file" type="file" />

GlToggle 구성요소에 접근 가능한 이름이 있는 것

GlToggle 예시:

<!-- 라벨이 있는 GlToggle -->
<gl-toggle v-model="notifications" :label="__('알림')" />

<!-- 숨겨진 라벨이 있는 GlToggle -->
<gl-toggle v-model="notifications" :label="__('알림')" label-position="hidden" />

GlFormCombobox 구성요소에 접근 가능한 이름이 있는 것

GlFormCombobox 예시:

<!-- 라벨이 있는 GlFormCombobox -->
<gl-form-combobox :label-text="__('키')" :token-list="$options.tokenList" />

접근 가능한 이름이 있는 이미지

이미지 예시:

<img :src="imagePath" :alt="__('이미지 설명')" />

<!-- SVG는 시멘틱하게 이미지로 구성되어 있으므로 `role="img"`를 적용해야 합니다 -->
<svg role="img" :alt="__('이미지 설명')" />

<!-- 장식용 이미지, 스크린 리더에 표시되지 않음 -->
<img :src="imagePath" :alt="" />

설명적인 접근 이름이 있는 버튼 및 링크

버튼과 링크는 독립적으로 이해할 수 있을 정도로 설명적인 접근 이름을 가져야 합니다.

<!-- 나쁜 예시 -->
<gl-button @click="handleClick">{{ __('제출') }}</gl-button>

<gl-link :href="url">{{ __('페이지') }}</gl-link>

<!-- 좋은 예시 -->
<gl-button @click="handleClick">{{ __('리뷰 제출') }}</gl-button>

<gl-link :href="url">{{ __("GitLab의 접근성 페이지") }}</gl-link>

버튼과 같은 스타일이 적용된 링크

링크는 GlButton을 사용하여 버튼과 같은 스타일을 적용할 수 있습니다.

 <gl-button :href="url">{{ __('버튼처럼 스타일이 적용된 링크') }}</gl-button>

역할

일반적으로 role을 사용하는 것을 피하세요. 암시적으로 role을 가지고 있는 의미론적인 HTML 요소를 사용하세요.

나쁜 예시 좋은 예시
<div role="button"> <button>
<div role="img"> <img>
<div role="link"> <a>
<div role="header"> <h1> to <h6>
<div role="textbox"> <input> 또는 <textarea>
<div role="article"> <article>
<div role="list"> <ol> 또는 <ul>
<div role="listitem"> <li>
<div role="table"> <table>
<div role="rowgroup"> <thead>, <tbody>, 또는 <tfoot>
<div role="row"> <tr>
<div role="columnheader"> <th>
<div role="cell"> <td>

키보드 전용 사용 지원

키보드 사용자는 페이지 상의 위치를 이해하기 위해 포커스 아웃라인에 의존합니다. 따라서 상호작용 가능한 요소는 다음이 보장되어야 합니다:

  • 키보드 포커스를 받을 수 있어야 합니다.
  • 시각적으로 포커스 상태가 보여져야 합니다.

기본적으로 이러한 동작을 제공하는 의미론적 HTML인 a(GlLink) 및 button(GlButton)을 사용하세요.

기억하세요:

  • TabShift-Tab은 상호작용 가능한 요소 간에만 이동해야 하며 정적 콘텐츠 사이에선 이동하면 안 됩니다.
  • :hover 스타일을 추가할 때 대부분의 경우 :focus 스타일도 추가하여 마우스와 키보드 사용자에게 스타일이 적용되도록 합니다.
  • 상호작용 요소의 outline을 제거하는 경우 box-shadow와 같은 다른 방법으로 시각적 포커스 상태를 유지해야 합니다.

자세한 내용은 파자마 키보드 전용 페이지를 참조하세요.

tabindex

tabindex 사용보다는 사용하지 않는 것을 선호하세요.

  • button(GlButton)과 같은 의미론적인 HTML을 사용하면 기본적으로 tabindex="0"을 제공합니다.
  • 탭 순서는 시각적으로 읽히는 순서와 일치해야 하며, 양수의 tabindex 값은 이러한 일치를 방해합니다.

tabindex="0"을 사용하여 요소를 상호작용 가능하게 만들지 마세요

divspan 태그 대신 상호작용 가능한 요소를 사용하세요. 예를 들어:

  • 요소를 클릭할 수 있어야 한다면 button(GlButton)을 사용하세요.
  • 텍스트를 편집할 수 있어야 한다면 input 또는 textarea을 사용하세요.

마크업이 의미론적으로 완성된 후 CSS를 사용하여 원하는 시각적 상태로 업데이트하세요.

<!-- 나쁜 예시 -->
<div role="button" tabindex="0" @click="expand">확장</div>

<!-- 좋은 예시 -->
<gl-button class="gl-p-0!" category="tertiary" @click="expand">확장</gl-button>

상호작용 요소에 tabindex="0"을 사용하지 마세요

상호작용 요소는 이미 탭으로 접근할 수 있으므로 tabindex를 추가하는 것은 중복되어 있습니다.

<!-- 나쁜 예시 -->
<gl-link href="help" tabindex="0">도움말</gl-link>
<gl-button tabindex="0">제출</gl-button>

<!-- 좋은 예시 -->
<gl-link href="help">도움말</gl-link>
<gl-button>제출</gl-button>

스크린 리더가 읽을 요소에 tabindex="0"을 사용하지 마세요

스크린 리더는 탭으로 접근이 가능하지 않은 텍스트를 읽을 수 있습니다. tabindex="0"을 사용하는 것은 불필요하며 문제를 일으킬 수 있습니다. 왜냐하면 스크린 리더 사용자들은 상호작용할 수 있기를 기대하기 때문입니다.

<!-- 나쁜 예시 -->
<p tabindex="0" :aria-label="message">{{ message }}</p>

<!-- 좋은 예시 -->
<p>{{ message }}</p>

양수의 tabindex를 사용하지 마세요

항상 tabindex="1” 이상을 사용하는 것을 피하세요. (https://webaim.org/techniques/keyboard/tabindex#overview)

장식용 아이콘들

아이콘은 UI에서 제거되었을 때 사용자에게 정보가 손실되지 않는 경우에 장식용입니다.

GitLab 내 대다수의 아이콘은 장식용이므로 GlIcon은 자동으로 스크린 리더에서 렌더링된 아이콘을 숨깁니다. 따라서 GlIconaria-hidden="true"를 추가할 필요가 없습니다. 이것은 중복된 작업입니다.

<!-- 불필요함 — gl-icon은 스크린 리더에서 아이콘을 기본적으로 숨김 -->
<gl-icon name="rocket" aria-hidden="true" />

<!-- 괜찮음 -->
<gl-icon name="rocket" />

정보 전달용 아이콘들

아이콘이 UI에서 제거되면 사용자에게 정보가 손실되는 경우, 해당 아이콘은 정보를 전달합니다.

예를 들어, 기밀 아이콘은 해당 이슈가 기밀임을 나타내며 “기밀”이라는 텍스트가 옆에 없는 경우입니다.

정보를 전달하는 아이콘은 스크린 리더 사용자에게도 정보가 전달되도록 접근 가능한 이름을 가져야 합니다.

<!-- 나쁨 -->
<gl-icon name="eye-slash" />

<!-- 좋음 -->
<gl-icon name="eye-slash" :aria-label="__('기밀 이슈')" />

클릭 가능한 아이콘들

클릭 가능한 아이콘들은 의미론적으로 버튼이므로 접근 가능한 이름과 함께 버튼으로 렌더링되어야 합니다.

<!-- 나쁨 -->
<gl-icon name="close" :aria-label="__('닫기')" @click="handleClick" />

<!-- 좋음 -->
<gl-button icon="close" category="tertiary" :aria-label="__('닫기')" @click="handleClick" />

툴팁

툴팁을 추가할 때, 키보드 사용자가 툴팁을 볼 수 있도록 툴팁이 포커스를 받을 수 있어야 합니다. 아이콘과 같은 정적인 요소인 경우, 이미 포커스를 받을 수 있는 버튼으로 감쌀 수 있으므로 아이콘에 tabindex=0을 추가할 필요가 없습니다.

다음 코드 조각은 툴팁이 있는 아이콘의 좋은 예시입니다.

  • 이미 버튼이기 때문에 자동으로 포커스를 받을 수 있습니다.
  • 텍스트가 없는 버튼이므로 접근 가능한 이름이 aria-label로 제공됩니다.
  • gl-hover-bg-transparent! 클래스를 사용하여 버튼이 호버될 때 회색으로 변하지 않도록 할 수 있습니다.
  • 필요한 경우 버튼의 여백을 제거하기 위해 gl-p-0! 클래스를 사용할 수 있습니다.
<gl-button
  v-gl-tooltip
  class="gl-hover-bg-transparent! gl-p-0!"
  icon="warning"
  category="tertiary"
  :title="tooltipText"
  :aria-label="__('경고')"
/>

요소 숨김

다음 테이블을 사용하여 적절한 경우 사용자에게 요소를 숨깁니다.

시각 장애가 없는 사용자에게 숨김 스크린 리더에서 숨김 시각 장애가 있는 사용자와 스크린 리더에서 숨김
.gl-sr-only aria-hidden="true" display: none, visibility: hidden, 또는 hidden 속성

시각 장애가 없는 사용자로부터 장식용 이미지 숨김

스크린 리더 사용자를 위해 잡음을 줄이기 위해 장식용 이미지는 alt=""를 사용하여 숨깁니다. img 요소가 아닌 경우, 인라인 SVG와 같은 이미지는 role="img"alt=""을 추가하여 숨길 수 있습니다.

gl-icon 컴포넌트는 자동으로 스크린 리더에서 아이콘을 숨기므로 aria-hidden="true"는 사용하지 않아도 됩니다.

<!-- 좋음 - 스크린 리더에서 장식용 이미지 숨김 -->

<img src="decorative.jpg" alt="">

<svg role="img" alt="" />

<gl-icon name="epic" />

ARIA 사용 시기

의미론적 HTML을 사용하는 경우 ARIA가 필요하지 않습니다. 왜냐하면 이미 접근성이 통합되어 있기 때문입니다.

그러나 의미론적 HTML과 동일한 UI 패턴이 없는 경우가 있습니다. 이에 대한 일반적인 예로는 대화 상자(모달)와 탭이 있습니다. GitLab 특정 예시로는 담당자와 라벨 드롭다운이 있습니다. 이러한 위젯을 구축하는 데는 스크린 리더 사용자가 이해할 수 있도록 ARIA가 필요합니다. WCAG 규정을 준수하기 위해 적절한 조사와 테스트가 수행되어야 합니다.