모바일 접근성
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
6단계: 모바일 접근성
이 문서는 5단계: 컴포넌트별 접근성 패턴을 선행한 후 학습하는 것을 권장한다.
모바일 환경에서의 접근성 구현과 테스트 방법을 다룬다.
개요
모바일 기기는 이제 웹 트래픽의 과반수를 차지하지만, 모바일 접근성은 데스크톱 접근성과 다른 고유한 과제를 가진다. 터치 인터페이스, 작은 화면, 다양한 보조 기술 환경을 고려해야 한다.
이 문서에서는 반응형 웹(Responsive Web) 개발자 관점에서 모바일 접근성을 다룬다. React 기반 웹 애플리케이션이 모바일 브라우저에서 접근 가능하게 동작하도록 하는 데 초점을 맞춘다.
1. 모바일 접근성의 특수성
1.1 웹 접근성과 모바일 접근성의 차이
데스크톱 웹 접근성은 주로 키보드와 스크린 리더를 중심으로 다루지만, 모바일 접근성은 추가적인 요소를 고려해야 한다.
구분 | 데스크톱 웹 | 모바일 웹 |
|---|---|---|
주요 입력 방식 | 키보드 + 마우스 | 터치 + 제스처 |
보조 기술 | NVDA, JAWS, VoiceOver | VoiceOver(iOS), TalkBack(Android) |
화면 크기 | 넓음 (1024px 이상) | 좁음 (320~428px) |
호버 상태 | 마우스 호버 가능 | 호버 불가 (터치에는 호버 개념이 없음) |
화면 방향 | 대부분 가로 고정 | 세로/가로 전환 가능 |
물리적 키보드 | 항상 존재 | 가상 키보드 (필요 시 표시) |
확대/축소 | 브라우저 줌 | 핀치 줌 + 시스템 글꼴 크기 |
1.2 터치 인터페이스의 접근성 도전 과제
터치 인터페이스는 직관적이지만 접근성 측면에서 고유한 문제를 가진다.
정밀도 부족: 손가락은 마우스 포인터보다 부정확하다. 작은 터치 타겟은 운동 장애가 있는 사용자에게 특히 어렵다
호버 불가: CSS
:hover상태에만 의존하는 정보나 기능은 터치 기기에서 접근할 수 없다제스처 의존: 스와이프, 핀치 같은 복잡한 제스처는 일부 사용자가 수행할 수 없다
가상 키보드: 화면 공간을 차지하며, 입력 필드가 가려질 수 있다
화면 크기 제약: 작은 화면에 많은 콘텐츠를 배치하면 가독성과 조작성이 떨어진다
1.3 반응형 웹 vs 네이티브 앱 접근성 비교
구분 | 반응형 웹 | 네이티브 앱 |
|---|---|---|
접근성 표준 | WCAG 2.2 | iOS: Apple Accessibility API, Android: Android Accessibility Framework |
마크업 | HTML + ARIA | iOS: UIKit/SwiftUI 접근성 속성, Android: View 접근성 속성 |
스크린 리더 호환 | HTML 시맨틱에 의존 | 플랫폼 네이티브 API와 직접 통합 |
테스트 | 브라우저 내 보조 기술 | 플랫폼 접근성 검사기 (Accessibility Inspector 등) |
장점 | 한 번의 구현으로 모든 플랫폼 지원 | 플랫폼별 최적화된 접근성 경험 |
단점 | 브라우저/OS 조합에 따른 불일치 가능 | 플랫폼별 별도 구현 필요 |
프론트엔드 개발자로서 반응형 웹을 개발할 때는 WCAG 2.2를 기준으로 하되, 모바일 브라우저의 특성을 추가로 고려해야 한다.
2. 터치 타겟 크기
터치 타겟(Touch Target)은 사용자가 손가락으로 탭할 수 있는 영역이다. 충분한 크기의 터치 타겟은 운동 장애, 시각 장애, 고령 사용자 등 다양한 사용자에게 필수적이다.
2.1 기준 요약
기준 | 레벨 | 최소 크기 | 비고 |
|---|---|---|---|
WCAG 2.2 기준 2.5.8 타겟 크기 (최소) | AA | 24 x 24 CSS 픽셀 | 인접 타겟 간 간격으로 보완 가능 |
WCAG 2.2 기준 2.5.5 타겟 크기 (향상) | AAA | 44 x 44 CSS 픽셀 | 권장 크기 |
Apple Human Interface Guidelines | - | 44 x 44 pt | iOS 기본 가이드라인 |
Material Design | - | 48 x 48 dp | Android 기본 가이드라인 |
WCAG 2.2 기준 2.5.8 타겟 크기 최소 (레벨 AA)에 따르면, 터치 타겟의 크기가 최소 24x24 CSS 픽셀이어야 한다. 단, 인라인 텍스트 링크, 사용자 에이전트가 결정하는 크기, 법적으로 요구되는 표시 등은 예외이다.
WCAG 2.2 기준 2.5.5 타겟 크기 향상 (레벨 AAA)에서는 최소 44x44 CSS 픽셀을 요구한다. 실무에서는 이 크기를 목표로 구현하는 것이 좋다.
2.2 CSS로 터치 영역 확대하는 기법
시각적 크기를 변경하지 않으면서 터치 가능 영역만 확대할 수 있다.
padding을 활용한 방법
/* 접근성 위반: 터치 타겟이 너무 작음 */
.icon-button {
width: 16px;
height: 16px;
padding: 0;
}
/* 접근성 준수: padding으로 터치 영역 확대 */
.icon-button {
width: 16px;
height: 16px;
padding: 14px; /* 16 + 14*2 = 44px 터치 영역 확보 */
box-sizing: content-box;
}
::after 의사 요소를 활용한 방법
시각적 크기를 완전히 유지하면서 터치 영역만 확대하는 방법이다.
/* 접근성 준수: ::after로 투명한 터치 영역 확대 */
.small-button {
position: relative;
width: 24px;
height: 24px;
}
.small-button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
/* 시각적으로 보이지 않지만 터치 이벤트를 받음 */
}
2.3 인접 타겟 간 간격
터치 타겟이 24x24px 미만이더라도, 인접한 타겟과의 간격으로 보완할 수 있다. 핵심은 타겟과 주변 여백을 합친 영역이 24x24px 이상이 되어야 한다는 것이다.
/* 접근성 위반: 작은 버튼이 간격 없이 나열됨 */
.button-group button {
width: 20px;
height: 20px;
margin: 0;
}
/* 접근성 준수: 간격으로 터치 영역 확보 */
.button-group button {
width: 20px;
height: 20px;
margin: 2px; /* 20 + 2*2 = 24px 이상 확보 */
}
2.4 React 컴포넌트 예시
// 접근성 준수: 최소 터치 타겟을 보장하는 버튼 컴포넌트
import { forwardRef } from 'react'
interface TouchTargetProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode
minSize?: number
}
const TouchButton = forwardRef<HTMLButtonElement, TouchTargetProps>(
function TouchButton({ children, minSize = 44, style, ...props }, ref) {
return (
<button
ref={ref}
style={{
position: 'relative',
minWidth: `${minSize}px`,
minHeight: `${minSize}px`,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
...style,
}}
{...props}
>
{children}
</button>
)
}
)
3. 제스처 접근성
WCAG 2.2 기준 2.5.1 포인터 제스처 (레벨 A)는 멀티포인트(두 손가락 이상) 또는 경로 기반(path-based) 제스처로 조작되는 기능에 대해, 단일 포인터(single pointer)로 조작 가능한 대안을 제공하도록 요구한다.
3.1 복잡한 제스처와 대안
복잡한 제스처 | 대안 (단일 포인터) |
|---|---|
스와이프로 삭제 | 삭제 버튼 제공 |
핀치 줌 | + / - 버튼 제공 |
두 손가락 회전 | 회전 버튼 제공 |
드래그 앤 드롭 정렬 | 위/아래 이동 버튼 제공 |
스와이프 캐러셀 | 이전/다음 버튼 제공 |
길게 누르기(long press) | 일반 탭으로 동작하는 대안 |
3.2 코드 예시: 스와이프 캐러셀의 접근 가능한 대안
// 접근성 위반: 스와이프로만 이동 가능한 캐러셀
function BadSwipeCarousel({ items }: { items: string[] }) {
// 터치 이벤트로만 이동, 버튼 없음
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{items[currentIndex]}
</div>
)
}
// 접근성 준수: 스와이프 + 버튼 + 키보드 모두 지원
import { useState, useRef, useCallback } from 'react'
interface SwipeCarouselProps {
items: { id: string; content: React.ReactNode; label: string }[]
}
function SwipeCarousel({ items }: SwipeCarouselProps) {
const [currentIndex, setCurrentIndex] = useState(0)
const touchStartX = useRef<number>(0)
const touchEndX = useRef<number>(0)
const goToPrevious = useCallback(() => {
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
}, [items.length])
const goToNext = useCallback(() => {
setCurrentIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
}, [items.length])
// 스와이프 제스처 처리 (추가 방법, 유일한 방법이 아님)
function handleTouchStart(event: React.TouchEvent) {
touchStartX.current = event.touches[0].clientX
}
function handleTouchEnd(event: React.TouchEvent) {
touchEndX.current = event.changedTouches[0].clientX
const diff = touchStartX.current - touchEndX.current
const threshold = 50
if (Math.abs(diff) > threshold) {
if (diff > 0) {
goToNext()
} else {
goToPrevious()
}
}
}
return (
<section
aria-roledescription="carousel"
aria-label="상품 이미지"
>
{/* 접근성 준수: 단일 포인터(버튼 탭)로 조작 가능한 대안 */}
<div>
<button onClick={goToPrevious} aria-label="이전 항목">
이전
</button>
<span aria-live="polite">
{items.length}개 중 {currentIndex + 1}번째
</span>
<button onClick={goToNext} aria-label="다음 항목">
다음
</button>
</div>
{/* 스와이프도 지원하지만, 버튼이 대안으로 존재함 */}
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
<div
role="group"
aria-roledescription="slide"
aria-label={items[currentIndex].label}
>
{items[currentIndex].content}
</div>
</div>
</section>
)
}
3.3 드래그 앤 드롭의 접근 가능한 대안
// 접근성 준수: 드래그 앤 드롭 + 버튼으로 이동 가능한 리스트
import { useState, useCallback } from 'react'
interface SortableItem {
id: string
label: string
}
function AccessibleSortableList({ initialItems }: { initialItems: SortableItem[] }) {
const [items, setItems] = useState(initialItems)
const moveItem = useCallback((index: number, direction: 'up' | 'down') => {
setItems((prev) => {
const newItems = [...prev]
const targetIndex = direction === 'up' ? index - 1 : index + 1
if (targetIndex < 0 || targetIndex >= newItems.length) return prev
const temp = newItems[index]
newItems[index] = newItems[targetIndex]
newItems[targetIndex] = temp
return newItems
})
}, [])
return (
<ul aria-label="정렬 가능한 목록">
{items.map((item, index) => (
<li key={item.id}>
<span>{item.label}</span>
{/* 접근성 준수: 단일 포인터로 순서 변경 가능 */}
<button
onClick={() => moveItem(index, 'up')}
disabled={index === 0}
aria-label={`${item.label}을(를) 위로 이동`}
>
위로
</button>
<button
onClick={() => moveItem(index, 'down')}
disabled={index === items.length - 1}
aria-label={`${item.label}을(를) 아래로 이동`}
>
아래로
</button>
</li>
))}
</ul>
)
}
4. 화면 방향과 뷰포트
4.1 화면 방향
WCAG 2.2 기준 1.3.4 방향 (레벨 AA)은 콘텐츠의 표시와 조작이 세로 또는 가로 같은 단일 화면 방향으로 제한되지 않아야 한다고 요구한다. 단, 특정 방향이 필수적인 경우(예: 피아노 앱, 수표 촬영)는 예외이다.
/* 접근성 위반: 특정 방향으로 제한 */
@media (orientation: portrait) {
.app {
display: block;
}
}
@media (orientation: landscape) {
.app {
display: none; /* 가로 모드에서 콘텐츠를 숨김 */
}
}
/* 접근성 준수: 양쪽 방향 모두 지원, 레이아웃만 조정 */
@media (orientation: portrait) {
.layout {
flex-direction: column;
}
}
@media (orientation: landscape) {
.layout {
flex-direction: row;
}
}
JavaScript로 방향을 감지하더라도 특정 방향을 강제하면 안 된다.
// 접근성 위반: 가로 모드 강제 전환 요청
if (window.screen.orientation.type.startsWith('portrait')) {
window.screen.orientation.lock('landscape')
}
// 접근성 준수: 방향에 따라 레이아웃만 조정
function useOrientation() {
const [isLandscape, setIsLandscape] = useState(
window.matchMedia('(orientation: landscape)').matches
)
useEffect(() => {
const mediaQuery = window.matchMedia('(orientation: landscape)')
function handler(event: MediaQueryListEvent) {
setIsLandscape(event.matches)
}
mediaQuery.addEventListener('change', handler)
return () => mediaQuery.removeEventListener('change', handler)
}, [])
return { isLandscape }
}
4.2 뷰포트 확대 제한 금지
사용자가 콘텐츠를 확대할 수 있는 기능은 저시력 사용자에게 필수적이다. 뷰포트 메타 태그에서 확대를 제한하면 안 된다.
<!-- 접근성 위반: 확대를 제한하는 뷰포트 설정 -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1">
<!-- 접근성 준수: 확대를 허용하는 뷰포트 설정 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
user-scalable=no와 maximum-scale=1은 사용자의 확대 기능을 차단하므로 사용하지 않아야 한다. 이는 WCAG 기준 1.4.4 텍스트 크기 조절 (레벨 AA)과 직접 관련된다.
참고: iOS Safari는 보안상의 이유로 입력 필드 포커스 시 자동 확대가 발생한다. 이를 방지하기 위해
user-scalable=no를 사용하는 대신, 입력 필드의font-size를 16px 이상으로 설정하면 자동 확대가 발생하지 않는다.
/* 접근성 준수: iOS 자동 확대를 방지하되 사용자 확대는 허용 */
input,
select,
textarea {
font-size: 16px; /* 16px 이상이면 iOS가 자동 확대하지 않음 */
}
4.3 안전 영역 (Safe Area)
노치(notch)나 둥근 모서리가 있는 기기에서는 콘텐츠가 잘릴 수 있다. CSS 환경 변수를 사용하여 안전 영역을 고려해야 한다.
/* 접근성 준수: 안전 영역을 고려한 레이아웃 */
.app-container {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
/* viewport-fit=cover와 함께 사용 */
/* <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> */
/* 하단 고정 버튼의 안전 영역 처리 */
.fixed-bottom-button {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
안전 영역을 고려하지 않으면 인터랙티브 요소가 물리적으로 접근 불가능한 위치에 배치될 수 있다.
5. iOS VoiceOver 가이드
VoiceOver는 Apple의 내장 스크린 리더로, iOS와 macOS 모두에서 사용할 수 있다. 모바일 웹 접근성 테스트에서 가장 중요한 도구 중 하나이다.
5.1 VoiceOver 기본 제스처
제스처 | 동작 |
|---|---|
한 손가락 탭 | 탭한 위치의 항목을 읽는다 |
한 손가락 좌/우 스와이프 | 이전/다음 항목으로 이동한다 |
한 손가락 더블 탭 | 현재 항목을 활성화한다 (탭에 해당) |
세 손가락 좌/우 스와이프 | 페이지 스크롤 (수평) |
세 손가락 위/아래 스와이프 | 페이지 스크롤 (수직) |
두 손가락 탭 | 미디어 재생/일시정지 |
두 손가락 위로 스와이프 | 페이지 처음부터 모두 읽기 |
한 손가락 위/아래 스와이프 | 로터(Rotor) 설정에 따른 탐색 (제목, 링크, 폼 요소 등) |
두 손가락 비틀기 (회전) | 로터 설정 변경 |
한 손가락 더블 탭 후 홀드 | 드래그 시작 (끌어서 놓기) |
두 손가락 Z자 제스처 | 뒤로 가기 / 닫기 (Escape에 해당) |
5.2 VoiceOver에서 자주 발생하는 문제와 해결법
문제 1: 숨겨진 요소가 읽힘
/* 접근성 위반: display:none이 아닌 방법으로 숨김 처리 시 VoiceOver가 읽을 수 있음 */
.hidden {
opacity: 0;
position: absolute;
left: -9999px;
}
/* 접근성 준수: aria-hidden 또는 display:none으로 보조 기술에서도 숨김 */
.visually-hidden-but-accessible {
/* 시각적으로 숨기되 스크린 리더에는 노출 (의도적인 경우) */
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hidden-from-all {
/* 모든 사용자에게 숨김 (스크린 리더 포함) */
display: none;
}
문제 2: 동적 콘텐츠 변경이 감지되지 않음
// 접근성 위반: 콘텐츠가 변경되어도 VoiceOver가 알지 못함
function SearchResults({ results }: { results: string[] }) {
return (
<div>
{results.map((item) => (
<div key={item}>{item}</div>
))}
</div>
)
}
// 접근성 준수: aria-live로 동적 콘텐츠 변경 알림
function SearchResults({ results }: { results: string[] }) {
return (
<div>
<div aria-live="polite" aria-atomic="true">
검색 결과 {results.length}건
</div>
<ul>
{results.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
)
}
문제 3: 커스텀 컴포넌트의 역할 누락
// 접근성 위반: div로 만든 버튼은 VoiceOver가 "버튼"으로 인식하지 못함
<div onClick={handleClick} className="button">제출</div>
// 접근성 준수: 네이티브 button 사용
<button onClick={handleClick}>제출</button>
5.3 웹 앱에서 VoiceOver 테스트하는 방법
VoiceOver 활성화: 설정 > 손쉬운 사용 > VoiceOver, 또는 Siri에게 "VoiceOver 켜줘"
Safari에서 페이지 열기: iOS의 VoiceOver는 Safari에서 가장 안정적으로 동작한다
기본 탐색 수행:
한 손가락 좌우 스와이프로 모든 요소를 순서대로 탐색한다
각 요소의 이름, 역할, 상태가 올바르게 읽히는지 확인한다
로터 사용: 두 손가락 비틀기로 로터를 열고, 제목/링크/폼 요소별 탐색을 확인한다
동적 콘텐츠 확인: 콘텐츠 변경 시 자동으로 알림이 읽히는지 확인한다
포커스 관리 확인: 모달, 탭 전환 등에서 포커스가 올바른 위치로 이동하는지 확인한다
5.4 iOS Safari의 접근성 특이사항
100vh 문제: iOS Safari에서
100vh는 주소 표시줄을 포함한 높이로 계산되어 콘텐츠가 잘릴 수 있다.100dvh(Dynamic Viewport Height)를 사용하거나-webkit-fill-available을 활용한다포커스 시 자동 스크롤: 요소에 포커스가 이동하면 Safari가 자동으로 스크롤한다. 고정 헤더가 있으면 요소가 가려질 수 있으므로
scroll-padding-top을 설정한다터치 이벤트 지연: Safari의 300ms 탭 지연은
touch-action: manipulation으로 해결한다-webkit-tap-highlight-color: 기본 탭 하이라이트를 제거할 때 포커스 표시기까지 제거하지 않도록 주의한다
/* 접근성 준수: iOS Safari 특이사항 대응 */
html {
/* 100vh 문제 해결 */
height: 100dvh;
}
/* 고정 헤더 아래 콘텐츠가 가려지는 문제 */
html {
scroll-padding-top: 60px; /* 헤더 높이만큼 */
}
/* 탭 지연 제거 (포커스 스타일은 유지) */
button, a, input {
touch-action: manipulation;
}
/* 탭 하이라이트 제거 시 포커스 스타일 보존 */
button {
-webkit-tap-highlight-color: transparent;
}
button:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
6. Android TalkBack 가이드
TalkBack은 Android의 내장 스크린 리더이다. Google의 Android 접근성 서비스(Android Accessibility Suite)에 포함되어 있으며, Chrome과 함께 웹 접근성 테스트에 사용된다.
6.1 TalkBack 기본 제스처
제스처 | 동작 |
|---|---|
한 손가락 탭 | 탭한 위치의 항목을 읽는다 |
한 손가락 좌/우 스와이프 | 이전/다음 항목으로 이동한다 |
한 손가락 더블 탭 | 현재 항목을 활성화한다 |
두 손가락 위/아래 스와이프 | 페이지 스크롤 |
세 손가락 좌/우 스와이프 | 읽기 단위 변경 (문자, 단어, 줄, 제목, 링크 등) |
한 손가락 아래 후 오른쪽으로 L자 | 다음 항목으로 이동 (빠른 탐색) |
한 손가락 위 후 왼쪽으로 L자 | 이전 항목으로 이동 (빠른 탐색) |
한 손가락 아래 후 왼쪽으로 L자 | 뒤로 가기 / 닫기 (Escape에 해당) |
한 손가락 더블 탭 후 홀드 | 드래그 시작 |
세 손가락 탭 | TalkBack 메뉴 열기 |
참고: TalkBack의 제스처는 Android 버전에 따라 차이가 있을 수 있다. Android 13 이상에서는 멀티 핑거 제스처가 추가되었다.
6.2 TalkBack에서 자주 발생하는 문제와 해결법
문제 1: 포커스 순서 불일치
Android에서 시각적 순서와 DOM 순서가 다르면 TalkBack의 탐색 순서가 혼란스러워진다.
/* 접근성 위반: CSS로 시각적 순서만 변경 (DOM 순서와 불일치) */
.container {
display: flex;
flex-direction: row-reverse;
}
/* 접근성 준수: DOM 순서를 시각적 순서와 일치시킴 */
.container {
display: flex;
}
/* 필요하면 DOM 순서 자체를 변경한다 */
문제 2: WebView에서 포커스 누락
// 접근성 위반: 동적으로 추가된 콘텐츠에 포커스가 이동하지 않음
function showAlert(message: string) {
const div = document.createElement('div')
div.textContent = message
document.body.appendChild(div)
}
// 접근성 준수: role="alert"로 자동 읽기 + 필요 시 포커스 이동
function showAlert(message: string) {
const div = document.createElement('div')
div.setAttribute('role', 'alert')
div.textContent = message
document.body.appendChild(div)
}
문제 3: 커스텀 셀렉트의 인식 문제
// 접근성 위반: TalkBack이 드롭다운으로 인식하지 못함
<div className="select" onClick={toggleDropdown}>
{selectedValue}
</div>
// 접근성 준수: 네이티브 select 사용 또는 적절한 ARIA 적용
<select aria-label="정렬 기준">
<option value="latest">최신순</option>
<option value="price">가격순</option>
<option value="popular">인기순</option>
</select>
6.3 Chrome for Android에서 TalkBack 테스트하는 방법
TalkBack 활성화: 설정 > 접근성 > TalkBack 활성화, 또는 "Hey Google, TalkBack 켜줘"
Chrome에서 페이지 열기: TalkBack은 Chrome에서 가장 잘 동작한다
기본 탐색:
한 손가락 좌우 스와이프로 모든 요소를 순서대로 탐색한다
읽기 단위를 변경(세 손가락 좌우 스와이프)하여 제목, 링크별로 탐색한다
폼 테스트: 입력 필드에 포커스하고 레이블, 힌트, 에러가 올바르게 읽히는지 확인한다
동적 콘텐츠 확인: AJAX 업데이트 시 aria-live 영역이 읽히는지 확인한다
6.4 Android WebView의 접근성 특이사항
하이브리드 앱에서 WebView를 사용하는 경우 추가적인 접근성 고려사항이 있다.
WebView 접근성 활성화: Android에서
WebView.setAccessibilityDelegate()가 설정되어 있어야 웹 콘텐츠의 접근성이 네이티브 레이어로 전달된다JavaScript 인터페이스: WebView와 네이티브 코드 간 통신 시 접근성 이벤트가 누락될 수 있다
포커스 전환: WebView 안팎으로 포커스가 전환될 때 TalkBack이 혼란을 겪을 수 있다. 명시적인 포커스 관리가 필요하다
가상 키보드 처리: WebView 내 입력 필드의 가상 키보드 동작이 네이티브와 다를 수 있다
실무 팁: 프론트엔드 개발자가 WebView 접근성 문제를 발견하면, 웹 코드에서 해결할 수 있는 부분(ARIA 속성, 포커스 관리)과 네이티브 코드에서 해결해야 하는 부분(WebView 설정)을 구분하여 네이티브 개발자와 협업해야 한다.
7. 반응형 웹의 모바일 접근성 체크포인트
7.1 모바일 뷰포트에서 확인해야 할 항목
모든 텍스트가 확대/축소 없이 읽히는가 (최소 16px 이상 권장)
터치 타겟이 최소 24x24px 이상인가 (44x44px 권장)
가로 스크롤 없이 320px 뷰포트에서 콘텐츠가 표시되는가
세로/가로 모드 전환 시 콘텐츠가 정상 표시되는가
입력 필드 포커스 시 콘텐츠가 가려지지 않는가
고정 헤더/푸터가 콘텐츠를 가리지 않는가
user-scalable=no가 뷰포트 메타 태그에 없는가안전 영역(safe-area-inset)이 고려되어 있는가
7.2 터치와 마우스 이벤트 병행 처리
반응형 웹은 터치와 마우스 입력을 모두 지원해야 한다. 포인터 이벤트(Pointer Events) API를 사용하면 양쪽을 통합하여 처리할 수 있다.
// 접근성 위반: 마우스 이벤트만 처리
<div onMouseDown={handleStart} onMouseMove={handleMove} onMouseUp={handleEnd}>
드래그 영역
</div>
// 접근성 준수: 포인터 이벤트로 터치와 마우스 통합 처리
<div
onPointerDown={handleStart}
onPointerMove={handleMove}
onPointerUp={handleEnd}
style={{ touchAction: 'none' }} /* 브라우저 기본 터치 동작 방지 */
>
드래그 영역
</div>
// 접근성 준수: 입력 방식에 따른 조건부 처리
function useInputMethod() {
const [inputMethod, setInputMethod] = useState<'touch' | 'mouse' | 'keyboard'>('mouse')
useEffect(() => {
function handleTouch() {
setInputMethod('touch')
}
function handleMouse(event: MouseEvent) {
// mousemove가 실제 마우스에서만 발생하는지 확인
if (event.movementX !== 0 || event.movementY !== 0) {
setInputMethod('mouse')
}
}
function handleKeyboard() {
setInputMethod('keyboard')
}
window.addEventListener('touchstart', handleTouch, { passive: true })
window.addEventListener('mousemove', handleMouse)
window.addEventListener('keydown', handleKeyboard)
return () => {
window.removeEventListener('touchstart', handleTouch)
window.removeEventListener('mousemove', handleMouse)
window.removeEventListener('keydown', handleKeyboard)
}
}, [])
return inputMethod
}
7.3 호버 상태 의존 콘텐츠 문제
터치 기기에는 호버 개념이 없다. 호버에만 의존하는 콘텐츠나 기능은 터치 사용자가 접근할 수 없다.
// 접근성 위반: 호버에서만 나타나는 삭제 버튼
function ListItem({ item }: { item: Item }) {
return (
<div className="list-item">
<span>{item.name}</span>
{/* CSS로 .list-item:hover .delete-btn { display: block } */}
<button className="delete-btn" style={{ display: 'none' }}>삭제</button>
</div>
)
}
// 접근성 준수: 호버 가능 여부에 따른 조건부 표시
function ListItem({ item, onDelete }: { item: Item; onDelete: (id: string) => void }) {
return (
<div className="list-item">
<span>{item.name}</span>
{/* 터치 기기: 항상 표시, 마우스: 호버 시 표시 */}
<button
className="delete-btn"
onClick={() => onDelete(item.id)}
aria-label={`${item.name} 삭제`}
>
삭제
</button>
</div>
)
}
/* 접근성 준수: 미디어 쿼리로 호버 가능 여부 감지 */
.delete-btn {
/* 기본: 항상 표시 (터치 기기 대응) */
opacity: 1;
}
@media (hover: hover) {
/* 호버가 가능한 기기에서만 호버 기반 표시/숨김 적용 */
.delete-btn {
opacity: 0;
transition: opacity 0.2s;
}
.list-item:hover .delete-btn,
.delete-btn:focus-visible {
opacity: 1;
}
}
핵심 포인트:
@media (hover: hover)미디어 쿼리로 호버가 가능한 기기를 감지할 수 있다. 터치 전용 기기에서는 호버에 의존하는 UI를 표시하지 않는다.@media (pointer: coarse)는 부정확한 포인터(손가락)를 감지한다.
7.4 모바일에서 포커스 관리 시 주의점
모바일 브라우저에서의 포커스 관리는 데스크톱과 몇 가지 다른 점이 있다.
// 문제: iOS Safari에서 프로그래밍적 포커스가 스크린 리더에 반영되지 않을 수 있음
function focusElement(element: HTMLElement) {
element.focus()
}
// 접근성 준수: iOS Safari 호환 포커스 관리
function focusElement(element: HTMLElement) {
// 포커스 가능하지 않은 요소에 tabIndex를 추가
if (!element.hasAttribute('tabindex')) {
element.setAttribute('tabindex', '-1')
}
// requestAnimationFrame으로 렌더링 이후 포커스
requestAnimationFrame(() => {
element.focus()
})
}
주의 사항:
iOS Safari에서는
tabindex="-1"이 없는 비인터랙티브 요소에focus()를 호출해도 VoiceOver가 해당 요소를 읽지 않을 수 있다가상 키보드가 열려 있는 상태에서 포커스가 이동하면 화면이 예기치 않게 스크롤될 수 있다
document.activeElement가 iframe 내부 요소를 반환하지 않을 수 있다Android TalkBack에서는
aria-live영역의 변경이 포커스 이동보다 우선하여 읽힐 수 있다
8. 모바일 접근성 테스트 도구
8.1 iOS: VoiceOver + Safari
항목 | 내용 |
|---|---|
활성화 | 설정 > 손쉬운 사용 > VoiceOver |
브라우저 | Safari (VoiceOver 호환성이 가장 높음) |
단축키 | 측면 버튼 3회 연속 클릭 (접근성 단축키 설정 시) |
디버깅 | Mac의 Safari Web Inspector로 연결하여 접근성 트리 확인 |
Mac에서 iOS Safari 원격 디버깅 방법:
iOS: 설정 > Safari > 고급 > 웹 인스펙터 활성화
Mac: Safari > 개발자 메뉴 > 연결된 기기 이름 > 해당 페이지 선택
Mac의 Safari Web Inspector에서 요소 탭 > 접근성 속성 확인
8.2 Android: TalkBack + Chrome
항목 | 내용 |
|---|---|
활성화 | 설정 > 접근성 > TalkBack |
브라우저 | Chrome (TalkBack 호환성이 가장 높음) |
단축키 | 볼륨 키 두 개를 3초간 동시에 누르기 (설정 시) |
디버깅 | Chrome DevTools의 Remote Debugging으로 접근성 트리 확인 |
Chrome DevTools 원격 디버깅 방법:
Android: 설정 > 개발자 옵션 > USB 디버깅 활성화
USB로 컴퓨터에 연결
데스크톱 Chrome에서
chrome://inspect/#devices접속연결된 기기의 해당 페이지에서 "inspect" 클릭
Chrome DevTools > Elements 탭 > Accessibility 패널에서 접근성 트리 확인
8.3 원격 디버깅 없이 테스트하기
원격 디버깅 환경을 구성하기 어려운 경우, 다음 방법으로도 기본적인 접근성 테스트가 가능하다.
Chrome DevTools 모바일 시뮬레이션: 데스크톱 Chrome에서 DevTools > 기기 도구 모음(Device Toolbar)으로 모바일 뷰포트를 시뮬레이션한다. 단, 실제 터치 동작과 스크린 리더 테스트는 불가능하다
BrowserStack / Sauce Labs: 원격 실제 기기에서 테스트할 수 있는 클라우드 서비스
Android 에뮬레이터의 TalkBack: Android Studio의 에뮬레이터에서 TalkBack을 활성화하여 테스트할 수 있다
8.4 모바일 접근성 테스트 체크리스트
기본 테스트
VoiceOver(iOS) 또는 TalkBack(Android)으로 모든 페이지를 순서대로 탐색할 수 있다
모든 인터랙티브 요소의 이름, 역할, 상태가 올바르게 읽힌다
이미지에 적절한 대체 텍스트가 있다
제목 구조(h1~h6)가 논리적이며, 제목 탐색으로 빠르게 이동할 수 있다
터치 접근성
모든 터치 타겟이 최소 24x24px(AA) 또는 44x44px(AAA) 이상이다
스와이프, 핀치 등 복잡한 제스처에 대한 단일 포인터 대안이 있다
호버에만 의존하는 기능이 없다
길게 누르기(long press)에만 의존하는 기능이 없다
뷰포트와 레이아웃
user-scalable=no가 사용되지 않았다320px 뷰포트에서 가로 스크롤 없이 콘텐츠가 표시된다
세로/가로 모드 전환이 자유롭다
안전 영역(safe-area-inset)이 적용되어 있다
입력 필드의 font-size가 16px 이상이다 (iOS 자동 확대 방지)
동적 콘텐츠
AJAX 업데이트 시 aria-live 영역이 올바르게 읽힌다
모달, 페이지 전환 시 포커스가 적절한 위치로 이동한다
자동 재생 콘텐츠에 정지 버튼이 있다
토스트/알림이 스크린 리더로 전달된다
폼 접근성
모든 입력 필드에 레이블이 연결되어 있다
에러 메시지가 스크린 리더로 전달된다
가상 키보드의 "다음"/"완료" 버튼이 올바르게 동작한다
입력 타입(
type="email",type="tel"등)에 맞는 가상 키보드가 표시된다
9. 단계 완료 체크리스트
이 단계의 학습을 완료하려면 다음 항목을 모두 실습해 보아야 한다.
자신이 개발한 웹 앱을 iOS VoiceOver로 처음부터 끝까지 탐색해 보았다
자신이 개발한 웹 앱을 Android TalkBack으로 처음부터 끝까지 탐색해 보았다
모든 터치 타겟의 크기를 측정하고 최소 기준(24x24px)을 충족하는지 확인했다
복잡한 제스처(스와이프, 핀치 등)에 대한 대안 UI를 구현했다
뷰포트 메타 태그에서
user-scalable=no를 제거했다320px 뷰포트에서 가로 스크롤 없이 모든 콘텐츠가 표시되는지 확인했다
세로/가로 모드 전환 시 레이아웃이 정상 동작하는지 확인했다
호버에만 의존하는 UI가 없는지 확인하고, 있다면 대안을 구현했다
@media (hover: hover)미디어 쿼리를 활용하여 입력 방식별 UI를 분기했다iOS Safari에서 입력 필드의 font-size가 16px 이상인지 확인했다
안전 영역(safe-area-inset)을 고려한 레이아웃을 적용했다
원격 디버깅 환경을 구성하고 모바일 접근성 트리를 확인했다
다음 단계
이 단계를 마쳤다면 7단계: 테스트와 자동화로 진행한다. axe-core, Lighthouse를 활용한 자동 접근성 테스트, 스크린 리더 수동 테스트 방법, CI/CD 파이프라인에 접근성 테스트를 통합하는 방법을 학습한다.
작성일: 2026-02-21
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.