웹 접근성은 모든 사용자가 웹 콘텐츠를 동등하게 이용할 수 있도록 보장하는 중요한 요소입니다. 오늘은 실제 구현된 이미지 갤러리 컴포넌트를 통해 접근성을 어떻게 개선했는지 살펴보겠습니다.

1. 시맨틱 HTML과 ARIA 속성 활용

적절한 시맨틱 요소 사용

<section
  className="relative rounded-lg overflow-hidden group"
  ref={containerRef}
  role="region" // 지워도 됨
  aria-label="이미지 갤러리"
  tabIndex={0}
>
  {/* 컴포넌트 내용 */}
</section>

여기서는 section 요소를 사용하고 role="region"aria-label을 추가하여 스크린 리더 사용자에게 이 영역이 무엇인지 명확히 알려줍니다.

role="region" : 직역으로 지역 역할 특정 영역의 랜드마크 역할을 합니다. 작성자가 중요하다고 생각하는 문서 영역을 식별하는 데 사용됩니다.

요소를 사용하면 접근 가능한 이름이 주어질 경우 섹션이 region 역할을 가지고 있음을 자동으로 전달합니다. 개발자는 항상 ARIA를 사용하는 것보다 이 경우

와 같은 올바른 의미론적 HTML 요소를 사용하는 것을 선호해야 합니다.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/region_role
공식 홈페이지에 가보니 다음과 같은 얘기가 있었다

해당 내용을 perplexity에 물어보니

aria-label="이미지 갤러리"가 접근 가능한 이름을 제공하므로,

요소는 자동으로 region 역할을 가지게 됩니다.
라는 답변을 듣게되어 수정하였다.

ARIA 속성으로 상태 정보 제공

<button
  onClick={() => setIsExpanded(!isExpanded)}
  aria-label="썸네일 보기"
  aria-expanded={isExpanded}
  aria-controls="thumbnails-panel"
  tabIndex={0}
>
  <PiImages />
</button>

<aside
  id="thumbnails-panel"
  ref={thumbnailPanelRef}
  className={`... ${isExpanded ? "block" : "hidden"}`}
  role="dialog"
  aria-label="썸네일 갤러리"
  aria-modal={isExpanded}
  aria-hidden={!isExpanded}
>
  {/* 썸네일 패널 내용 */}
</aside>

여기서 주목할 점:

  • aria-expanded: 버튼의 현재 확장 상태를 알려줍니다.
  • aria-controls: 이 버튼이 제어하는 요소의 ID를 지정합니다.
  • aria-modal: 모달 다이얼로그임을 알려줍니다.
  • aria-hidden: 패널이 숨겨져 있을 때 스크린 리더가 무시하도록 합니다.
  • aria-label : 때때로 요소의 기본 접근자 이름이 없는 경우, 또는 그 콘텐츠를 명확하게 설명하지 못한 경우, 또는 aria-labelledby 속성을 통해 참조되는 dom 안에 보이는 컨텐츠가 없는 경우에 씁니다. 해당 요소에게 접근자 이름을제공하게 됩니다.

2. 키보드 접근성 개선

탭 포커스 관리

<button
  ref={prevButtonRef}
  onClick={handlePrev}
  aria-label="이전 이미지"
  className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/60 text-white px-3 py-5 rounded-2xl hover:bg-black/80 focus:bg-black/80 z-10 cursor-pointer control-visibility"
  tabIndex={0}
  type="button"
>
  <GrPrevious aria-hidden="true" />
</button>

모든 인터랙티브 요소에 tabIndex={0}를 적용하여 키보드 탐색이 가능하도록 했습니다.

아이콘에 대한 접근성

<GrPrevious aria-hidden="true" />

아이콘은 순수하게 장식 목적이므로 aria-hidden="true"를 사용하여 스크린 리더가 읽지 않도록 합니다.

감싸고 있는 버튼에는 내가 따로 text를 주지 않았기 때문에 aria-label 속성을 달아주도록 하자

