접근성

contenteditable 에디터를 위한 접근성 고려사항 및 모범 사례입니다.

개요

contenteditable을 접근 가능하게 만드는 것은 어렵습니다. 스크린 리더가 변경사항을 제대로 알리지 않을 수 있고, ARIA 속성이 존중되지 않을 수 있으며, 키보드 탐색이 일관되지 않을 수 있습니다. 이 가이드는 접근성 문제와 해결책을 다룹니다.

contenteditable의 접근성 도전과제:

  • 스크린 리더가 콘텐츠 변경을 자동으로 알리지 않음
  • ARIA 속성이 제대로 알려지지 않을 수 있음 (특히 Safari)
  • 키보드 탐색이 일관되지 않을 수 있음
  • 포커스 관리가 복잡함
  • 서식 변경이 알려지지 않음

스크린 리더 지원

문제: contenteditable 영역에서 콘텐츠가 변경될 때(텍스트 입력, 삭제, 서식 적용), 스크린 리더가 이러한 변경사항을 사용자에게 알리지 않습니다. 이로 인해 보조 기술에 의존하는 사용자가 에디터에서 무슨 일이 일어나고 있는지 이해하기 어렵습니다.

// ❌ 나쁨: 콘텐츠 변경에 대한 알림 없음
<div contenteditable>
  <!-- 스크린 리더에 변경사항이 알려지지 않음 -->
</div>

// ✅ 좋음: 알림을 위해 aria-live 영역 사용
<div contenteditable
     role="textbox"
     aria-label="리치 텍스트 에디터"
     aria-live="polite"
     aria-atomic="false">
  <!-- 콘텐츠 -->
</div>

// 또는 별도의 라이브 영역 사용
<div id="announcements" 
     aria-live="polite" 
     aria-atomic="false"
     class="sr-only">
</div>

function announceChange(message) {
  const announcements = document.getElementById('announcements');
  announcements.textContent = message;
  // 알림 후 지우기
  setTimeout(() => {
    announcements.textContent = '';
  }, 1000);
}
// 예제: 서식 변경 알림
element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    e.preventDefault();
    applyBold();
    announceChange('굵게 서식이 적용되었습니다');
  } else if (e.inputType === 'formatRemove') {
    e.preventDefault();
    removeFormatting();
    announceChange('서식이 제거되었습니다');
  }
});

// 예제: 선택 영역 변경 알림
document.addEventListener('selectionchange', () => {
  const selection = window.getSelection();
  if (selection.rangeCount > 0) {
    const range = selection.getRangeAt(0);
    if (!range.collapsed) {
      const text = range.toString();
      const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
      announceChange(`${wordCount}개 단어가 선택되었습니다`);
    }
  }
});

ARIA 속성

문제: contenteditable 영역에 ARIA 속성(role, aria-label, aria-describedby)을 적용할 때, 스크린 리더가 이를 제대로 알리지 않을 수 있으며, 특히 Safari에서 그렇습니다. 접근성 정보가 손실됩니다.

// ❌ 나쁨: Safari에서 ARIA 속성이 알려지지 않을 수 있음
<div contenteditable role="textbox" aria-label="에디터">
  <!-- ARIA 정보가 손실될 수 있음 -->
</div>

// ✅ 좋음: 여러 ARIA 속성 사용 및 스크린 리더 간 테스트
<div contenteditable
     role="textbox"
     aria-label="리치 텍스트 에디터"
     aria-describedby="editor-help"
     aria-multiline="true"
     aria-haspopup="false"
     tabindex="0">
  <!-- 콘텐츠 -->
</div>
<div id="editor-help" class="sr-only">
  텍스트 서식을 위해 키보드 단축키를 사용하세요. Ctrl+B는 굵게, Ctrl+I는 기울임꼴입니다.
</div>

키보드 탐색

