본문으로 건너뛰기

클라이언트 앱 접근성 가이드

작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Browser Extension, Tauri Desktop, Flutter Mobile


목차

  1. WCAG 2.1 AA 기준
  2. 키보드 네비게이션
  3. 스크린 리더 지원
  4. 시각적 접근성
  5. 텍스트 크기 조절
  6. 모션 감소
  7. 플랫폼별 접근성 도구 및 테스트
  8. 스토어 심사 접근성 요구사항

1. WCAG 2.1 AA 기준

1.1 WCAG 4대 원칙 (POUR)

원칙설명주요 지침
Perceivable (인식 가능)정보와 UI 구성 요소가 인식 가능해야 함대체 텍스트, 자막, 색상 대비
Operable (조작 가능)UI 구성 요소와 네비게이션이 조작 가능해야 함키보드 접근, 충분한 시간, 발작 방지
Understandable (이해 가능)정보와 UI 조작이 이해 가능해야 함가독성, 예측 가능, 입력 도움
Robust (견고함)다양한 보조 기술에서 해석 가능해야 함표준 준수, 호환성

1.2 핵심 성공 기준 (AA 레벨)

기준 번호이름설명적용 대상
1.1.1비텍스트 콘텐츠모든 이미지에 대체 텍스트전체
1.3.1정보와 관계시맨틱 마크업으로 구조 전달전체
1.4.3대비 (최소)텍스트 대비 4.5:1 이상전체
1.4.4텍스트 크기 조절200%까지 확대 가능전체
1.4.11비텍스트 대비UI 요소 대비 3:1 이상전체
2.1.1키보드모든 기능 키보드로 접근 가능전체
2.1.2키보드 함정 없음포커스가 갇히지 않음전체
2.4.3포커스 순서논리적 탭 순서전체
2.4.7포커스 표시현재 포커스 위치 시각적 표시전체
3.2.1포커스 시포커스만으로 컨텍스트 변경 금지전체
3.3.1에러 식별입력 에러를 텍스트로 설명전체
3.3.2라벨 또는 지시문입력 필드에 라벨 제공전체
4.1.2이름, 역할, 값보조 기술이 읽을 수 있는 속성전체

1.3 준수 레벨별 목표

레벨설명프로젝트 목표
A최소 준수 (필수)100%
AA권장 준수 (법적 기준)95% 이상
AAA최고 준수 (선택)핵심 화면만

2. 키보드 네비게이션

2.1 기본 키보드 인터랙션

동작비고
Tab다음 포커스 가능 요소로 이동순방향
Shift + Tab이전 포커스 가능 요소로 이동역방향
Enter버튼/링크 활성화-
Space체크박스 토글, 버튼 활성화스크롤과 구분
Escape모달/드롭다운 닫기이전 포커스로 복원
Arrow Keys라디오 그룹, 탭, 메뉴 내 이동컨테이너 내부
Home / End목록의 처음/끝으로 이동-

2.2 포커스 관리 규칙

규칙설명구현
논리적 순서DOM 순서 = 시각적 순서tabindex 사용 최소화
포커스 표시기모든 포커스 가능 요소에 visible focus ringCSS outline, :focus-visible
포커스 트랩모달 내에서만 Tab 순환focus-trap 라이브러리
포커스 복원모달 닫기 시 트리거 요소로 복귀상태 저장/복원
스킵 링크메인 콘텐츠로 바로 이동첫 번째 Tab에 "본문으로 이동"

2.3 포커스 표시기 CSS

/* 모든 포커스 가능 요소 기본 스타일 */
:focus-visible {
outline: 2px solid var(--color-focus-ring, #4F46E5);
outline-offset: 2px;
border-radius: 2px;
}

/* 마우스 클릭 시 포커스 링 숨기기 (키보드만 표시) */
:focus:not(:focus-visible) {
outline: none;
}

/* 고대비 모드 대응 */
@media (forced-colors: active) {
:focus-visible {
outline: 2px solid Highlight;
}
}

2.4 플랫폼별 키보드 지원

항목Browser ExtensionTauriFlutter
Tab 네비게이션HTML 기본HTML 기본 (WebView)FocusTraversalGroup
포커스 트랩focus-trap 라이브러리focus-trap 라이브러리FocusScope
글로벌 단축키chrome.commands APITauri GlobalShortcutShortcuts 위젯
키보드 이벤트addEventListener('keydown')동일RawKeyboardListener

Browser Extension 단축키

// manifest.json
{
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"mac": "Command+Shift+Y"
},
"description": "확장 프로그램 열기"
},
"toggle-feature": {
"suggested_key": {
"default": "Alt+Shift+F"
},
"description": "기능 토글"
}
}
}

