대용량 트래픽 성능 최적화 (4) - 가상화 환경에서의 접근성(a11y) 개선

hannah·2026년 2월 18일

TroubleShooting

목록 보기
9/9
post-thumbnail

렌더링 성능을 개선하기 위해 가상화를 도입하면서 예상치 못한 문제가 발생했다. 화면에 보이지 않는 수많은 카드들이 DOM에서 완전히 제거되면서 스크린 리더 사용자가 리스트의 전체 구조를 파악할 수 없게 된 것이다. 화면 밖의 카드에는 접근조차 불가능했고 컬럼과 카드 간의 관계도 모호해졌다.

이번 마지막 글에서는 시맨틱 HTML과 ARIA 속성을 활용해 가상화 환경에서 잃어버린 접근성을 다시 확보한 과정을 정리한다.

화면에 보이지 않는 정보 구조화하기

Accessibility 트리 구조

Chrome DevTools의 Accessibility 패널에서 확인한 컬럼의 ARIA 트리 구조. 왼쪽 트리에서 "할 일 컬럼, 334개의 카드"라는 region이 보이고, 오른쪽 패널에서는 role="region"과 aria-label 속성이 스크린 리더에 전달될 Name으로 변환되는 것을 확인할 수 있다.

가상화가 적용된 상태에서는 스크린 리더가 렌더링된 소수의 카드만 읽을 수 있다. 사용자는 현재 컬럼에 카드가 총 몇 개 있는지 알 방법이 없다. 이를 해결하기 위해 컬럼과 카드의 맥락을 명시했다.

// Column.tsx
<section
  role="region"
  aria-label={`${column.title} 컬럼, ${cards.length}개의 카드`}
>
  <header>
    <h2>{column.title}</h2>
    <span aria-live="polite">
      {cards.length}</span>
  </header>
  <div
    role="group"
    aria-label={`${column.title} 카드 목록`}
  >
    {/* 카드 리스트 */}
  </div>
</section>

컬럼을 의미 있는 구역으로 묶기 위해 role="region"을 부여했다. 스크린 리더가 컬럼 이름과 전체 카드 개수를 파악할 수 있도록 aria-label을 작성했고, 카드가 추가되거나 삭제될 때 변경 사항을 부드럽게 알리도록 aria-live="polite" 속성을 활용했다.

// Card.tsx
<article
  role="article"
  aria-label={`카드: ${card.title}${card.description ? `, ${card.description.slice(0, 30)}...` : ""}`}
  tabIndex={0}
>
  <h3>{card.title}</h3>
  {/* 카드 내용 */}
</article>

개별 카드에도 독립적인 콘텐츠임을 나타내는 role="article"을 적용했다. 제목과 설명을 요약해 전달하고 tabIndex={0}을 추가해 마우스 없이 키보드 포커스 이동이 가능하도록 개선했다.

마우스 의존성 낮추기

칸반 보드는 마우스 DnD 중심의 UI를 가진다. 키보드 사용자도 핵심 기능을 조작할 수 있도록 지원하는 작업이 필요했다.

드래그 가능한 카드에는 aria-label로 드래그 가능 여부를 명시하고, @dnd-kit이 제공하는 기본 속성들로 키보드 드래그를 지원했다.

카드 편집 모드에서는 사용성을 높이기 위해 단축키를 추가했다. 편집 모드 진입 시 autoFocus로 즉시 입력을 시작할 수 있게 만들고, Enter 키로 저장, Escape 키로 취소하는 흐름을 구현했다.

// PrioritySelect.tsx
<button
  aria-label={`우선순위 선택: ${currentLabel}`}
  aria-expanded={open}
  aria-haspopup="listbox"
>
  {/* ... */}
</button>

{open && (
  <div role="listbox" aria-label="우선순위 선택">
    {/* ... */}
    <button role="option" aria-selected={value === option.value}>
      {option.label}
    </button>
  </div>
)}

우선순위 설정에 사용한 커스텀 드롭다운 메뉴는 기본 태그가 아니어서 스크린 리더가 메뉴 구조를 인식하지 못한다. aria-haspopuprole="listbox" 등의 속성을 조합하여 네이티브 셀렉트 박스와 동일한 맥락을 제공했다.

인터랙션 요소와 상태 변화 알리기

화면의 상태 변화를 소리로 전달하고 아이콘 버튼들의 목적을 텍스트로 정의했다.

// LoadingOverlay.tsx
<div aria-live="polite" aria-busy="true">
  <div
    className="h-12 w-12 rounded-full border-4 border-kanban-border border-t-kanban-text animate-spin"
    aria-label="저장 중"
  />
</div>

데이터를 저장하거나 불러올 때 화면 밖의 작업 상태를 인지할 수 있도록 aria-busy 속성을 활용했다. 로딩 스피너에 aria-label="저장 중"을 부여하여 스크린 리더가 작업 진행 상태를 알 수 있도록 했다.

아이콘만 있는 추가 버튼이나 입력 필드에는 aria-label을 붙여 해당 요소의 역할을 확실히 부여했다. 예를 들어, 플러스 아이콘만 있는 카드 추가 버튼에는 aria-label="${column.title} 컬럼에 카드 추가"를 부여하여 스크린 리더가 버튼의 목적을 정확히 전달할 수 있도록 했다.

개선 결과와 남은 과제

lighthouse 결과

ARIA 속성 사용, 버튼/링크 접근 가능한 이름, 역할과 속성의 일치 등 21개 항목이 모두 통과하며 Lighthouse 접근성 감사 결과 100점을 달성했다.

접근성을 개선한 결과 스크린 리더 환경에서도 칸반 보드의 전체 구조를 파악하고 조작할 수 있게 되었다. 컬럼에 접근할 때 전체 카드 개수를 파악할 수 있고 상태 변화가 자동으로 안내된다. 키보드만으로도 카드 추가나 내용 수정이 가능해졌다.

DOM에 렌더링되지 않은 나머지 카드들을 스크린 리더로 직접 탐색하지 못하는 가상화 리스트의 근본적인 한계는 여전히 존재한다. 하지만 최대한 대용량 트래픽에 대비하여 렌더링 성능을 개선하는 동시에 다양한 방식을 적용하며 성능과 접근성의 균형을 맞추는 것이 중요하다는 점을 배울 수 있었다.

0개의 댓글