ARIA 접근성 가이드

컴포넌트 패턴

캐러셀(슬라이드)

여러 콘텐츠를 한 번에 하나씩 보여 주는 캐러셀을 만드는 방법입니다. 자동 넘김 정지·시작 버튼(WCAG 2.2.2), aria-roledescription, 상태에 따라 켜고 끄는 라이브 리전을 다룹니다.

언제 쓰나요

캐러셀(carousel)은 슬라이드라 부르는 여러 콘텐츠 묶음을 같은 자리에서 한 번에 하나씩 보여 주는 컴포넌트입니다. 홈 화면의 “롤링 배너”, 상품 이미지 슬라이드가 대표적입니다. 이전·다음 버튼으로 넘기거나, 일정 시간마다 자동으로 넘어가기도 합니다.

  • 캐러셀은 첫 슬라이드 외의 내용이 잘 읽히지 않고, 자동으로 바뀌는 내용은 읽기 느린 사용자·주의력 장애가 있는 사용자·화면 확대 사용자에게 큰 부담이 됩니다. 정말 필요한지 먼저 검토합니다.
  • 모든 내용을 한 화면에 목록이나 카드 그리드로 나열할 수 있다면 그쪽이 대부분의 사용자에게 더 낫습니다. 별도의 ARIA도 필요 없습니다.
  • 꼭 캐러셀을 쓴다면 자동 넘김은 기본으로 끄거나, 최소한 쉽게 찾을 수 있는 정지 버튼을 맨 앞에 둡니다.

자동 넘김에는 정지 수단이 필수입니다

WCAG 2.2.2는 자동으로 움직이고, 5초보다 오래 지속되며, 다른 내용과 함께 표시되는 콘텐츠에 일시 정지·정지·숨기기 수단을 요구합니다. 자동으로 넘어가는 캐러셀은 여기에 해당하므로, 정지 버튼 없이 자동으로 넘기면 수준 A 위반입니다.

구조

APG는 컨트롤 구성에 따라 두 가지 형태를 제시합니다.

  • 기본형: 정지·시작 버튼과 이전·다음 버튼으로 슬라이드를 넘깁니다.
  • 탭형: 슬라이드 선택 점(●○○)을 탭 패턴으로 만들어 원하는 슬라이드를 바로 고릅니다.

기본형의 구조는 다음과 같습니다.

  • 캐러셀 컨테이너: 랜드마크로 둘 만큼 중요하면 region(<section>에 이름을 주면 됨), 아니면 group을 씁니다. aria-roledescription="캐러셀"로 역할을 “캐러셀”이라고 안내하고, aria-label 또는 aria-labelledby로 이름을 줍니다. 이름에는 “캐러셀”이라는 말을 넣지 않습니다.
  • 슬라이드: 각 슬라이드에 group 역할과 aria-roledescription="슬라이드"를 줍니다. 슬라이드마다 고유한 제목이 없으면 aria-label="3개 중 1번째"처럼 위치를 이름으로 줍니다.
  • 정지·시작 버튼: 캐러셀 안에서 Tab 순서상 맨 앞에 둡니다. 상태 속성(aria-pressed) 대신 버튼 이름을 다음 동작에 맞게 바꿉니다(“자동 넘김 정지” ↔ “자동 넘김 시작”).
  • 이전·다음 버튼: 일반 <button>입니다. 눌러도 포커스가 옮겨 가지 않아 여러 번 연달아 누를 수 있습니다. 슬라이드 묶음을 aria-controls로 가리킬 수 있습니다.
  • 슬라이드 묶음(라이브 리전): 슬라이드들을 감싼 요소에 aria-live를 둡니다. 자동으로 넘어가는 동안에는 off, 멈춰 있을 때는 polite로 바꿔서, 사용자가 직접 넘길 때만 새 슬라이드를 읽게 합니다.
  • 현재 보이지 않는 슬라이드는 hidden(또는 display: none)으로 숨겨 Tab 순서와 가상 커서에서도 빠지게 합니다.
역할과 속성 관계
<section aria-roledescription="캐러셀" aria-label="이번 주 추천 강좌">
  <div class="controls">
    <button type="button">자동 넘김 정지</button>   <!-- 맨 앞 -->
    <button type="button" aria-controls="slides" aria-label="이전 슬라이드">‹</button>
    <button type="button" aria-controls="slides" aria-label="다음 슬라이드">›</button>
  </div>
  <div id="slides" aria-live="off">            <!-- 정지하면 polite -->
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 1번째">…</div>
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 2번째" hidden>…</div>
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 3번째" hidden>…</div>
  </div>