포커스 트래핑 구현

포커스 트래핑

모달 다이얼로그가 열려 있을 때 포커스가 그 안에 갇히도록 하는 기능:

포커스 트래핑(Focus Trapping) 코드 분석

이미지 뷰어에 구현된 포커스 트래핑은 접근성을 높이기 위한 중요한 기능입니다. 특히 모달이나 대화상자처럼 분리된 UI 요소에서 키보드 사용자가 적절히 인터페이스를 탐색할 수 있도록 도와줍니다.

코드 분석

// 포커스 트래핑
useEffect(() => {
  if (!isExpanded) return;

  const handleTabKey = (e: KeyboardEvent) => {
    if (e.key !== "Tab" || !thumbnailPanelRef.current) return;

    const focusableElements = thumbnailPanelRef.current.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[
      focusableElements.length - 1
    ] as HTMLElement;

    if (e.shiftKey) {
      if (document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
    } else {
      if (document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
  };

  document.addEventListener("keydown", handleTabKey);
  return () => {
    document.removeEventListener("keydown", handleTabKey);
  };
}, [isExpanded]);

작동 방식

  1. 조건부 실행: 썸네일 패널이 확장된 상태(isExpandedtrue)일 때만 포커스 트래핑이 활성화됩니다.
  2. 포커스 가능한 요소 선택:이 코드는 모달 내에서 포커스 가능한 모든 요소를 선택합니다. 여기에는 버튼, 링크, 입력 필드, 선택상자, 텍스트 영역 및 탭 인덱스가 설정된 요소가 포함됩니다(단, tabindex="-1"인 요소는 제외).
  3. const focusableElements = thumbnailPanelRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' );
  4. 첫 번째와 마지막 요소 식별:선택된 요소들 중 첫 번째와 마지막 요소를 식별합니다.
  5. const firstElement = focusableElements[0] as HTMLElement; const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
  6. 순환 포커스 처리:
    • Shift+Tab 키 조합 처리: 첫 번째 요소에서 Shift+Tab을 누르면 마지막 요소로 포커스가 이동합니다.
    • if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } }
    • Tab 키 처리: 마지막 요소에서 Tab을 누르면 첫 번째 요소로 포커스가 이동합니다.
    • else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } }
  7. 이벤트 정리: 컴포넌트가 언마운트되거나 isExpandedfalse로 바뀌면 이벤트 리스너를 제거합니다.

이 기능의 중요성

  1. 접근성 향상: 키보드 사용자가 모달 내에서 탭을 눌러 순환적으로 포커스를 이동할 수 있습니다.
  2. 사용자 경험 개선: 사용자가 모달 외부로 실수로 포커스를 이동시키는 것을 방지합니다.
  3. WCAG 준수: 웹 콘텐츠 접근성 지침(WCAG)의 요구사항을 충족시킵니다.

이 코드는 모달(썸네일 패널)이 열려 있을 때 키보드 포커스가 해당 모달 내에서만 순환하도록 보장하여, 키보드 사용자에게 더 나은 네비게이션 경험을 제공합니다.

3. 키보드 단축키 제공

useEffect(() => {
  const handleKeyPress = (e: KeyboardEvent) => {
    if (e.key === "ArrowLeft") {
      handlePrev();
    } else if (e.key === "ArrowRight") {
      handleNext();
    } else if (e.key === "f") {
      toggleFullscreen();
    } else if (e.key === "Escape" && isExpanded) {
      setIsExpanded(false);
    }
  };

  window.addEventListener("keydown", handleKeyPress);
  return () => {
    window.removeEventListener("keydown", handleKeyPress);
  };
}, [handlePrev, handleNext, toggleFullscreen, isExpanded]);

왼쪽/오른쪽 화살표 키, 'f' 키, ESC 키 등의 단축키를 제공하여 키보드 사용자의 편의성을 높였습니다.

4. 포커스 관리와 메모리