Flutter 포커스 관리

// FocusTraversalGroup으로 탭 순서 관리
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextField(decoration: InputDecoration(labelText: '이름')),
),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextField(decoration: InputDecoration(labelText: '이메일')),
),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: ElevatedButton(
onPressed: () {},
child: const Text('제출'),
),
),
],
),
);

3. 스크린 리더 지원

3.1 스크린 리더별 지원 매트릭스

스크린 리더OS브라우저/플랫폼우선순위
NVDAWindowsChrome, FirefoxP0
JAWSWindowsChromeP1
VoiceOvermacOSSafari, ChromeP0
VoiceOveriOSSafari, 앱 내P0
TalkBackAndroidChrome, 앱 내P0
OrcaLinuxFirefoxP2

3.2 ARIA 속성 가이드

필수 ARIA 패턴

컴포넌트ARIA 역할/속성예시
버튼role="button", aria-pressed토글 버튼
모달role="dialog", aria-modal="true", aria-labelledby대화 상자
role="tablist", role="tab", role="tabpanel"탭 인터페이스
알림role="alert", aria-live="assertive"에러 메시지
상태 업데이트aria-live="polite"로딩 완료, 카운트 변경
토글aria-expanded, aria-controls아코디언, 드롭다운
진행률role="progressbar", aria-valuenow로딩 바
입력 필드aria-label 또는 aria-labelledby, aria-describedby폼 필드
에러 상태aria-invalid="true", aria-errormessage유효성 검사

HTML 시맨틱 우선 원칙

<!-- 나쁨: div에 ARIA 추가 -->
<div role="button" tabindex="0" onclick="submit()">제출</div>

<!-- 좋음: 네이티브 HTML 요소 사용 -->
<button type="submit">제출</button>

<!-- 나쁨: 의미 없는 컨테이너 -->
<div class="nav">
<div class="nav-item">메뉴 1</div>
</div>

<!-- 좋음: 시맨틱 HTML -->
<nav aria-label="주 메뉴">
<ul>
<li><a href="/menu1">메뉴 1</a></li>
</ul>
</nav>

3.3 라이브 리전 (동적 콘텐츠 알림)

<!-- 폴라이트: 사용자 현재 작업을 방해하지 않음 -->
<div aria-live="polite" aria-atomic="true">
<p>3개의 검색 결과를 찾았습니다.</p>
</div>

<!-- 어서티브: 즉시 알림 (에러, 중요 경고) -->
<div role="alert" aria-live="assertive">
<p>결제에 실패했습니다. 카드 정보를 확인해주세요.</p>
</div>

<!-- 로딩 상태 -->
<button aria-busy="true" aria-live="polite">
<span class="sr-only">로딩 중...</span>
<span aria-hidden="true"></span>
</button>

3.4 Flutter Semantics

// 커스텀 위젯에 Semantics 추가
Semantics(
label: '프로필 사진',
hint: '탭하여 변경',
image: true,
child: GestureDetector(
onTap: () => showImagePicker(),
child: CircleAvatar(backgroundImage: NetworkImage(user.avatarUrl)),
),
);

// 시맨틱 트리에서 제외 (장식용)
ExcludeSemantics(
child: Icon(Icons.chevron_right),
);

// 여러 위젯을 하나의 시맨틱으로 병합
MergeSemantics(
child: ListTile(
leading: Icon(Icons.settings),
title: Text('설정'),
subtitle: Text('앱 설정을 변경합니다'),
),
);

3.5 스크린 리더 전용 텍스트

