ARIA 속성
19.2.1. 기본 ARIA 속성
ARIA (Accessible Rich Internet Applications) 속성은 네이티브 HTML 요소만으로 의미를 전달할 수 없는 경우에 사용합니다. 네이티브 HTML 시맨틱으로 충분한 경우 ARIA 속성을 추가하지 않습니다.
| 속성 | 용도 | 사용 규칙 |
|---|---|---|
role | 요소의 역할 정의 | 네이티브 HTML 요소의 암시적 역할과 중복되지 않도록 사용 |
aria-label | 요소의 접근 가능한 이름 지정 | 시각적 레이블이 없는 요소에 사용 |
aria-labelledby | 다른 요소의 텍스트로 이름 지정 | 시각적 레이블이 있는 경우 해당 요소의 id를 참조 |
aria-describedby | 요소에 대한 추가 설명 연결 | 도움말, 힌트 텍스트 등을 연결할 때 사용 |
vue
<template>
<!-- aria-label: 시각적 텍스트가 없는 아이콘 버튼 -->
<button aria-label="검색" @click="handleSearch">
<SearchIcon />
</button>
<!-- aria-labelledby: 시각적 레이블을 참조 -->
<h2 id="section-title">사용자 목록</h2>
<table aria-labelledby="section-title">
<!-- 테이블 내용 -->
</table>
<!-- aria-describedby: 추가 설명 연결 -->
<input
id="password"
type="password"
aria-describedby="password-hint"
/>
<p id="password-hint">8자 이상, 영문과 숫자를 포함해야 합니다.</p>
</template><button>,<nav>,<header>등 네이티브 시맨틱 요소에 동일한 의미의role을 중복 지정하지 않습니다.aria-label과aria-labelledby를 동시에 사용하지 않습니다.aria-labelledby가 우선합니다.
19.2.2. 라이브 리전
동적으로 변경되는 콘텐츠를 보조 기술에 알리기 위해 라이브 리전을 사용합니다.
| 속성 | 값 | 동작 |
|---|---|---|
aria-live | polite | 사용자의 현재 작업이 끝난 후 변경 사항을 알림 |
aria-live | assertive | 즉시 변경 사항을 알림. 긴급한 알림에만 사용 |
aria-atomic | true / false | 리전 전체를 다시 읽을지, 변경된 부분만 읽을지 결정 |
vue
<script setup lang="ts">
import { ref } from 'vue'
const statusMessage = ref('')
const errorMessage = ref('')
async function handleSave() {
try {
await saveData()
statusMessage.value = '저장이 완료되었습니다.'
} catch {
errorMessage.value = '저장에 실패했습니다. 다시 시도해 주십시오.'
}
}
</script>
<template>
<!-- polite: 일반 상태 메시지 -->
<div aria-live="polite" aria-atomic="true">
{{ statusMessage }}
</div>
<!-- assertive: 긴급 오류 메시지 -->
<div aria-live="assertive" aria-atomic="true" role="alert">
{{ errorMessage }}
</div>
</template>aria-live="assertive"는 오류 알림, 세션 만료 경고 등 긴급한 경우에만 사용합니다.- 일반적인 상태 업데이트(로딩 완료, 데이터 갱신 등)에는
aria-live="polite"를 사용합니다. - 라이브 리전 요소는 페이지 로드 시점에 DOM에 존재해야 합니다. 동적으로 생성하면 보조 기술이 인식하지 못합니다.
19.2.3. 모달/대화상자
모달 대화상자는 다음 ARIA 속성과 포커스 관리 규칙을 준수해야 합니다.
vue
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const isOpen = ref(false)
const dialogRef = ref<HTMLElement | null>(null)
let previousFocus: HTMLElement | null = null
async function openModal() {
previousFocus = document.activeElement as HTMLElement
isOpen.value = true
await nextTick()
dialogRef.value?.focus()
}
function closeModal() {
isOpen.value = false
previousFocus?.focus()
}
</script>
<template>
<div
v-if="isOpen"
ref="dialogRef"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabindex="-1"
>
<h2 id="dialog-title">확인</h2>
<p>변경 사항을 저장하시겠습니까?</p>
<button @click="closeModal">취소</button>
<button @click="handleConfirm">저장</button>
</div>
</template>role="dialog"와aria-modal="true"를 반드시 지정해야 합니다.aria-labelledby로 대화상자의 제목 요소를 참조해야 합니다.- 모달이 열리면 모달 내부의 첫 번째 포커스 가능 요소 또는 대화상자 자체로 포커스를 이동해야 합니다.
- 모달이 닫히면 모달을 열기 전의 요소로 포커스를 복원해야 합니다.
- 모달이 열린 상태에서 포커스가 모달 외부로 이동하지 않도록 포커스 트래핑을 구현해야 합니다.
19.2.4. 폼 접근성
폼 요소는 다음 ARIA 속성을 사용하여 접근성을 보장해야 합니다.
| 속성 | 용도 | 적용 대상 |
|---|---|---|
aria-required | 필수 입력 필드 표시 | 필수 입력 필드 |
aria-invalid | 유효성 검증 실패 상태 표시 | 검증에 실패한 필드 |
aria-errormessage | 오류 메시지 요소 연결 | aria-invalid="true"인 필드 |
vue
<template>
<form @submit.prevent="handleSubmit">
<label for="user-email">이메일</label>
<input
id="user-email"
v-model="form.email"
type="email"
aria-required="true"
:aria-invalid="!!errors.email"
:aria-errormessage="errors.email ? 'email-error' : undefined"
@blur="validateField('email')"
/>
<p
v-if="errors.email"
id="email-error"
role="alert"
>
{{ errors.email }}
</p>
</form>
</template>- 모든 입력 필드에는
<label>요소를 연결해야 합니다.<label>의for속성과<input>의id속성을 일치시킵니다. placeholder를 레이블 대용으로 사용하지 않습니다.placeholder는 보조적인 힌트 텍스트로만 사용합니다.aria-invalid는 검증이 실행된 후에만true로 설정합니다. 초기 상태에서는 설정하지 않습니다.aria-errormessage는aria-invalid="true"일 때만 참조 대상이 유효해야 합니다.