useEffect(() => {
  if (isExpanded) {
    lastFocusedElementRef.current = document.activeElement as HTMLElement;

    setTimeout(() => {
      if (closeButtonRef.current) {
        closeButtonRef.current.focus();
      }
    }, 100);
  } else {
    setTimeout(() => {
      if (lastFocusedElementRef.current) {
        lastFocusedElementRef.current.focus();
      }
    }, 100);
  }
}, [isExpanded]);

이 코드는:

  1. 모달이 열릴 때 현재 포커스된 요소를 기억합니다.
  2. 모달의 닫기 버튼에 포커스를 이동시킵니다.
  3. 모달이 닫힐 때 이전에 포커스되어 있던 요소로 포커스를 복원합니다.

5. 시각적 피드백과 상태 표시

현재 상태 알림

<div
  className="absolute bottom-4 left-4 bg-black/50 text-white px-3 py-1 rounded-full text-sm cursor-default control-visibility"
  aria-live="polite"
  role="status"
>
  {currentIndex + 1} / {totalImagesNumber}
</div>

aria-live="polite"를 사용하여 내용이 변경될 때 스크린 리더가 이를 알려주도록 합니다.

썸네일에 현재 선택 상태 표시

<div
  role="button"
  tabIndex={0}
  aria-label={`이미지 ${index + 1}${
    currentIndex === index ? " (현재 선택됨)" : ""
  }`}
>
  {/* 썸네일 내용 */}
</div>

현재 선택된 이미지에는 "(현재 선택됨)"이라는 텍스트를 aria-label에 추가하여 스크린 리더 사용자에게 현재 위치를 알려줍니다.

6. 컴포넌트 렌더링 순서의 중요성

return (
  <section>
    {/* 이전/다음 버튼 - 먼저 렌더링 */}
    <button ref={prevButtonRef}>...</button>
    <button ref={nextButtonRef}>...</button>

    {/* 컨트롤 버튼 */}
    <button onClick={() => setIsExpanded(!isExpanded)}>...</button>
    <button onClick={toggleFullscreen}>...</button>

    {/* 이미지 컨테이너 - 나중에 렌더링 */}
    <div>
      <TransformViwer ... />
    </div>

    {/* 기타 컴포넌트 */}
  </section>
);

여기서 중요한 점은 버튼들을 TransformViwer 컴포넌트보다 먼저 렌더링하는 것입니다. 이렇게 하면 DOM 순서상 버튼이 이미지 위에 위치하게 되어 키보드 탐색이 제대로 작동합니다.

7. CSS를 통한 접근성 개선

.control-visibility {
  @apply transition-opacity duration-150 opacity-0 group-hover:opacity-100 group-focus:opacity-100 group-focus-within:opacity-100 focus:opacity-100;
}

이 클래스는:

  • 컨테이너에 마우스를 올릴 때 컨트롤이 표시됩니다 (group-hover:opacity-100)
  • 컨테이너에 포커스가 있을 때 컨트롤이 표시됩니다 (group-focus:opacity-100)
  • 컨테이너 내부 요소에 포커스가 있을 때도 컨트롤이 표시됩니다 (group-focus-within:opacity-100)
  • 컨트롤 자체에 포커스가 있을 때도 표시됩니다 (focus:opacity-100)

결론

이 이미지 갤러리 컴포넌트는 다양한 접근성 기법을 구현하여 모든 사용자에게 동등한 경험을 제공합니다:

  1. 시맨틱 HTML과 ARIA 속성을 통한 구조적 접근성
  2. 키보드 탐색 지원과 포커스 관리
  3. 단축키를 통한 편리한 조작
  4. 상태 변화에 대한 적절한 알림
  5. 올바른 컴포넌트 렌더링 순서 고려
  6. CSS를 통한 시각적 접근성 강화

이러한 접근성 개선 사항들은 단순히 장애를 가진 사용자뿐만 아니라 키보드 사용자, 모바일 사용자 등 모든 사용자에게 더 나은 경험을 제공합니다.

+ Recent posts