next js 컴포넌트에서 사용 가능한 접근성을 고려한 BottomSheetAccessibilityManager 컴포넌트 구현 가이드
next js 컴포넌트에서 사용 가능한 접근성을 고려한 BottomSheetAccessibilityManager 컴포넌트 구현 가이드
안녕하세요, 엔비전스 입니다.
바텀시트는 화면의 하단에서 나타나는 UI 요소로, 사용자와의 상호작용을 위해 정보를 표시하거나 작업을 제공하는 데 사용됩니다. 바텀시트가 나타나면 기존 콘텐츠가 완전히 숨겨지지 않고 흐릿하게 보이게 되는데, 이는 바텀시트와의 상호작용이 끝난 후 사용자가 즉��� 원래 콘텐츠로 복귀할 수 있도록 자연스러운 전환을 제공하기 위함입니다. 이 접근 방식은 화면의 맥락을 유지하면서도 사용자가 현재 작업에 집중하도록 돕는 데 효과적입니다.
그러나 이와 같은 bottom sheet 구현 방법은 접근성 측면에서 몇 가지 문제를 일으킵니다. 첫째, 키보드 및 스크린 리더에서는 바텀시트를 포함한 모든 영역에 다 동일하게 접근되기 때문에 흐릿하게 비활성화 되어 보여지는 배경 요소들과 바텀시트를 명확히 구분하기 어려워 문제가 있습니다. 둘째, 바텀시트가 화면에 나타나더라도 바텀시트 영역으로 키보드 초점이 이동하지 않아 콘텐츠가 변경되었는지조차 알 수 없는 문제가 있습니다. 마지막으로, 바텀시트를 닫았을 때 포커스가 기존 요소로 돌아오지 않아 연속적인 탐색이 불가능해지는 문제가 발생합니다. 이러한 문제들은 접근성을 개선하기 위해 반드시 해결해야 합니다.
따라서 요즘 next JS가 웹 개발에서 많이 사용되고 있는 점을 감안하여 bottomSheet 구현 시 나타나는 접근성 문제를 해결할 수 있는 컴포넌트를 개발하여 소개하려 합니다.
BottomSheetAccessibilityManager의 특징
1. 외부 요소 접근 제어
- inert 속성을 사용하여 바텀시트 외부의 모든 요소들에 대한 접근을 차단합니다.
- 스크린 리더 사용자가 바텀시트 내부 콘텐츠에만 집중할 수 있도록 돕습니다.
2. 포커스 관리
- 바텀시트가 열릴 때 지정된 요소(initialFocusElementId)로 포커스가 자동 이동합니다.
- 지정된 요소가 없다면 첫 번째 포커스 가능한 요소로 이동합니다.
- 바텀시트가 닫힐 때 이전 포커스 위치로 자연스럽게 복귀합니다.
3. 쉬운 적용
- 기존 바텀시트 컴포넌트를 감싸는 것만으로도 접근성 개선이 가능합니다.
- Next.js 환경에서 SSR을 고려한 안전한 구현을 제공합니다.
BottomSheetAccessibilityManager 적용하기
1. 컴포넌트 추가 및 import
BottomSheetAccessibilityManager.tsx 파일을 다운로드하여 프로젝트의 components 폴더에 추가하고 import 합니다.
import { BottomSheetAccessibilityManager } from '@/components/BottomSheetAccessibilityManager';
2. 상태 분리하기
바텀시트가 닫힐 때 inert 속성을 안전하게 제거하기 위해서는 상태를 분리해야 합니다. 컴포넌트가 즉시 언마운트되면 cleanup 함수가 제대로 실행되지 않아 inert 속성이 남게 되므로, 정리 작업을 위한 시간이 필요합니다.
// 기존 코드
const [isOpen, setIsOpen] = useState(false);
// 수정된 코드
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false); // 마운트 상태
const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(false); // 시각적 표시 상태
3. 열기/닫기 핸들러 수정
const handleOpen = () => {
setIsBottomSheetOpen(true); // 먼저 마운트
setIsBottomSheetVisible(true); // 그 다음 표시
};
const handleClose = useCallback(() => {
setIsBottomSheetVisible(false); // 먼저 시각적으로 숨김
setTimeout(() => {
setIsBottomSheetOpen(false); // 300ms 후 언마운트
}, 300); // inert 속성 제거를 위한 시간
}, []);
4. BottomSheetAccessibilityManager 적용
{isBottomSheetOpen && (
<BottomSheetAccessibilityManager
isOpen={isBottomSheetOpen}
bottomSheetId="my-bottom-sheet"
>
<div
id="my-bottom-sheet"
role="dialog"
aria-modal="true"
aria-labelledby="bottom-sheet-title"
className={`bottom-sheet ${isBottomSheetVisible ? 'visible' : 'hidden'}`}
>
<h2 id="bottom-sheet-title">바텀시트 제목</h2>
{/* 바텀시트 내용 */}
</div>
</BottomSheetAccessibilityManager>
)}
5. 초기 포커스 설정 (선택사항)
특정 요소에 초기 포커스를 주고 싶은 경우 initialFocusElementId를 설정할 수 있습니다.
<BottomSheetAccessibilityManager
isOpen={isBottomSheetOpen}
bottomSheetId="my-bottom-sheet"
initialFocusElementId="close-button"
>
<div id="my-bottom-sheet" role="dialog" aria-modal="true">
<button id="close-button" onClick={handleClose}>
닫기
</button>
{/* 바텀시트 내용 */}
</div>
</BottomSheetAccessibilityManager>
6. 전체 적용 예시
const BottomSheet = () => {
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(false);
const handleClose = useCallback(() => {
setIsBottomSheetVisible(false);
setTimeout(() => {
setIsBottomSheetOpen(false);
}, 300);
}, []);
return (
<>
{isBottomSheetOpen && (
<BottomSheetAccessibilityManager
isOpen={isBottomSheetOpen}
bottomSheetId="my-bottom-sheet"
initialFocusElementId="close-button"
>
<div
id="my-bottom-sheet"
role="dialog"
aria-modal="true"
aria-labelledby="bottom-sheet-title"
className={`bottom-sheet ${isBottomSheetVisible ? 'visible' : 'hidden'}`}
>
<h2 id="bottom-sheet-title">바텀시트 제목</h2>
<button id="close-button" onClick={handleClose}>
닫기
</button>
{/* 바텀시트 내용 */}
</div>
</BottomSheetAccessibilityManager>
)}
</>
);
};
이렇게 적용하면 바텀시트의 접근성이 자동으로 관리되며, 스크린리더 사용자들도 원활하게 바텀시트를 이용할 수 있습니다.