접근성(a11y)
웹 접근성(a11y)은 장애를 가진 사용자도 웹 콘텐츠를 인식하고 사용할 수 있게 만드는 것입니다. 법적 요구사항이기도 하고, 더 넓은 사용자에게 서비스를 제공하는 것이기도 합니다.
면접에서 "접근성을 고려해 개발한 경험이 있나요?"라고 물으면, 구체적인 ARIA 속성이나 키보드 네비게이션 구현 사례를 들 수 있으면 좋습니다.
시맨틱 HTML
<template>
<!-- 나쁜 예: div만으로 구성 -->
<div class="header">
<div class="nav">...</div>
</div>
<!-- 좋은 예: 시맨틱 태그 사용 -->
<header>
<nav aria-label="메인 네비게이션">
<ul>
<li><RouterLink to="/">홈</RouterLink></li>
<li><RouterLink to="/about">소개</RouterLink></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>제목</h1>
<section aria-labelledby="section-title">
<h2 id="section-title">섹션 제목</h2>
<p>본문</p>
</section>
</article>
</main>
<footer>
<p>저작권 정보</p>
</footer>
</template>
ARIA 속성 활용
<script setup lang="ts">
import { ref } from 'vue'
const isMenuOpen = ref(false)
const selectedTab = ref(0)
const tabs = ['개요', '상세', '리뷰']
</script>
<template>
<!-- 토글 버튼 -->
<button
:aria-expanded="isMenuOpen"
aria-controls="menu"
@click="isMenuOpen = !isMenuOpen"
>
메뉴 {{ isMenuOpen ? '닫기' : '열기' }}
</button>
<nav v-show="isMenuOpen" id="menu" role="menu">
<a role="menuitem" href="/">홈</a>
<a role="menuitem" href="/about">소개</a>
</nav>
<!-- 탭 패널 -->
<div role="tablist" aria-label="콘텐츠 탭">
<button
v-for="(tab, index) in tabs"
:key="tab"
role="tab"
:aria-selected="selectedTab === index"
:tabindex="selectedTab === index ? 0 : -1"
@click="selectedTab = index"
>
{{ tab }}
</button>
</div>
<div
v-for="(tab, index) in tabs"
:key="tab"
role="tabpanel"
:aria-labelledby="`tab-${index}`"
v-show="selectedTab === index"
>
{{ tab }} 내용
</div>
</template>
키보드 네비게이션
<script setup lang="ts">
import { ref } from 'vue'
const items = ref(['항목 1', '항목 2', '항목 3'])
const focusedIndex = ref(0)
const handleKeydown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
focusedIndex.value = Math.min(focusedIndex.value + 1, items.value.length - 1)
break
case 'ArrowUp':
event.preventDefault()
focusedIndex.value = Math.max(focusedIndex.value - 1, 0)
break
case 'Enter':
case ' ':
event.preventDefault()
selectItem(focusedIndex.value)
break
}
}
</script>
<template>
<ul role="listbox" @keydown="handleKeydown">
<li
v-for="(item, index) in items"
:key="item"
role="option"
:aria-selected="focusedIndex === index"
:tabindex="focusedIndex === index ? 0 : -1"
>
{{ item }}
</li>
</ul>
</template>
라이브 리전 — 동적 컨텐츠 알림
<template>
<!-- 스크린 리더에게 동적 변경사항을 알림 -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
{{ statusMessage }}
</div>
<!-- assertive — 즉시 알림 (긴급한 경우만) -->
<div aria-live="assertive" class="sr-only">
{{ errorMessage }}
</div>
</template>
<style>
/* 화면에는 보이지 않지만 스크린 리더는 읽을 수 있음 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
폼 접근성
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="email">이메일 (필수)</label>
<input
id="email"
type="email"
v-model="form.email"
aria-required="true"
:aria-invalid="!!errors.email"
:aria-describedby="errors.email ? 'email-error' : undefined"
/>
<span v-if="errors.email" id="email-error" role="alert">
{{ errors.email }}
</span>
</div>
</form>
</template>
SPA 라우트 전환 알림
// SPA에서 페이지 전환 시 스크린 리더에 알림
router.afterEach((to) => {
// 포커스를 메인 컨텐츠로 이동
nextTick(() => {
const main = document.querySelector('main')
main?.focus()
// 페이지 제목 변경 알림
document.title = (to.meta.title as string) || '앱 이름'
})
})
면접 팁
- 접근성은 선택이 아닌 필수 라는 인식을 보여주세요. 한국은 장애인차별금지법에 따라 웹 접근성 준수가 의무입니다
aria-label,aria-expanded,aria-live의 용도를 구체적으로 설명할 수 있으면 좋습니다- SPA에서의 접근성 과제(라우트 전환 시 포커스 관리, 동적 컨텐츠 알림)를 알고 있으면 실무 역량을 보여줄 수 있습니다
요약
Vue 앱의 접근성은 시맨틱 HTML을 기본으로, ARIA 속성으로 동적 UI의 상태를 전달하고, 키보드 네비게이션을 구현하는 것입니다. aria-live로 동적 변경을 알리고, SPA 라우트 전환 시 포커스를 관리하는 것이 핵심입니다.
댓글 로딩 중...