웹 접근성은 모든 사용자가 웹 콘텐츠를 동등하게 이용할 수 있도록 보장하는 중요한 요소입니다. 오늘은 실제 구현된 이미지 갤러리 컴포넌트를 통해 접근성을 어떻게 개선했는지 살펴보겠습니다.
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]);
작동 방식
- 조건부 실행: 썸네일 패널이 확장된 상태(
isExpanded
가true
)일 때만 포커스 트래핑이 활성화됩니다. - 포커스 가능한 요소 선택:이 코드는 모달 내에서 포커스 가능한 모든 요소를 선택합니다. 여기에는 버튼, 링크, 입력 필드, 선택상자, 텍스트 영역 및 탭 인덱스가 설정된 요소가 포함됩니다(단,
tabindex="-1"
인 요소는 제외). 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;
- 순환 포커스 처리:
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(); } }
- 이벤트 정리: 컴포넌트가 언마운트되거나
isExpanded
가false
로 바뀌면 이벤트 리스너를 제거합니다.
이 기능의 중요성
- 접근성 향상: 키보드 사용자가 모달 내에서 탭을 눌러 순환적으로 포커스를 이동할 수 있습니다.
- 사용자 경험 개선: 사용자가 모달 외부로 실수로 포커스를 이동시키는 것을 방지합니다.
- 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]);
이 코드는:
- 모달이 열릴 때 현재 포커스된 요소를 기억합니다.
- 모달의 닫기 버튼에 포커스를 이동시킵니다.
- 모달이 닫힐 때 이전에 포커스되어 있던 요소로 포커스를 복원합니다.
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
)
결론
이 이미지 갤러리 컴포넌트는 다양한 접근성 기법을 구현하여 모든 사용자에게 동등한 경험을 제공합니다:
- 시맨틱 HTML과 ARIA 속성을 통한 구조적 접근성
- 키보드 탐색 지원과 포커스 관리
- 단축키를 통한 편리한 조작
- 상태 변화에 대한 적절한 알림
- 올바른 컴포넌트 렌더링 순서 고려
- CSS를 통한 시각적 접근성 강화
이러한 접근성 개선 사항들은 단순히 장애를 가진 사용자뿐만 아니라 키보드 사용자, 모바일 사용자 등 모든 사용자에게 더 나은 경험을 제공합니다.
'개발 > HTML, CSS' 카테고리의 다른 글
Tailwind css 동적 클래스 문제 해결 (0) | 2025.03.27 |
---|---|
Tab 인덱스가 작동하지 않은 원인: css의 중요성 (0) | 2025.03.22 |
엘리먼트를 안 보이게 하는 다양한 방식과 각각의 장단점 (0) | 2025.03.22 |