문제: 여러 contenteditable 영역에 tabindex 속성이 있을 때, 일부 브라우저에서 탭 순서가 tabindex 값을 올바르게 따르지 않을 수 있습니다. 포커스 순서가 일관되지 않거나 잘못될 수 있습니다.

// ❌ 나쁨: tabindex가 올바르게 작동하지 않을 수 있음
<div contenteditable tabindex="3">세 번째</div>
<div contenteditable tabindex="1">첫 번째</div>
<div contenteditable tabindex="2">두 번째</div>
// 포커스 순서가 잘못될 수 있음!

// ✅ 좋음: 순차적 tabindex 사용 또는 프로그래밍 방식으로 포커스 관리
<div contenteditable tabindex="0">첫 번째</div>
<div contenteditable tabindex="0">두 번째</div>
<div contenteditable tabindex="0">세 번째</div>
// 자연스러운 DOM 순서

포커스 관리

문제: 페이지 로드 시 폼 입력을 자동으로 포커스하는 autofocus 속성은 contenteditable 요소에서 작동하지 않습니다. 페이지가 로드될 때 contenteditable 영역을 자동으로 포커스하는 내장 방법이 없습니다.

// ❌ 나쁨: contenteditable에서 autofocus가 작동하지 않음
<div contenteditable autofocus>
  <!-- 자동으로 포커스를 받지 않음 -->
</div>

// ✅ 좋음: JavaScript를 사용하여 포커스
<div contenteditable id="editor">
  <!-- 콘텐츠 -->
</div>

<script>
  // 페이지 로드 시 포커스
  window.addEventListener('load', () => {
    const editor = document.getElementById('editor');
    editor.focus();
  });
  
  // 또는 더 나은 타이밍을 위해 requestAnimationFrame 사용
  requestAnimationFrame(() => {
    editor.focus();
  });
</script>

라이브 알림

// 전용 라이브 영역 생성
<div id="live-region" 
     aria-live="polite" 
     aria-atomic="false"
     class="sr-only">
</div>

// 변경사항 알림
function announce(message, priority = 'polite') {
  const liveRegion = document.getElementById('live-region');
  liveRegion.setAttribute('aria-live', priority);
  liveRegion.textContent = message;
  
  // 알림 후 지우기
  setTimeout(() => {
    liveRegion.textContent = '';
  }, 1000);
}

// 서식 알림
element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    announce('굵게 서식이 적용되었습니다');
  } else if (e.inputType === 'formatItalic') {
    announce('기울임꼴 서식이 적용되었습니다');
  }
});

// 선택 영역 알림
document.addEventListener('selectionchange', debounce(() => {
  const selection = window.getSelection();
  if (selection.rangeCount > 0) {
    const range = selection.getRangeAt(0);
    if (!range.collapsed) {
      const text = range.toString();
      const words = text.split(/\s+/).filter(w => w.length > 0);
      announce(`${words.length}개 단어가 선택되었습니다`);
    }
  }
}, 300));

스크린 리더 전용 CSS:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

플랫폼별 문제

브라우저 및 스크린 리더별 접근성 문제:

  • Safari: contenteditable 요소의 ARIA 속성이 스크린 리더(VoiceOver)에 의해 제대로 알려지지 않을 수 있음. 해결책으로 라이브 영역 사용
  • Chrome/Edge: 일반적으로 더 나은 ARIA 지원이지만, 동적 콘텐츠 변경에는 여전히 라이브 영역 사용
  • Firefox: 키보드 탐색이 다르게 동작할 수 있음. NVDA로 철저히 테스트
  • VoiceOver: 콘텐츠 변경을 자동으로 알리지 않을 수 있음. 모든 중요한 변경에 aria-live 영역 사용
  • NVDA: 일반적으로 더 나은 지원이지만, 여전히 라이브 영역으로 테스트

Related Pages

에디터 아키텍처

에디터 아키텍처 개요

입력 처리 & IME

입력 처리 가이드

모바일 지원

모바일 지원 가이드