/* 시각적으로 숨기되 스크린 리더에는 노출 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
<!-- 아이콘만 있는 버튼 -->
<button aria-label="닫기">
<span aria-hidden="true">&times;</span>
</button>

<!-- 스크린 리더 전용 설명 추가 -->
<a href="/dashboard">
대시보드
<span class="sr-only">(현재 페이지)</span>
</a>

4. 시각적 접근성

4.1 색상 대비 기준

요소최소 대비 (AA)권장 대비 (AAA)도구
일반 텍스트 (< 18pt)4.5:17:1WebAIM Contrast Checker
큰 텍스트 (>= 18pt 또는 14pt bold)3:14.5:1-
UI 컴포넌트/아이콘3:1--
비활성 요소면제--
플레이스홀더 텍스트4.5:1-종종 간과됨

4.2 색상 팔레트 접근성

/* 접근성 기준을 충족하는 색상 시스템 */
:root {
/* 배경 */
--bg-primary: #FFFFFF; /* 밝은 모드 */
--bg-secondary: #F3F4F6;

/* 텍스트 (bg-primary 위에서 대비 검증) */
--text-primary: #111827; /* 대비 19.4:1 (AAAA) */
--text-secondary: #4B5563; /* 대비 7.5:1 (AAA) */
--text-tertiary: #6B7280; /* 대비 5.0:1 (AA) */

/* 인터랙티브 */
--color-primary: #2563EB; /* 대비 4.6:1 (AA, 큰 텍스트) */
--color-primary-text: #1D4ED8;/* 대비 6.3:1 (AA) */

/* 상태 색상 */
--color-error: #DC2626; /* 대비 4.7:1 (AA) */
--color-success: #16A34A; /* 대비 3.5:1 (큰 텍스트만) */
--color-success-text: #15803D;/* 대비 4.6:1 (AA) */
--color-warning: #D97706; /* 대비 3.2:1 (큰 텍스트만) */
}

중요: 색상만으로 정보를 전달하지 말 것. 아이콘, 텍스트, 패턴 등을 함께 사용.

4.3 고대비 모드

/* Windows 고대비 모드 대응 */
@media (forced-colors: active) {
.button {
border: 2px solid ButtonText;
}

.icon {
forced-color-adjust: none; /* 아이콘 색상 유지 필요 시 */
}

/* 포커스 링 강화 */
:focus-visible {
outline: 3px solid Highlight;
outline-offset: 2px;
}
}

/* prefers-contrast 미디어 쿼리 */
@media (prefers-contrast: more) {
:root {
--text-secondary: #1F2937; /* 대비 강화 */
--border-color: #374151;
}
}

4.4 다크 모드 접근성

항목밝은 모드다크 모드비고
배경#FFFFFF#1F2937 (너무 어둡지 않게)순검정(#000) 피하기
주요 텍스트#111827#F9FAFB대비 확인
보조 텍스트#6B7280#9CA3AF대비 확인
링크#2563EB#60A5FA더 밝은 파란색
에러#DC2626#F87171더 밝은 빨간색
그림자box-shadow 사용테두리로 대체다크 모드에서 그림자 비가시
// 시스템 설정 감지 + 사용자 오버라이드
type ThemeMode = 'light' | 'dark' | 'system';

function getEffectiveTheme(userPreference: ThemeMode): 'light' | 'dark' {
if (userPreference === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
return userPreference;
}

// 시스템 설정 변경 감지
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (userPreference === 'system') {
applyTheme(e.matches ? 'dark' : 'light');
}
});

4.5 색상만으로 정보 전달 금지

<!-- 나쁨: 색상만으로 상태 표시 -->
<span style="color: red">오류</span>
<span style="color: green">성공</span>

<!-- 좋음: 색상 + 아이콘 + 텍스트 -->
<span class="status-error">
<svg aria-hidden="true"><!-- 에러 아이콘 --></svg>
<span>오류: 이메일 형식이 올바르지 않습니다</span>
</span>

<span class="status-success">
<svg aria-hidden="true"><!-- 체크 아이콘 --></svg>
<span>성공: 저장되었습니다</span>
</span>

5. 텍스트 크기 조절

5.1 상대 단위 사용

/* 나쁨: 고정 크기 */
.text { font-size: 14px; }
.container { width: 400px; }

/* 좋음: 상대 단위 */
.text { font-size: 0.875rem; } /* 루트 기준 상대 크기 */
.container { width: 25rem; } /* 텍스트 크기에 비례 */
.spacing { padding: 1em; } /* 요소 폰트 크기 기준 */

5.2 200% 확대 대응

확인 항목기준
텍스트 잘림 없음overflow: hidden 시 텍스트 손실 확인
가로 스크롤 없음320px 뷰포트(1280px의 400%)에서 확인
컨텐츠 겹침 없음고정 높이 컨테이너 확인
기능 사용 가능버튼, 링크 클릭 가능
정보 손실 없음모든 텍스트 읽기 가능

5.3 플랫폼별 텍스트 크기 지원

