-컴포넌트 마크업부터 초점 제어까지-
1. 들어가며: 정적 HTML과 React 환경의 접근성 차이
SI(System Integration) 프로젝트의 마크업 환경이 JSP 및 정적 HTML 중심에서 React, Vue 기반의 SPA(Single Page Application) 구조로 전환되면서, 퍼블리셔가 직면하는 가장 큰 기술적 과제 중 하나는 바로 웹 접근성(Web Accessibility, A11y) 품질 인증 심사입니다.
정적 마크업 환경에서는 웹 표준에 맞추어 시맨틱 태그를 바르게 작성하고 유효성 검사를 통과하면 대부분의 인증 기준을 충족할 수 있었습니다. 그러나 가상 DOM과 동적 상태(State) 기반으로 렌더링되는 React 환경에서는 동일한 수준으로 마크업을 작성하더라도 다음과 같은 접근성 결함이 빈번하게 지적됩니다.
-
모달 레이어 팝업이 활성화되었을 때, 키보드 탭(Tab) 키 초점이 뒷배경 본문으로 빠져나가는 현상이 발생합니다.
-
라우팅을 통해 화면 콘텐츠가 갱신되었음에도 화면 낭독기(스크린 리더)가 변경 사실을 인지하지 못하고 침묵합니다.
-
버튼 클릭으로 아코디언 패널이 열렸으나, 보조 기기에는 열림/닫힘 상태가 음성으로 전달되지 않습니다.
-
아이콘 전용 버튼에 스타일을 적용해 숨긴 텍스트를 스크린 리더가 통째로 건너뛰어 버립니다.
대다수 SI 사업은 연말 오픈을 앞둔 10~11월에 웹 접근성 품질 인증 검수가 집중됩니다. 이 시기에 초점 제어나 ARIA 속성 연동 결함이 발생하면 단순 마크업 수정을 넘어 컴포넌트의 상태 설계 자체를 변경해야 하므로 심각한 납기 지연이 초래됩니다.
따라서 컴포넌트 제작 단계에서부터 필수 규칙을 마크업에 반영하는 작업이 반드시 선행되어야 합니다.
2. React JSX 환경의 필수 접근성 문법 규칙
React의 JSX(JavaScript XML)는 브라우저 DOM을 직접 조작하지 않고 가상 DOM 객체를 생성하므로, 표준 HTML 작성 방식과 구별되는 명확한 문법적 차이를 숙지해야 합니다.
2.1. aria-* 속성의 하이픈(kebab-case) 유지
JSX에서는 예약어 충돌 방지를 위해 대다수 HTML 어트리뷰트를 카멜 케이스로 표기합니다(className, htmlFor, tabIndex). 그러나 WAI-ARIA 규격인 aria-* 속성과 데이터 속성인 data-*는 웹 표준 규격을 준수하기 위해 반드시 하이픈 형태(kebab-case)를 유지해야 합니다. 이를 카멜 케이스로 잘못 표기하면 브라우저가 유효하지 않은 속성으로 판단하여 스크린 리더가 이를 해석하지 못합니다.
// ❌ 잘못된 작성 방식: 속성명 오타 및 role 남발
<div tabindex="0" role="button" ariaHidden="false" onClick={handleClick}>
상세 보기
</div>
// ⭕ 올바른 작성 방식: 네이티브 버튼 태그 및 웹 표준 하이픈 표기
<button type="button" tabIndex={0} aria-hidden="false" onClick={handleClick}>
상세 보기
</button>
<button> 태그를 사용하면 키보드 Enter 및 Space 키 인터랙션이 브라우저 엔진에 의해 기본 지원되므로, <div> 태그에 별도의 키보드 이벤트 리스너를 구현할 필요가 없습니다.
2.2. 불필요한 <div> 래퍼와 시맨틱 구조 단절 방지
React 컴포넌트는 단일 루트 노드를 반환해야 합니다. 이때 관습적으로 컴포넌트 최상단을 <div className="wrapper">로 감싸게 되면 시맨틱 계층 구조가 손상됩니다.
특히 <ul>, <ol>의 직계 자식으로 <li>가 오지 않거나, <dl>의 직계 자식으로 <dt>, <dd>가 오지 못하고 <div>가 개입하면 스크린 리더는 목록 항목의 총개수와 현재 위치를 올바르게 계산하지 못합니다.
// ❌ <ul> 직하위에 <div>가 끼어들어 리스트 시맨틱이 단절된 형태
const Breadcrumb = () => (
<ul className="breadcrumb">
<div className="inner">
<li>홈</li>
<li>공지사항</li>
</div>
</ul>
);
// ⭕ React.Fragment(<>...</>)를 활용하여 실제 DOM 오염을 방지한 형태
const Breadcrumb = () => (
<ul className="breadcrumb">
<>
<li>홈</li>
<li>공지사항</li>
</>
</ul>
);
<React.Fragment>(단축 문법 <>...</>)를 적용하면 브라우저의 실제 렌더링 트리에 불필요한 태그를 남기지 않으면서도 올바른 계층 구조를 유지할 수 있습니다.
3. 장애 유형별 핵심 고려 사항 (시각 vs 청각)
웹 접근성 지침(KWCAG 2.2)의 기준을 충족하기 위해서는 사용자가 웹을 인지하고 조작하는 감각적 환경을 컴포넌트에 투영해야 합니다.
-
시각장애인 (전맹 및 저시력자): 마우스를 사용할 수 없으므로 오직 키보드(Tab, Shift + Tab, 방향키)와 스크린 리더의 낭독 음성에 의존합니다. 모든 인터랙션 요소는 키보드 초점을 받아야 하며, 시각적 정보는 반드시 대체 텍스트(alt, aria-label, IR 기법)로 환원되어야 합니다. 또한 비동기 통신을 통한 동적 화면 변경 사항은 실시간 음성 인터럽트 속성을 통해 안내되어야 합니다.
-
청각장애인 (농아인 및 난청인): 영상 미디어의 대사뿐만 아니라 배경 음악, 효과음 정보를 모두 담은 동기화 자막(WebVTT 규격의 <track>)이 필수로 제공되어야 합니다. 아울러 에러음이나 처리 완료 알림음처럼 소리로만 제공되는 피드백은 반드시 시각적 텍스트 및 토스트 팝업으로 동시 전달되어야 합니다.
4. 핵심 UI 컴포넌트 4종 실전 접근성 패턴
4.1. 스크린 리더 전용 숨김 텍스트 컴포넌트 (VisuallyHidden)
아이콘 형태의 버튼에 사용하는 display: none이나 visibility: hidden 스타일은 브라우저 렌더링 트리에서 요소를 제거하여 스크린 리더 역시 이를 읽지 못합니다. 보조 기기에는 온전히 전달되면서 시각적으로만 숨겨지는 IR(Image Replacement) 컴포넌트를 공통화하여 사용해야 합니다.
// components/common/VisuallyHidden.jsx
import React from 'react';
const hiddenStyle = {
position: 'absolute',
width: '1px',
height: '1px',
padding: '0',
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: '0',
};
export const VisuallyHidden = ({ children, as: Tag = 'span' }) => (
<Tag style={hiddenStyle}>{children}</Tag>
);
// 활용 예시: 닫기 아이콘 버튼
export const CloseButton = ({ onClick }) => (
<button type="button" onClick={onClick} className="icon-btn">
<svg aria-hidden="true" focusable="false">{/* 아이콘 그래픽 */}</svg>
<VisuallyHidden>팝업 레이어 닫기</VisuallyHidden>
</button>
);
aria-hidden="true" 속성으로 불필요한 SVG 그래픽의 낭독을 차단하고, 내부에 감싼 VisuallyHidden 텍스트를 통해 보조 기기 사용자에게 정확한 동작 의도를 제공합니다.
4.2. 아코디언 컴포넌트: 상태값과 WAI-ARIA 동기화
열림과 닫힘 상태가 존재하는 토글 UI는 React의 useState 상태와 WAI-ARIA 속성을 직접 매핑해야 합니다.
// components/ui/Accordion.jsx
import React, { useState } from 'react';
export const AccordionItem = ({ id, title, content }) => {
const [isOpen, setIsOpen] = useState(false);
const headerId = `acc-header-${id}`;
const panelId = `acc-panel-${id}`;
return (
<div className="accordion-item">
<h3>
<button
type="button"
id={headerId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen((prev) => !prev)}
>
{title}
<span aria-hidden="true">{isOpen ? '▲' : '▼'}</span>
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!isOpen}
>
<p>{content}</p>
</div>
</div>
);
};
-
aria-expanded={isOpen}: 불리언 상태값을 바인딩하여 스크린 리더가 현재 버튼의 상태를 "확장됨" 또는 "축소됨"으로 자동 음성 안내합니다.
-
aria-controls 및 aria-labelledby: 트리거 버튼과 실제 본문 패널의 관계를 명시적인 ID 값으로 결합합니다.
-
hidden={!isOpen}: 네이티브 hidden 속성을 부여하여 닫힌 상태에서 화면 렌더링과 접근성 트리 양쪽 모두에서 안전하게 제외시킵니다.
4.3. 모달(Modal) 컴포넌트: 초점 가두기(Focus Trap)와 복귀
모달 컴포넌트는 접근성 품질 심사에서 가장 엄격하게 평가받는 영역입니다. useRef와 useEffect를 조합하여 모달 내부의 키보드 초점 순환과 닫힘 시 이전 초점 복귀를 완벽하게 제어해야 합니다.
// components/ui/AccessibleModal.jsx
import React, { useEffect, useRef } from 'react';
export const AccessibleModal = ({ isOpen, onClose, title, children }) => {
const modalRef = useRef(null);
const prevFocusRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
// 1. 모달 활성화 전 현재 초점 요소 저장
prevFocusRef.current = document.activeElement;
// 2. 모달 내부 초점 가능 요소 탐색 및 첫 번째 요소로 초점 이동
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable?.length) focusable[0].focus();
// 3. Tab 키 순환(Focus Trap) 및 ESC 닫기 제어
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key === 'Tab' && focusable?.length) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
window.addEventListener('keydown', handleKeyDown);
// 4. 언마운트 또는 닫힘 시 기존 초점 복귀
return () => {
window.removeEventListener('keydown', handleKeyDown);
prevFocusRef.current?.focus();
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal-box"
onClick={(e) => e.stopPropagation()}
>
<h2 id="modal-title">{title}</h2>
<div className="modal-content">{children}</div>
<button type="button" onClick={onClose}>닫기</button>
</div>
</div>
);
};
role="dialog"와 aria-modal="true"를 부여함으로써 보조 기술이 현재 화면을 대화상자 레이어로 인지하게 만들며, 내부 포커스 루프를 통해 탭 키가 뒷배경으로 새어 나가는 치명적인 결함을 방어합니다.
4.4. 실시간 동적 알림(aria-live) 및 동영상 동기화 자막(track)
SPA의 비동기 통신 결과로 화면 일부에 에러 메시지가 출력될 때, 시각장애인에게 이를 실시간으로 알리기 위해서는 aria-live 속성을 부여해야 합니다. 또한 동영상 콘텐츠에는 WebVTT 표준 자막이 포함되어야 청각장애인의 접근성을 보장할 수 있습니다.
// 실시간 폼 에러 낭독 컴포넌트
export const FormAlert = ({ errorMsg }) => {
if (!errorMsg) return null;
return (
<div role="alert" aria-live="assertive" className="error-box">
<span aria-hidden="true">⚠️</span>
<p>{errorMsg}</p>
</div>
);
};
// 청각장애인을 위한 WebVTT 자막 비디오 컴포넌트
export const VideoPlayer = ({ videoSrc, subtitleSrc, title }) => (
<video controls aria-label={title}>
<source src={videoSrc} type="video/mp4" />
<track kind="subtitles" src={subtitleSrc} srcLang="ko" label="한국어 자막" default />
</video>
);
aria-live="assertive"는 오류가 렌더링되는 즉시 기존 낭독을 중단하고 에러 문구를 최우선으로 출력하며, 일반적인 단순 안내 메시지에는 사용자 읽기 흐름을 방해하지 않는 aria-live="polite"를 사용합니다.
5. 배포 전 필수 검증: 3단계 자체 점검 워크플로우
개발 완료 후 감리 기관의 피드백을 기다리지 않고 작업 단계에서 결함을 예방할 수 있는 3단계 검증 루틴입니다.
-
키보드 단독 탐색 (Mouse-Free Test): 마우스를 사용하지 않고 Tab, Shift + Tab, Enter, Space 키로만 화면 전체를 탐색합니다. 포커스 아웃라인(초점 표시선)이 육안으로 명확히 확인되는지, 레이어 팝업 내부에서 초점이 정상적으로 순환하는지 점검합니다.
-
자동화 진단 도구 실행: 브라우저 개발자 도구의 axe DevTools 또는 Chrome Lighthouse를 실행하여 텍스트와 배경 간 명도 대비(최소 4.5:1), 이미지 alt 누락, 폼 컨트롤의 id-htmlFor 매핑 여부를 기계적으로 검증합니다.
-
OS 내장 스크린 리더 청취: macOS의 VoiceOver(Cmd + F5) 또는 Windows의 NVDA(무료)를 실행하고 눈을 감은 상태에서 컴포넌트를 탐색합니다. 아이콘 버튼의 목적이 우리말 단어로 명확히 낭독되는지, 모달이 열릴 때 제목부터 바르게 읽어주는지 확인합니다.
6. 마치며: 접근성은 퍼블리셔의 고유한 전문성입니다
SI 프로젝트 현장은 늘 빠듯한 일정 속에 놓여 있습니다. 이러한 환경에서 웹 접근성을 '오픈 직전에 감점만 피하면 되는 부수적인 작업'으로 대하면 막바지 재작업으로 인해 전체 일정이 위협받게 됩니다.
퍼블리셔가 React 컴포넌트를 마크업하는 초기 단계부터 표준 시맨틱 구조를 수립하고, 상태값과 ARIA 속성을 연결하며, 키보드 초점 동선을 안정적으로 설계한다면 프로젝트의 전체 품질과 개발 생산성은 크게 향상됩니다.
sangsuni