Skip to content

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-labelaria-labelledby를 동시에 사용하지 않습니다. aria-labelledby가 우선합니다.

19.2.2. 라이브 리전

동적으로 변경되는 콘텐츠를 보조 기술에 알리기 위해 라이브 리전을 사용합니다.

속성동작
aria-livepolite사용자의 현재 작업이 끝난 후 변경 사항을 알림
aria-liveassertive즉시 변경 사항을 알림. 긴급한 알림에만 사용
aria-atomictrue / 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-errormessagearia-invalid="true"일 때만 참조 대상이 유효해야 합니다.

TIENIPIA QUALIFIED STANDARD