플랫폼OS 텍스트 크기 지원구현
Browser Extension브라우저 줌 (자동)rem 단위 사용
Tauri브라우저 줌 (WebView)rem 단위 사용
Flutter (iOS)Dynamic TypeMediaQuery.textScaleFactor 사용
Flutter (Android)시스템 폰트 크기MediaQuery.textScaleFactor 사용
// Flutter: 시스템 텍스트 크기 배율 반영
Widget build(BuildContext context) {
final textScale = MediaQuery.of(context).textScaleFactor;

return Text(
'안녕하세요',
style: TextStyle(
fontSize: 16, // 기본 크기, textScaleFactor로 자동 확대
),
// 최대 크기 제한 (레이아웃 깨짐 방지)
textScaler: TextScaler.linear(textScale.clamp(1.0, 2.0)),
);
}

6. 모션 감소

6.1 prefers-reduced-motion 대응

/* 기본: 애니메이션 적용 */
.animated-element {
transition: transform 0.3s ease, opacity 0.3s ease;
animation: slideIn 0.5s ease;
}

/* 모션 감소 설정 시: 애니메이션 제거 또는 최소화 */
@media (prefers-reduced-motion: reduce) {
.animated-element {
transition: none;
animation: none;
}

/* 또는 매우 짧은 트랜지션만 허용 */
* {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}

6.2 모션 관련 규칙

규칙설명
자동 재생 금지움직이는 콘텐츠는 5초 이내 또는 정지 버튼
깜빡임 제한1초에 3번 이상 깜빡이는 콘텐츠 금지
사용자 제어모든 애니메이션은 사용자가 끌 수 있어야 함
필수 모션 유지진행률, 로딩 등 기능적 모션은 단순화하되 유지

6.3 Flutter 모션 감소

// Flutter: 시스템 모션 설정 반영
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.of(context).disableAnimations;

return AnimatedContainer(
duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
curve: Curves.easeInOut,
// ...
);
}

6.4 JavaScript 모션 감지

// 모션 감소 설정 확인
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;

// 설정 변경 감지
window.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', (e) => {
updateAnimationSettings(e.matches);
});

// 조건부 애니메이션
function animateElement(element: HTMLElement) {
if (prefersReducedMotion) {
// 즉시 최종 상태로 전환
element.style.opacity = '1';
return;
}
// 일반 애니메이션
element.animate([
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' },
], { duration: 300, easing: 'ease-out' });
}

7. 플랫폼별 접근성 도구 및 테스트

7.1 자동 테스트 도구

도구플랫폼용도자동화
axe-coreExtension, TauriWCAG 위반 자동 감지O
LighthouseExtension, Tauri접근성 점수 측정O
Playwright + axeExtension, TauriCI에서 접근성 E2EO
flutter_test (Semantics)FlutterSemantics 트리 검증O
accessibility_toolsFlutter개발 중 실시간 경고개발 시

axe-core + Playwright (CI)

import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';

test.describe('Accessibility', () => {
test('main page has no violations', async ({ page }) => {
await page.goto('/');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('#third-party-widget') // 서드파티 제외
.analyze();

expect(results.violations).toEqual([]);
});

test('modal dialog is accessible', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="open-modal"]');

const results = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();

expect(results.violations).toEqual([]);
});
});

Flutter 접근성 테스트

import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('meets accessibility guidelines', (tester) async {
await tester.pumpWidget(const MyApp());

// 최소 터치 타겟 크기 (48x48dp)
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));

// 텍스트 대비
await expectLater(tester, meetsGuideline(textContrastGuideline));

// 라벨 존재
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
});
}

7.2 수동 테스트 체크리스트

테스트 항목방법빈도
키보드 전용 사용마우스 없이 모든 기능 수행릴리즈 전
스크린 리더 테스트VoiceOver/NVDA로 전체 플로우릴리즈 전
고대비 모드Windows 고대비 + macOS 대비 증가릴리즈 전
200% 확대브라우저 줌 200%릴리즈 전
모션 감소OS 설정에서 모션 줄이기 활성화릴리즈 전
색상만 의존 확인그레이스케일 모드에서 정보 확인분기별

7.3 개발 중 접근성 확인

도구용도설정
ESLint (jsx-a11y)JSX 접근성 린트eslint-plugin-jsx-a11y
Storybook a11y addon컴포넌트 접근성 검사@storybook/addon-a11y
Chrome DevTools대비, 시맨틱 트리 확인내장
Flutter accessibility_tools실시간 오버레이accessibility_tools 패키지
// Flutter: 개발 빌드에서 접근성 검사 오버레이
import 'package:accessibility_tools/accessibility_tools.dart';