</section>

aria-roledescription은 조심해서 씁니다

aria-roledescription은 보조기술이 읽는 역할 이름을 사람이 읽는 말로 바꿉니다. 캐러셀처럼 ARIA에 해당 역할이 없는 위젯에서 의미를 보충할 때만 쓰고, 실제 역할(region, group)은 그대로 둡니다. 화면 언어와 같은 언어로 쓰고(한국어 페이지면 “캐러셀”, “슬라이드”), 버튼처럼 이미 잘 알려진 역할에는 쓰지 않습니다.

탭형 슬라이드 선택

점 모양 선택 버튼을 두려면 tablist·tab·tabpanel로 만듭니다. 탭에는 “슬라이드 1”처럼 이름을 주고, 각 탭 패널에 aria-roledescription="슬라이드"를 줍니다. 키보드 동작은 탭 패턴과 같고, 정지·시작 버튼은 여전히 맨 앞에 둡니다.

키보드 동작

키보드 동작
키동작
Tab / Shift + Tab캐러셀 안의 조작 요소(정지·시작 버튼 → 이전·다음 버튼 → 현재 슬라이드의 링크)를 페이지 Tab 순서대로 이동합니다. 별도 스크립트가 필요 없습니다.
Enter / Space포커스된 버튼을 실행합니다. 포커스는 버튼에 그대로 남습니다.
(포커스 진입)키보드 포커스가 캐러셀 안의 요소에 들어가면 자동 넘김이 멈춥니다. 사용자가 시작 버튼을 누르기 전에는 다시 시작하지 않습니다.
← / → (탭형)슬라이드 선택 탭 사이를 이동하고 해당 슬라이드를 보여 줍니다. Home·End로 처음·마지막 탭으로 갑니다.

마우스 포인터가 캐러셀 위에 있는 동안에도 자동 넘김을 멈춥니다. 포인터가 떠나면 원래 상태로 돌아갑니다.

필요한 ARIA

필요한 ARIA
역할·속성붙이는 곳설명
region 또는 group캐러셀 컨테이너캐러셀 전체를 하나의 영역으로 묶습니다. 이름 있는 <section>은 자동으로 region이 됩니다.
aria-roledescription컨테이너, 슬라이드“캐러셀”, “슬라이드”처럼 역할을 쉽게 안내합니다.
aria-label / aria-labelledby컨테이너, 슬라이드, 아이콘 버튼캐러셀 이름, 슬라이드 위치(“3개 중 1번째”), “이전 슬라이드” 같은 버튼 이름을 줍니다.
group각 슬라이드슬라이드 하나의 내용을 묶습니다. 탭형에서는 tabpanel을 씁니다.
aria-live슬라이드 묶음자동 넘김 중 off, 멈췄을 때 polite.
aria-controls이전·다음 버튼슬라이드 묶음 id를 가리킵니다. 선택 사항입니다.
tablist, tab, aria-selected탭형 슬라이드 선택점 모양 선택 버튼을 탭으로 만들 때 씁니다.

직접 해 보기

자동 넘김을 지원하는 기본형 캐러셀입니다. 슬라이드는 5초마다 넘어가고, 마우스를 올리면 잠시 멈춥니다. Tab으로 캐러셀 안에 들어가면 자동 넘김이 멈추고 버튼 이름이 “자동 넘김 시작”으로 바뀝니다. 실시간 코드에서 aria-live 값이 off와 polite로 바뀌는 모습을 확인하세요. 운영체제에서 “움직임 줄이기”를 켜 두었다면 처음부터 정지 상태로 시작합니다.

잘못된 구현(정지 버튼 없이 계속 넘어가는 캐러셀)은 그 자체로 이 페이지를 WCAG 2.2.2 위반으로 만들기 때문에 실행 데모 대신 아래 “전체 코드”에서 코드로만 비교합니다.

전체 코드

비권장비권장: 정지 수단 없이 자동 넘김
<div class="banner">
  <div class="slide active"><img src="a.jpg"></div>
  <div class="slide"><img src="b.jpg"></div>
  <span class="dot" onclick="go(0)"></span>
  <span class="dot" onclick="go(1)"></span>
</div>
<script>
  setInterval(next, 3000); // 멈출 방법이 없음
