마크업 유효성 검사, 접근성 자동화 테스트, DevTools를 활용한 HTML 디버깅 워크플로를 다룬다.
검증·테스트·디버깅과 품질 관리
HTML은 오류를 조용히 삼킵니다. 닫는 태그가 빠져 있어도, aria-labelledby가 존재하지 않는 ID를 참조해도, 브라우저는 예외를 던지지 않고 최선의 추측으로 파싱을 마칩니다. 결과적으로 문제가 있어도 겉으로는 멀쩡해 보이고, 보조기기 사용자에게만 조용히 망가진 채 서비스됩니다.
입문편 9강에서 ARIA 속성의 의미와 올바른 작성법을 다뤘다면, 이번 레슨은 그 규칙이 실제로 지켜지고 있는지를 측정하고 자동화하는 도구와 프로세스에 집중합니다. W3C 검증기부터 DevTools 접근성 트리, axe-core CI 통합까지, 코드가 배포되기 전에 문제를 찾아내는 체계적인 워크플로를 구축하는 것이 목표입니다.
학습 목표
- W3C Markup Validator와 HTMLHint 린터로 정적 마크업을 검증하고 오류를 해석한다.
- DevTools Elements·Accessibility 트리를 사용하여 DOM 구조와 ARIA 매핑을 실시간으로 디버깅한다.
- axe-core와 Lighthouse로 접근성·성능을 자동화 점검하고 결과를 코드에 반영한다.
- 스크린 리더를 이용해 실제 보조기기 동작을 직접 검증하는 절차를 익힌다.
- 시맨틱·ARIA 회귀를 막기 위한 CI 파이프라인 통합 전략을 설계한다.
W3C Markup Validator와 HTMLHint 정적 검증
W3C Validator로 유효성 오류 해석하기
W3C Markup Validation Service는 HTML 문서를 파싱 스펙에 비춰 검사합니다. 단순한 태그 오류뿐 아니라 필수 속성 누락, 허용되지 않는 콘텐츠 모델 위반, 중복 id 등을 잡아냅니다. CI 환경에서는 vnu-jar(Nu Html Checker) 패키지를 로컬에서 실행할 수 있습니다. 이 패키지가 노출하는 실행 바이너리 이름은 vnu입니다. vnu.jar는 Java로 작성된 프로그램이므로, 실행 환경에 Java(JRE 또는 JDK)가 설치돼 있어야 합니다.
# Nu Html Checker CLI 설치 및 실행 (실행에는 Java가 필요)
npm install --save-dev vnu-jar
# HTML 파일 하나 검사 (bin 이름은 vnu)
npx vnu dist/index.html
# 디렉터리 전체 검사 (JSON 출력)
npx vnu --format json --errors-only dist/ 2> validation-report.json
⚠️ 주의
--errors-only플래그 없이 실행하면 경고(warning)까지 출력됩니다. CI 빌드를 실패로 처리할 기준(errors만, 또는 warnings 포함)을 팀 내에서 명확히 합의해 두세요.
HTMLHint로 린팅 규칙 적용하기
W3C Validator가 파싱 스펙 준수를 검사한다면, HTMLHint는 팀이 정한 코딩 컨벤션을 강제하는 린터입니다. alt 속성 필수화, id 중복 금지, 시맨틱 태그 권장 등 규칙을 .htmlhintrc에 선언합니다.
# 설치
npm install --save-dev htmlhint
# 기본 실행
npx htmlhint "src/**/*.html"
// .htmlhintrc — 프로젝트 루트에 위치
{
"tagname-lowercase": true,
"attr-lowercase": true,
"attr-value-double-quotes": true,
"doctype-first": true,
"tag-pair": true,
"id-unique": true,
"src-not-empty": true,
"alt-require": true,
"space-tab-mixed-disabled": "space",
"title-require": true
}
💡 TIP
alt-require규칙은<img>태그에alt속성 자체가 없는 경우를 잡습니다. 장식용 이미지에는alt=""(빈 문자열)을 명시해야 하는데, 이는 의도적인 빈 값이므로 린터가 통과시킵니다. 접근성의 의도를 코드로 명시하는 좋은 습관입니다.
두 도구를 함께 package.json의 lint:html 스크립트에 등록해 두면 일관성 있게 실행할 수 있습니다.
// package.json (scripts 일부)
{
"scripts": {
"lint:html": "htmlhint \"src/**/*.html\" && npx vnu --errors-only dist/index.html"
}
}
DevTools Elements·Accessibility 트리로 구조 디버깅
Elements 패널에서 파싱 결과 확인하기
브라우저는 잘못된 마크업을 자체 규칙으로 수정하여 DOM을 구성합니다. 소스에서는 <table> 안에 <div>를 직접 넣었더라도, Elements 패널을 열면 브라우저가 <div>를 <table> 밖으로 이동시킨 실제 DOM이 보입니다. 소스 뷰(Ctrl+U)가 아닌 Elements 패널이 진실을 보여줍니다.
<!-- ❌ 소스 — 잘못된 콘텐츠 모델 -->
<table>
<div class="wrapper">
<tr><td>데이터</td></tr>
</div>
</table>
<!-- ✅ 브라우저가 수정한 실제 DOM (Elements 패널에서 확인) -->
<div class="wrapper"></div>
<table>
<tbody>
<tr><td>데이터</td></tr>
</tbody>
</table>
이처럼 파싱 오류 수정 결과는 예측하기 어렵습니다. 레이아웃이 깨지거나 CSS 선택자가 동작하지 않는다면 Elements 패널에서 실제 DOM을 먼저 확인하세요.
Accessibility 트리 패널 활용
Chrome DevTools Elements 패널의 Accessibility 탭(우측 사이드바)은 보조기기가 읽는 접근성 트리를 시각화합니다. 각 노드의 role, name, description, 상태(expanded, checked 등)를 확인할 수 있습니다.
<!-- 디버깅 대상 예시 -->
<button aria-label="닫기">
<svg aria-hidden="true" focusable="false"><!-- 아이콘 --></svg>
</button>
위 코드를 Elements 패널에서 <button>을 선택하고 Accessibility 탭을 열면 다음과 같이 표시됩니다.
role: button
name: "닫기" ← aria-label 값
focusable: true
role이 generic이나 none으로 표시된다면 시맨틱 태그를 사용하지 않았거나 role="presentation"이 잘못 적용된 것입니다. name이 비어 있다면 보조기기 사용자가 해당 요소의 목적을 알 수 없습니다.
⚠️ 주의
aria-labelledby가 참조하는id가 DOM에 존재하지 않으면name이 계산되지 않아 빈 값으로 표시됩니다. Accessibility 탭은 이 상황을 경고 없이 빈 값으로 보여주므로, 반드시 직접 확인해야 합니다.
Full Accessibility Tree 모드
Chrome 92 이후 DevTools 설정(F1 → Experiments)에서 **"Enable full accessibility tree view in the Elements panel"**을 활성화하면, Elements 패널 자체를 DOM 트리가 아닌 접근성 트리 뷰로 전환할 수 있습니다. 이 모드에서는 presentation이나 none role을 가진 요소가 트리에서 사라지고, 보조기기가 실제로 탐색하는 구조만 표시됩니다. 복잡한 컴포넌트의 접근성 계층을 빠르게 파악하는 데 유용합니다.
axe-core·Lighthouse로 접근성·성능 자동화 점검
axe-core: 접근성 규칙 엔진
axe-core는 Deque Systems가 개발한 오픈소스 접근성 검사 엔진입니다. WCAG 2.1 AA 기준을 포함한 수백 개의 규칙을 실행하며, 브라우저 확장·Node.js·테스트 프레임워크 어디서나 실행할 수 있습니다.
브라우저 콘솔에서 바로 실행하려면 axe DevTools 확장을 설치하거나, axe-core를 페이지에 인라인으로 삽입할 수 있습니다.
// Node.js 환경에서 Playwright와 axe-core 연동
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
// violations 배열이 비어 있어야 테스트 통과
console.log(results.violations);
await browser.close();
# @axe-core/playwright 설치
npm install --save-dev @axe-core/playwright
결과의 violations 배열은 각 위반 항목에 대해 영향을 받는 DOM 노드, 위반 규칙 ID, 수정 방법 링크를 포함합니다.
// axe-core violations 출력 예시 (일부)
{
"id": "color-contrast",
"impact": "serious",
"description": "Elements must meet minimum color contrast ratio thresholds",
"nodes": [
{
"target": [".btn-secondary"],
"failureSummary": "Fix: Ensure the contrast ratio is at least 4.5:1"
}
]
}
Lighthouse: 종합 품질 감사
Lighthouse는 성능·접근성·모범 사례·SEO를 하나의 보고서로 출력합니다. Chrome DevTools의 Lighthouse 탭에서 실행하거나, CLI로 자동화할 수 있습니다.
# Lighthouse CLI 설치
npm install --save-dev lighthouse
# CI에서 점수 임계값 설정
npx lighthouse http://localhost:3000 \
--output json \
--output-path ./lighthouse-report.json \
--chrome-flags="--headless" \
--only-categories=accessibility,performance
Lighthouse 보고서의 접근성 점수는 axe-core 기반이지만, 모든 WCAG 규칙을 커버하지는 않습니다. 보고서에서 각 항목에 "Not applicable", "Pass", "Fail" 세 상태로 표시됩니다. 100점이더라도 axe-core 전체 규칙셋을 별도로 실행하는 것이 좋습니다.
💡 TIP Lighthouse의 "Manually check" 항목은 자동화 도구로 검사할 수 없는 규칙입니다. 예를 들어 "Does the page have a logical tab order?"는 사람이 직접 탭 키로 탐색하면서 확인해야 합니다.
CI에서 Lighthouse 점수 임계값으로 빌드 게이트 설정
// scripts/check-lighthouse.js
import { readFileSync } from 'fs';
const report = JSON.parse(readFileSync('./lighthouse-report.json', 'utf-8'));
const a11yScore = report.categories.accessibility.score * 100;
const perfScore = report.categories.performance.score * 100;
const THRESHOLDS = { accessibility: 90, performance: 80 };
let failed = false;
if (a11yScore < THRESHOLDS.accessibility) {
console.error(`접근성 점수 부족: ${a11yScore} < ${THRESHOLDS.accessibility}`);
failed = true;
}
if (perfScore < THRESHOLDS.performance) {
console.error(`성능 점수 부족: ${perfScore} < ${THRESHOLDS.performance}`);
failed = true;
}
if (failed) process.exit(1);
console.log('Lighthouse 기준 통과');
스크린 리더로 실제 보조기기 동작 검증하기
자동화 도구는 DOM 구조와 ARIA 속성의 논리적 일관성을 검사하지만, 실제 보조기기가 어떻게 읽어주는지는 직접 들어봐야만 알 수 있습니다. 스크린 리더마다 ARIA 해석 방식이 미묘하게 다르기 때문입니다.
주요 스크린 리더 조합
| 스크린 리더 | 권장 브라우저 | 플랫폼 | 특징 |
|---|---|---|---|
| NVDA | Firefox / Chrome | Windows | 무료 오픈소스, 실무 테스트 표준 |
| JAWS | Chrome / Edge | Windows | 기업 환경 점유율 1위, 유료 |
| VoiceOver | Safari | macOS / iOS | OS 내장, 모바일 검증 필수 |
| TalkBack | Chrome | Android | OS 내장, 터치 인터랙션 검증 |
NVDA + Chrome 기본 테스트 절차
1. NVDA 실행 후 Chrome에서 테스트 페이지 열기
2. NVDA+F7 → 요소 목록(링크·제목·폼 컨트롤) 확인
- 페이지 제목 계층(h1→h2→h3)이 논리적으로 구성되어 있는가?
- 링크 텍스트가 "여기를 클릭" 같은 비의미적 텍스트가 아닌가?
3. Tab 키로 인터랙티브 요소 순서 탐색
- 포커스가 논리적 순서로 이동하는가?
- 모달/드롭다운 열릴 때 포커스가 내부로 이동하는가?
4. Browse Mode(삽입 키+스페이스)로 문서 선형 읽기
- 이미지 alt 텍스트가 맥락에 맞게 읽히는가?
- 표의 헤더가 각 셀 읽기 전에 올바르게 발음되는가?
<!-- ❌ 스크린 리더가 "링크, 여기를 클릭" 으로 읽는 경우 -->
<a href="/docs">여기를 클릭하세요</a>
<!-- ✅ 맥락 없이도 목적이 명확한 링크 텍스트 -->
<a href="/docs">HTML 심화 문서 보기</a>
<!-- ✅ 시각적으로는 짧게 유지하고 스크린 리더용 텍스트 추가 -->
<a href="/docs">
문서 보기
<span class="sr-only"> (HTML 심화 강좌)</span>
</a>
/* sr-only: 화면에는 숨기되 접근성 트리에서는 유지 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
⚠️ 주의
display: none이나visibility: hidden은 접근성 트리에서도 요소를 완전히 제거합니다. 보조기기에 읽히면 안 되는 장식 요소에는 사용하되, 화면에 숨기면서 보조기기에는 제공해야 하는 텍스트에는.sr-only패턴을 사용하세요.
시맨틱·ARIA 회귀를 막는 CI 자동화 통합
수동 검사와 로컬 도구 실행만으로는 팀 규모가 커질수록 한계가 생깁니다. PR마다 자동으로 검증이 돌아가도록 CI 파이프라인에 통합하는 것이 핵심입니다.
Jest + axe-core 단위 테스트
컴포넌트 단위에서 접근성 회귀를 막으려면 jest-axe 라이브러리를 사용합니다.
npm install --save-dev jest-axe
바닐라 DOM 노드를 만들어 axe()에 넘기거나, jest-axe의 axe()는 HTML 문자열도 직접 받으므로 문자열을 그대로 전달할 수 있습니다. (프레임워크 컴포넌트를 테스트한다면 @testing-library/react의 render(<Component/>) 결과를 넘기세요.)
// button.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('아이콘 버튼은 접근 가능한 이름을 가져야 한다', async () => {
const container = document.createElement('div');
container.innerHTML = `
<button aria-label="메뉴 열기">
<svg aria-hidden="true" focusable="false">
<use href="#icon-menu"></use>
</svg>
</button>
`;
document.body.appendChild(container);
const results = await axe(container);
expect(results).toHaveNoViolations(); // ✅
});
test('aria-label 없는 아이콘 버튼은 위반이 감지된다', async () => {
// axe()는 HTML 문자열도 직접 받습니다
const results = await axe(`
<button>
<svg aria-hidden="true" focusable="false">
<use href="#icon-menu"></use>
</svg>
</button>
`);
// ❌ button-name 규칙 위반 — violations 배열이 비어있지 않음
expect(results.violations.length).toBeGreaterThan(0);
});
GitHub Actions 워크플로 예시
# .github/workflows/a11y.yml
name: 접근성 및 마크업 검증
on:
pull_request:
paths:
- 'src/**/*.html'
- 'src/**/*.vue'
- 'src/**/*.jsx'
- 'src/**/*.tsx'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Node.js 설정
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 의존성 설치
run: npm ci
- name: HTMLHint 린팅
run: npx htmlhint "src/**/*.html"
- name: 빌드
run: npm run build
- name: W3C Validator (vnu — Java 런타임 필요, ubuntu-latest에 기본 포함)
run: npx vnu --errors-only dist/index.html
- name: Jest 접근성 단위 테스트
run: npm test -- --testPathPattern="a11y|accessibility"
- name: Playwright + axe E2E 접근성 검사
run: npx playwright test tests/a11y.spec.ts
💡 TIP 모든 페이지를 E2E로 검사하면 CI 시간이 길어집니다. 신규 컴포넌트는 jest-axe 단위 테스트로, 크리티컬 페이지(홈·로그인·체크아웃)는 Playwright E2E로 나누어 커버리지와 속도를 균형 있게 관리하세요.
흔한 마크업 안티패턴 탐지와 수정 워크플로
자동화가 잡아주는 오류 이외에도, 코드 리뷰 단계에서 반복적으로 보이는 안티패턴들이 있습니다.
안티패턴 1: 클릭 이벤트를 <div>에 붙이기
<!-- ❌ div는 키보드 접근 불가, 스크린 리더가 버튼으로 인식 못 함 -->
<div class="btn" onclick="submit()">제출</div>
<!-- ✅ 네이티브 버튼 사용 — 포커스·키보드·ARIA role 자동 제공 -->
<button type="button" onclick="submit()">제출</button>
HTMLHint의 id-unique 규칙은 잡지 못하는 패턴입니다. axe-core의 interactive-supports-focus 및 click-events-have-key-events 규칙이 감지합니다.
안티패턴 2: 긍정적 tabindex로 탭 순서 강제
<!-- ❌ tabindex 양수 값은 자연스러운 탭 순서를 깨뜨림 -->
<a href="/home" tabindex="3">홈</a>
<a href="/about" tabindex="1">소개</a>
<a href="/contact" tabindex="2">연락처</a>
<!-- ✅ DOM 순서를 논리적으로 정렬하고 tabindex 제거 -->
<a href="/home">홈</a>
<a href="/about">소개</a>
<a href="/contact">연락처</a>
안티패턴 3: 의미 없는 alt 텍스트
<!-- ❌ 파일명이나 "이미지"를 그대로 쓰는 경우 -->
<img src="hero.jpg" alt="hero.jpg">
<img src="photo.jpg" alt="이미지">
<!-- ✅ 이미지의 정보 내용을 전달하는 alt -->
<img src="hero.jpg" alt="서울 야경을 배경으로 선 개발자들">
<!-- ✅ 장식용 이미지 — 빈 alt로 보조기기 건너뜀 -->
<img src="divider.svg" alt="" role="presentation">
안티패턴 4: 불완전한 폼 레이블 연결
<!-- ❌ placeholder만 있고 label이 없는 폼 -->
<input type="email" placeholder="이메일 입력">
<!-- ❌ label과 input의 for/id가 불일치 -->
<label for="user-email">이메일</label>
<input type="email" id="email">
<!-- ✅ for/id 일치, 또는 label로 input을 감싸는 방식 -->
<label for="user-email">이메일</label>
<input type="email" id="user-email" name="email" autocomplete="email">
수정 워크플로 체크리스트
실무에서는 다음 순서로 진행하면 효율적입니다.
| 단계 | 도구 | 목적 |
|---|---|---|
| 1. 정적 린팅 | HTMLHint, vnu (vnu-jar) | 문법·규칙 위반 즉시 피드백 |
| 2. 단위 테스트 | jest-axe | 컴포넌트별 회귀 방지 |
| 3. E2E 자동화 | axe-core + Playwright | 통합 페이지 접근성 검사 |
| 4. 수동 DevTools | Accessibility 트리 | 의심 영역 즉석 디버깅 |
| 5. 스크린 리더 | NVDA, VoiceOver | 실제 사용자 경험 검증 |
| 6. CI 게이트 | GitHub Actions | PR 병합 전 자동 차단 |
요약
- 정적 검증은 W3C Validator(파싱 스펙 준수)와 HTMLHint(팀 컨벤션 강제)를 함께 사용하여 빌드 단계에서 마크업 오류를 차단한다.
- DevTools Accessibility 트리는 보조기기가 실제로 읽는 role·name·상태를 확인하는 가장 빠른 방법이며,
aria-labelledbyID 불일치 같은 조용한 버그를 잡아낸다. - axe-core는 WCAG 기반 접근성 규칙 엔진으로, jest-axe(단위)와 @axe-core/playwright(E2E) 형태로 테스트 파이프라인에 직접 통합할 수 있다.
- Lighthouse는 접근성·성능 종합 점수를 제공하지만, 모든 접근성 규칙을 커버하지는 않으므로 axe-core 전체 실행을 병행해야 한다.
- 스크린 리더 직접 테스트는 자동화로 대체할 수 없으며, 특히 동적 콘텐츠·라이브 리전·포커스 관리를 검증하는 데 필수다.
- 모든 검증 단계를 CI 파이프라인에 통합하여 PR 병합 전에 회귀를 자동 차단하는 것이 지속 가능한 품질 관리의 핵심이다.
연습문제
- 다음 HTML에는 접근성 문제가 여러 개 있습니다. HTMLHint와 axe-core가 각각 어떤 오류를 감지할지 나열하고, 수정된 코드를 작성하세요.
<div onclick="openMenu()" style="cursor:pointer">
<img src="logo.png">
메뉴 열기
</div>
<input type="text" placeholder="검색어 입력">
<label for="search-btn">검색</label>
<button id="btn">GO</button>
힌트
alt누락,label의for/id불일치,div클릭 이벤트 세 가지 이상을 찾아보세요.
- axe-core를 사용하는 Playwright 테스트를 작성하여 페이지 전체가 WCAG 2.1 AA를 통과하는지 검사하는 코드를 작성하고,
violations가 있을 때 어떤 정보가 출력되는지 설명하세요.
힌트
@axe-core/playwright의AxeBuilder와.withTags(['wcag2aa'])메서드를 활용하세요.
- GitHub Actions 워크플로에서 Lighthouse 접근성 점수가 90점 미만이면 빌드를 실패시키려 합니다. 워크플로 YAML과 점수 체크 스크립트를 작성하세요.
힌트
lighthouse --output json으로 보고서를 저장한 뒤 Node.js 스크립트로categories.accessibility.score를 읽어process.exit(1)을 호출하세요.
<table>내에서 데이터를<div>로 직접 감쌌을 때 브라우저가 DOM을 어떻게 수정하는지, DevTools Elements 패널에서 확인하는 방법과 그 결과가 레이아웃에 어떤 영향을 미치는지 서술하세요.
힌트 브라우저 파싱 오류 복구 규칙을 떠올려 보세요. Elements 패널에서 소스와 실제 DOM의 차이를 비교하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“HTML 심화” 강좌에 대한 댓글입니다.