MaterialApp(
builder: (context, child) {
// 디버그 빌드에서만 활성화
if (kDebugMode) {
return AccessibilityTools(child: child);
}
return child!;
},
);

8. 스토어 심사 접근성 요구사항

8.1 Apple App Store

요구 항목가이드라인세부 사항
VoiceOver 지원HIG - Accessibility모든 인터랙티브 요소에 label
Dynamic TypeHIG - Typography시스템 텍스트 크기 반영
색상 접근성HIG - Color색상만으로 정보 전달 금지
터치 타겟HIG - Layout최소 44x44pt
굵은 텍스트HIG - AccessibilityBold Text 설정 반영

심사 리젝 사유: VoiceOver로 앱을 사용할 수 없는 경우 리젝 가능. 특히 커스텀 UI 컴포넌트에 Accessibility label이 없으면 지적받음.

8.2 Google Play Store

요구 항목정책세부 사항
TalkBack 지원접근성 정책contentDescription 필수
터치 타겟Material Design최소 48x48dp
색상 대비Material Design4.5:1 이상
텍스트 크기시스템 설정sp 단위 사용, 시스템 배율 반영

Play Store 심사: 접근성으로 인한 직접적인 리젝은 드물지만, 사용자 신고 + 정책 위반으로 경고 가능. Pre-launch Report에서 접근성 스캔 실행.

8.3 Chrome Web Store

요구 항목정책세부 사항
키보드 접근성브라우저 확장 정책키보드만으로 사용 가능
색상 대비권장사항명시적 요구는 아님
스크린 리더권장사항ARIA 사용 권장

CWS 심사: 접근성이 명시적 리젝 사유는 아니지만, 접근성이 좋은 확장이 추천/피처에 유리.

8.4 Firefox AMO

요구 항목정책세부 사항
키보드 접근성모범 사례권장
ARIA 사용모범 사례시맨틱 HTML 우선

8.5 법적 요구사항

법/규정지역대상기준
ADA (Americans with Disabilities Act)미국공공 서비스WCAG 2.1 AA
Section 508미국연방 기관WCAG 2.0 AA
EN 301 549EU공공 조달WCAG 2.1 AA
장애인차별금지법한국공공기관, 대기업KWCAG 2.2
Accessibility Act (EAA)EUB2C 디지털 서비스 (2025~)WCAG 2.1 AA

참고: B2B SaaS도 고객(공공기관)의 요구로 접근성 준수가 필수가 될 수 있음. Enterprise 플랜에서 VPAT (Voluntary Product Accessibility Template) 제공 고려.


부록: 접근성 전체 체크리스트

개발 단계

항목상태
시맨틱 HTML 사용 (div 남용 금지)[ ]
모든 이미지에 alt 텍스트[ ]
모든 폼 필드에 label 연결[ ]
색상 대비 4.5:1 이상 확인[ ]
Tab 순서 논리적 확인[ ]
포커스 표시기 visible[ ]
모달에 포커스 트랩 구현[ ]
에러 메시지 텍스트로 전달[ ]
ARIA 라이브 리전 적용 (동적 콘텐츠)[ ]
색상만으로 정보 전달하지 않음[ ]
rem/em 상대 단위 사용[ ]
prefers-reduced-motion 대응[ ]
prefers-color-scheme 대응[ ]
forced-colors 대응[ ]

테스트 단계

항목상태
axe-core 자동 테스트 통과 (0 violations)[ ]
Lighthouse 접근성 점수 90+[ ]
키보드 전용 네비게이션 테스트[ ]
NVDA/VoiceOver 스크린 리더 테스트[ ]
200% 확대 테스트[ ]
고대비 모드 테스트[ ]
모션 감소 모드 테스트[ ]
그레이스케일 모드 테스트[ ]

Flutter 전용

항목상태
모든 탭 타겟 최소 48x48dp[ ]
Semantics 위젯 적절히 사용[ ]
textScaleFactor 2.0까지 대응[ ]
meetsGuideline 테스트 통과[ ]
TalkBack (Android) 테스트[ ]
VoiceOver (iOS) 테스트[ ]
Dynamic Type (iOS) 테스트[ ]
Bold Text (iOS) 반영 확인[ ]