</script>
권장권장: 정지 버튼 + 역할 안내 + 라이브 리전
<section id="carousel" aria-roledescription="캐러셀" aria-labelledby="c-title">
  <h3 id="c-title">이번 주 추천 강좌</h3>
  <button type="button" id="rotate">자동 넘김 정지</button>
  <button type="button" id="prev" aria-controls="slides"
          aria-label="이전 슬라이드">‹</button>
  <button type="button" id="next" aria-controls="slides"
          aria-label="다음 슬라이드">›</button>
  <div id="slides" aria-live="off">
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 1번째">…</div>
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 2번째" hidden>…</div>
    <div role="group" aria-roledescription="슬라이드" aria-label="3개 중 3번째" hidden>…</div>
  </div>
</section>
JavaScript
var root = document.getElementById('carousel');
var live = document.getElementById('slides');
var slides = live.querySelectorAll('[role="group"]');
var rotateBtn = document.getElementById('rotate');
var current = 0, playing = false, hovering = false;

function show(i) {
  current = (i + slides.length) % slides.length;
  slides.forEach(function (s, k) { s.hidden = k !== current; });
}

function setPlaying(on) {
  playing = on;
  rotateBtn.textContent = on ? '자동 넘김 정지' : '자동 넘김 시작'; // 이름으로 상태 전달
  live.setAttribute('aria-live', on ? 'off' : 'polite');
}

rotateBtn.addEventListener('click', function () { setPlaying(!playing); });
document.getElementById('prev').addEventListener('click', function () { show(current - 1); });
document.getElementById('next').addEventListener('click', function () { show(current + 1); });

// 키보드 포커스가 들어오면 정지 (다시 시작은 사용자가 버튼으로)
root.addEventListener('focusin', function (e) {
  var fromOutside = !e.relatedTarget || !root.contains(e.relatedTarget);
  if (playing && (fromOutside || live.contains(e.target))) setPlaying(false);
});
// 마우스를 올린 동안 잠시 멈춤
root.addEventListener('mouseenter', function () { hovering = true; });
root.addEventListener('mouseleave', function () { hovering = false; });

var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
show(0);
setPlaying(!reduce);
setInterval(function () {
  if (playing && !hovering) show(current + 1);
}, 5000);

화면낭독기에서는

화면낭독기 안내 예시 (표현은 화면낭독기·브라우저·버전에 따라 조금씩 다릅니다)
상황들리는 안내 (예)
Tab으로 캐러셀에 들어감이번 주 추천 강좌, 캐러셀, 자동 넘김 정지, 버튼 → 포커스가 들어가며 자동 넘김이 멈추고 버튼 이름이 바뀜
정지·시작 버튼에 다시 포커스자동 넘김 시작, 버튼
정지 상태에서 “다음 슬라이드” 누름3개 중 2번째, 슬라이드, 키보드 포커스 관리, … (aria-live="polite"로 새 슬라이드를 읽음)
자동 넘김 중 슬라이드가 바뀜(아무것도 읽지 않음 — aria-live="off")
가상 커서로 슬라이드 내용 진입3개 중 1번째, 슬라이드, 접근 가능한 이름 이해하기, 제목 수준 4

점검 체크리스트

  • 자동으로 넘어가는 캐러셀에 정지·시작 버튼이 있고, 캐러셀 안에서 Tab 순서상 맨 앞에 있습니다.
  • 정지·시작 버튼의 이름이 다음 동작(“정지” / “시작”)에 맞게 바뀝니다.
  • 키보드 포커스가 캐러셀 안으로 들어오면 자동 넘김이 멈추고, 사용자가 시작하기 전에는 다시 시작하지 않습니다.
  • 마우스를 캐러셀 위에 올린 동안 자동 넘김이 멈춥니다.
  • 컨테이너에 이름과 aria-roledescription="캐러셀", 슬라이드에 role="group"과 aria-roledescription="슬라이드", 위치나 제목을 담은 이름이 있습니다.
  • 슬라이드 묶음의 aria-live가 자동 넘김 중 off, 정지 중 polite입니다.
  • 보이지 않는 슬라이드는 숨겨져 있어 그 안의 링크로 Tab 포커스가 가지 않습니다.
  • 이전·다음 같은 아이콘 버튼에 이름이 있고, 눌러도 포커스가 버튼에 남습니다.
  • “움직임 줄이기”(prefers-reduced-motion) 설정을 존중해 자동 넘김이나 전환 애니메이션을 끕니다.
  • 각 슬라이드 링크가 “자세히 보기”만으로 끝나지 않고 어떤 내용인지 알 수 있습니다.

관련 WCAG 성공 기준

참고 자료