REPO DEEP DIVE · 2026-07-29 · DEQUELABS/AXE-CORE · 브라우저 안에서 도는 접근성 자동 검사 엔진

dequelabs/axe-core 딥다이브
"규칙 하나하나가 selector + matches + checks"로 조립되는 방식

axe-core는 웹페이지의 접근성(accessibility, 약칭 a11y) 문제를 브라우저 안에서 직접 실행해 찾아주는 테스트 엔진(라이브러리)이다. 사람이 눈으로 훑어보는 대신, "이미지에 대체 텍스트가 있는가", "글자와 배경의 색 대비가 충분한가", "버튼에 이름이 있는가" 같은 규칙 100여 개를 실제 DOM에 대고 돌려 violations(위반) · passes(통과) · incomplete(불확실) · inapplicable(해당없음) 4가지로 분류해 돌려준다.

Deque Systems가 만들고 오픈소스로 공개한 이 엔진은 그 자체로는 화면도, CLI도, 리포트 뷰어도 없다. 다른 도구들이 가져다 쓰는 "심장"이다 — 크롬 개발자도구의 Lighthouse 접근성 감사, axe DevTools 확장, Playwright/Selenium/Cypress용 axe 플러그인들이 전부 이 엔진 위에서 돌아간다.

(저장소 dequelabs/axe-core · 제작·유지보수 Deque Systems · 라이선스 MPL-2.0 · 버전 4.12.1(package.json 기준) · 런타임 의존성 0개(전부 빌드 시 axe.js 한 파일로 번들) · 규칙 파일 105개(lib/rules/*.json, 활성 100개) · 테스트 파일 654개 · 최신 커밋 9a779f1, 2026-07-27)
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 / 누가 쓰면 좋은가

1한 줄 정체

"웹페이지에 대고 접근성 규칙 100여 개를 돌려 4가지로 분류해주는 자바스크립트 엔진"

axe-core는 브라우저의 DOM을 직접 들여다보는 접근성 자동 검사기다. axe.run() 한 줄을 페이지 위에서 실행하면, 지금 화면에 그려진 요소들을 규칙(rule) 100여 개에 대고 하나씩 확인한다. 각 규칙은 "어떤 요소를 볼지(selector) → 그 요소가 이 규칙 대상이 맞는지(matches) → 실제 조건을 만족하는지(checks)"라는 3단 구조로 짜여 있고, 그 결과가 모여 violations(위반) · passes(통과) · incomplete(불확실, 사람이 봐야 함) · inapplicable(해당 요소 없음) 4개 배열로 반환된다.

중요한 건 axe-core 혼자서는 아무 화면도 보여주지 않는다는 점이다. npm으로 설치해 axe.js를 페이지에 넣고 콘솔이나 테스트 코드에서 axe.run()을 호출해야 결과(JSON)를 받는다. 그 JSON을 예쁘게 그려주는 일은 Lighthouse·axe DevTools·Playwright 리포터 같은 상위 도구들의 몫이다.

한 줄 비유

"건물 안전 점검관"이 체크리스트를 들고 층마다 돌며 표시를 남기는 일

axe-core는 체크리스트(규칙 100여 개)를 들고 건물(웹페이지)의 방(DOM 요소) 하나하나를 돈다. "이 문에 손잡이가 있는가", "이 계단에 손잡이가 있는가"처럼 정해진 항목만 확인하고, 각 항목을 합격 / 불합격 / "여기는 사람이 직접 봐야 함" / "이 방에는 해당 없음"으로 표시한다.

점검관은 보고서를 예쁘게 꾸며주지 않는다. 체크 결과 목록만 건네줄 뿐이고, 그걸 그래프로 그리거나 담당자에게 전달하는 건 다른 사람(다른 도구)의 일이다.

2왜 지금 주목받는가

웹 접근성 규제 강화 · "모든 접근성 도구의 뒷단"이라는 사실상 표준 지위 · AI 코드생성 시대의 안전망

axe-core는 2016년 첫 릴리스 이후 꾸준히 쓰여온 프로젝트라 "신생 트렌드"는 아니다. 그런데도 최근 다시 주목받는 이유는 아래 세 갈래로 정리된다.

① 접근성 규제가 전 세계적으로 강화되는 시기

유럽연합의 웹 접근성 지침(EN 301 549), 미국의 ADA(장애인차별금지법) 관련 소송 증가, 한국의 웹 접근성 품질인증 요구 등으로 기업들이 "우리 사이트가 WCAG를 지키는지"를 실제로 자동화된 코드로 증명해야 하는 상황이 늘었다. axe-core의 규칙 태그 체계에는 wcag2a·wcag21aa·section508·EN-301-549 같은 국제 표준 코드가 직접 박혀 있어, "이 코드가 어떤 법적 기준에 대응하는지"를 그대로 추적할 수 있다.

② 사실상 업계 표준 엔진 — 경쟁 도구조차 이걸 쓴다

구글 크롬 개발자도구에 내장된 Lighthouse의 접근성 감사가 실제로는 axe-core 기반 규칙을 가져다 쓰고, Deque 자체 상용 제품(axe DevTools)은 물론 @axe-core/playwright·@axe-core/react·webdriverjs·puppeteer용 공식 래퍼까지 전부 이 저장소 하나의 axe.core 엔진을 감싼 것이다. "접근성 자동 검사"라는 카테고리에서 이 저장소가 사실상 공용 코어 역할을 한다.

③ AI가 코드를 대량 생성하는 시대의 안전망

AI 코딩 도구가 UI 컴포넌트를 빠르게 찍어내는 지금, "빠르게 만든 코드가 스크린리더 사용자·저시력 사용자에게도 쓸 수 있는가"를 사람이 매번 손으로 확인하기는 현실적으로 어렵다. axe-core처럼 CI 파이프라인에 넣어 자동으로 회귀를 잡는 엔진의 필요성이 오히려 더 커졌다.

경쟁 지형
"직접 만든 검사기" 대신 "공용 엔진 위에 얹기"가 대세

Lighthouse·Pa11y·IBM Equal Access 같은 다른 접근성 도구들도 완전히 독자적인 검사 로직을 새로 짜기보다, WCAG 규칙 해석과 DOM 순회라는 어려운 부분은 axe-core 같은 검증된 엔진에 기대는 흐름이 강하다(대안 비교 절에서 상세히 다룬다). 그만큼 "규칙 엔진 자체"가 잘 만들어져 있으면 그 위 생태계 전체가 혜택을 본다.

3무엇을 해결하나

"접근성 검토는 사람이 수동으로, 느리게, 비일관되게 한다"는 문제를 자동화·표준화로 해결

접근성 문제(이미지에 설명 없음, 색 대비 부족, 폼 입력에 라벨 없음 등)는 전문 지식이 있어야 눈에 띄는 경우가 많고, 사람이 수동으로 훑으면 느리고, 사람마다 결과가 다르고, 매번 반복해야 하는 3중고가 있다. axe-core는 이 중 "기계적으로 판단 가능한 부분"을 명확히 잘라내 자동화한다.

해결 ① — 반복 검토를 코드 한 줄로

axe.run() 한 줄이면 화면에 보이는 모든 요소를 규칙 100여 개에 대해 검사한다. 사람이 페이지를 열 때마다 눈으로 훑을 필요 없이, 단위 테스트·통합 테스트 안에 끼워 넣어 매 커밋마다 자동으로 돌릴 수 있다. README의 "Getting started" 예시처럼 results.violations.length가 있으면 테스트를 실패시키는 식으로 CI에 바로 연결된다.

해결 ② — "확신 없는 결과"를 숨기지 않고 따로 분류

많은 접근성 문제는 기계가 100% 확신할 수 없다(예: 이미지의 대체 텍스트가 있긴 한데 그 내용이 적절한지는 사람이 봐야 안다). axe-core는 이런 경우를 억지로 통과·실패로 우기지 않고 incomplete(불확실)라는 별도 칸에 담아 "여기는 사람이 확인하라"고 명시적으로 신호를 준다. 이것이 4번째 결과 분류가 있는 근본 이유다(5번 섹션에서 소스로 자세히 짚는다).

해결 ③ — "이 규칙이 어떤 법적 기준인가"를 코드에 못박기

각 규칙 JSON에는 wcag2a·wcag21aa·best-practice·section508·EN-301-549 같은 태그가 붙어 있어, "이 위반이 WCAG 2.1 AA 기준 위반인지, 그냥 권장 사항(모범 사례)인지"를 코드 레벨에서 구분할 수 있다. 회사 정책이 "WCAG 2.1 AA까지만 강제한다"면 runOnly 옵션으로 그 태그만 골라 돌릴 수 있다.

용어
WCAG (Web Content Accessibility Guidelines)
W3C가 만든 웹 접근성 국제 표준 지침. "웹 콘텐츠 접근성 가이드라인"이라는 뜻이며, A·AA·AAA 세 단계 준수 수준이 있다. 대부분의 법·규제는 AA 수준을 요구한다. axe-core의 규칙 태그(wcag2a, wcag21aa 등)가 바로 이 기준 버전·레벨을 가리킨다.

4전체 구조

"규칙 정의(JSON) + 판정 함수(JS) → Audit(감사 세트) → Context(범위 결정) → 결과 집계" 파이프라인

axe-core의 소스는 lib/ 아래 core(실행 엔진), rules(규칙 정의), checks(판정 함수), commons(공용 로직 — 색·DOM·텍스트 등), standards(ARIA/HTML 명세 데이터) 다섯 축으로 나뉜다. 전체 흐름을 한 장으로 보면 이렇다.

┌─────────────────────────────────────────────────────────────────┐ │ AXE-CORE 실행 파이프라인 │ └─────────────────────────────────────────────────────────────────┘ [사용자 코드] axe.run(context?, options?) │ ▼ [1] lib/core/public/run.js setupGlobals → normalizeRunParams → assert(axe._audit) axe._running 락(lock)으로 동시 실행 방지 │ ▼ [2] lib/core/base/context.js 선택자(context)로 검사 "범위" 결정 + iframe들을 재귀 탐색 → select('frame, iframe', this) 로 프레임 큐에 등록 │ ▼ [3] lib/core/base/audit.js (Audit = 규칙 105개 + 체크 120개 묶음) 각 Rule에 대해 순서대로 실행 │ ▼ [4] lib/core/base/rule.js (규칙 하나) ├ gather() selector 로 후보 DOM 노드 수집 (VirtualNode 래핑) ├ matches() 이 규칙이 이 노드에 실제로 적용되는지 필터링 └ runChecks() any / all / none 체크 목록을 실행 │ ▼ [5] lib/core/base/check.js (체크 하나 — 진짜 판정 로직) evaluate 함수 실행 → true / false / undefined 반환 │ ▼ [6] lib/core/utils/aggregate-checks.js any/all/none 결과를 우선순위로 합쳐 노드 하나의 최종 판정 산출 │ ▼ [7] lib/core/utils/aggregate-result.js + constants.js NA(0)/PASS(1)/CANTTELL(2)/FAIL(3) 우선순위로 violations / passes / incomplete / inapplicable 4묶음 완성 │ ▼ [8] lib/core/reporters/v2.js 결과를 JSON으로 정리해 반환(Promise)

이 구조에서 가장 중요한 관찰은, "규칙(rule)"과 "체크(check)"가 분리돼 있다는 점이다. 규칙은 "어떤 요소를, 어떤 체크 조합으로 판정할지"를 정의한 선언(JSON)일 뿐이고, 실제 판정 코드(진짜 로직)는 체크 쪽에 있다. 체크는 여러 규칙에서 재사용된다 — 예를 들어 aria-label 체크는 이름을 요구하는 규칙 여러 개에서 공유된다.

5핵심 설계·동작 원리

규칙 = selector + matches + checks(any/all/none) · 결과 4분류의 우선순위 산술 · 색 대비 계산 · VirtualNode와 프레임 순회

이 섹션이 이 문서에서 가장 두꺼운 이유는, axe-core를 "그냥 쓰는" 사람과 "왜 이렇게 결과가 나오는지 이해하는" 사람의 차이가 전부 여기 있기 때문이다. 실제 소스 파일과 함수명을 하나씩 짚는다.

① 규칙 하나의 실체 — lib/rules/color-contrast.json

가장 유명한 규칙 하나를 실제 파일 그대로 보자(주석은 설명용으로 추가).

{
  "id": "color-contrast",              // 규칙 고유 ID
  "impact": "serious",                 // 위반 시 심각도
  "matches": "color-contrast-matches", // 이 규칙이 적용될 노드를 거르는 함수
  "excludeHidden": false,
  "tags": [
    "cat.color", "wcag2aa", "wcag143", // WCAG 2 AA / 기준 1.4.3
    "TTv5", "EN-301-549", "ACT"
  ],
  "actIds": ["afw4f7", "09o5cg"],       // W3C ACT 규칙 대응 ID
  "metadata": {
    "description": "...",
    "help": "Elements must meet minimum color contrast ratio thresholds"
  },
  "all": [],
  "any": ["color-contrast"],           // any 목록에 체크 1개
  "none": []
}

selector 필드가 없으면 lib/core/base/rule.jsRule 생성자가 this.selector = spec.selector || '*'로 기본값을 채운다. 즉 규칙 JSON은 "어떤 요소(selector) + 어떤 필터(matches) + 어떤 체크 묶음(any/all/none)"이라는 선언일 뿐이고, 실제 실행은 자바스크립트 코드(Rule.prototype)가 담당한다.

용어
any / all / none
규칙 하나가 여러 체크(check)를 어떻게 조합할지 정하는 3종 목록. any는 "이 중 하나라도 통과하면 됨"(OR), all은 "전부 통과해야 함"(AND), none은 "이 중 하나라도 걸리면 실패"(부정 조건)다. 하나의 규칙이 이 셋을 섞어 복잡한 조건도 표현할 수 있다.

② 규칙 실행 — gather → matches → runChecks

lib/core/base/rule.jsRule.prototype.gatherAndMatchNodes가 실제 흐름을 보여준다.

Rule.prototype.gatherAndMatchNodes = function (context, options) {
  let nodes = this.gather(context, options);      // selector 로 후보 수집
  return nodes.filter(node => {
    return this.matches(node.actualNode, node, context); // matches 함수로 필터
  });
};

그 뒤 Rule.prototype.run은 살아남은 노드마다 ['any', 'all', 'none'].forEach(type => ...)로 세 목록을 각각 runChecks에 넘겨 병렬 실행하고, 노드별로 { any: [...], all: [...], none: [...] } 형태의 원시 결과를 만든다. runChecks 내부에서는 각 체크의 check.run(node, option, context, resolve, reject)를 호출하는데, 이 check.run이 바로 다음 항목이다.

③ 체크 하나의 실체 — evaluate 함수와 true/false/undefined

lib/core/base/check.js의 체크 실행은 checkHelper가 만든 this(data()·relatedNodes()·async() 메서드 제공) 컨텍스트 안에서 evaluate 함수를 돌린다. evaluate가 true를 반환하면 통과, false면 실패, undefined를 반환하면 "확신 못 함"으로 취급된다 — 이 세 번째 값이 바로 incomplete가 생기는 근원지다.

④ any/all/none을 하나의 판정으로 합치기 — 우선순위 산술

세 목록의 개별 결과를 노드 하나의 최종 판정으로 합치는 코드가 lib/core/utils/aggregate-checks.js다. 여기서 핵심은 결과값을 우선순위 숫자로 바꿔 최댓값을 취하는 산술이다.

const checkMap = [];
checkMap[PASS_PRIO] = true;      // 우선순위 1
checkMap[CANTTELL_PRIO] = null;  // 우선순위 2
checkMap[FAIL_PRIO] = false;     // 우선순위 3

// none 타입은 pass/fail 을 뒤집는다 (있으면 실패인 규칙이므로)
if (type === 'none') {
  if (check.priority === PASS_PRIO) check.priority = FAIL_PRIO;
  else if (check.priority === FAIL_PRIO) check.priority = PASS_PRIO;
}

priorities.all  = max(all 체크들의 priority, 0 초기값)
priorities.none = max(none 체크들의 priority, 0 초기값)
priorities.any  = min(any 체크들의 priority, 4) % 4   // any는 "가장 낮은(가장 잘 통과한)" 것 채택

nodeResult.priority = max(all, none, any)  // 셋 중 가장 나쁜(높은) 것이 최종 판정

any는 "하나만 통과해도 됨"이므로 가장 낮은 우선순위(가장 좋은 결과)를 찾고, allnone은 "하나라도 걸리면 안 됨"이므로 가장 높은 우선순위(가장 나쁜 결과)를 찾는다. 그리고 세 그룹 중에서도 다시 최댓값을 취해 "하나라도 나쁘면 전체가 나쁘다"는 논리를 관철한다. 이 코드 한 파일이 "규칙 하나 = selector + matches + checks 조합"이라는 아키텍처의 실제 심장이다.

⑤ 4가지 결과 분류의 실제 정의 — lib/core/constants.js

violations/passes/incomplete/inapplicable이라는 이름이 어디서 나오는지, 소스에 그대로 있다.

const definitions = [
  { name: 'NA',       value: 'inapplicable', priority: 0, group: 'inapplicable' },
  { name: 'PASS',     value: 'passed',       priority: 1, group: 'passes' },
  { name: 'CANTTELL', value: 'cantTell',     priority: 2, group: 'incomplete' },
  { name: 'FAIL',     value: 'failed',       priority: 3, group: 'violations' }
];

lib/core/utils/aggregate-result.js는 각 노드 결과를 이 우선순위에 따라 4개 배열 중 하나로 나눈다. 에러가 났으면 강제로 incomplete, 결과가 NAinapplicable, 그 외엔 해당 그룹 배열에 내용이 있는지로 판단한다.

비유

공항 보안 검색대의 4가지 도장을 떠올리면 쉽다. "통과"(passes), "반입 금지 확정"(violations), "다시 열어봐야 함 — 담당자가 눈으로 확인"(incomplete), "이 검색대에서 다룰 물건이 아님, 예를 들어 액체가 아닌 가방"(inapplicable). 검색대는 애매하면 억지로 통과·금지를 결정짓지 않고 "확인 필요" 도장을 따로 찍는다 — 이게 incomplete가 왜 중요한지의 본질이다.

왜 중요한가
incomplete를 무시하면 "거짓 안전감"에 빠진다

많은 팀이 results.violations.length === 0만 확인하고 results.incomplete는 아예 안 본다. 하지만 axe-core README가 명시하듯 "자동으로 찾을 수 있는 WCAG 문제는 평균 57%뿐"이며, incomplete는 "기계가 확신할 수 없어 일부러 판정을 유보한" 항목이다. 즉 violations가 0이어도 접근성이 완벽하단 뜻이 아니다 — incomplete를 사람이 검토해야 진짜 커버리지가 올라간다.

⑥ 색 대비 계산 — 실제 공식이 그대로 코드에 있다

color-contrast 체크의 핵심 수학은 lib/commons/color/get-contrast.js 한 파일에 있다.

function getContrast(bgColor, fgColor) {
  if (!fgColor || !bgColor) return null;
  if (fgColor.alpha < 1) {
    fgColor = flattenColors(fgColor, bgColor);   // 반투명이면 배경과 합성
  }
  const bL = bgColor.getRelativeLuminance();      // 상대 휘도(밝기)
  const fL = fgColor.getRelativeLuminance();
  return (Math.max(fL, bL) + 0.05) / (Math.min(fL, bL) + 0.05); // WCAG 공식
}

이건 WCAG 2 명세에 나오는 상대 휘도 대비 공식 그 자체다. 이렇게 구한 비율은 has-valid-contrast-ratio.js에서 글자 크기·굵기에 따라 4.5:1(작은 글자) 또는 3:1(큰 글자·굵은 글자) 기준과 비교된다. 실제로는 이 앞에 get-foreground-color.js·get-background-color.js·flatten-colors.js·stacking-context.js 같은 파일들이 "이 텍스트의 진짜 전경색·배경색이 뭔지"(그림자, 겹친 레이어, 투명도까지 고려)를 먼저 알아내는 훨씬 복잡한 선행 작업을 한다 — 색상 연산 자체는 devDependency인 colorjs.io 라이브러리가 담당한다.

함정
jsdom(Node.js 가짜 브라우저)에서는 color-contrast가 안 된다

README가 직접 명시한다 — "jsdom에 대한 지원은 제한적이며, 특히 color-contrast 규칙은 jsdom에서 동작하지 않는 것으로 알려져 있다." 실제 컴퓨티드 스타일·레이어링 렌더링이 필요한 체크라 진짜 브라우저 렌더 엔진이 있어야 한다. Node 스크립트에서 axe-core를 돌릴 때 이 규칙만 유독 안 잡히면 당황하지 말 것 — 설계상 한계다. (실제로 최신 커밋도 "jsdom 30이 Node 20 지원을 끊어서 테스트 매트릭스를 손봤다"는 내용이라, 이 jsdom 경계는 지금도 계속 관리 중인 영역이다.)

⑦ VirtualNode — 진짜 DOM과 판정 로직 사이의 완충지대

axe-core는 규칙·체크가 진짜 브라우저 Node 객체를 직접 쥐지 않고, lib/core/base/virtual-node/AbstractVirtualNode를 상속한 VirtualNode(진짜 DOM용)·SerialVirtualNode(직렬화된 가짜 DOM용, 다른 프레임에서 넘어온 데이터를 재구성할 때 사용)로 감싸 쓴다. hasClass() 같은 공통 헬퍼를 여기 한 곳에 두고, "노드가 진짜 DOM인지 프레임 너머에서 온 데이터인지"를 규칙 코드가 신경 쓰지 않아도 되게 만든다.

⑧ 프레임(iframe) 순회 — 무한 깊이까지 재귀

lib/core/base/context.jsselect('frame, iframe', this)로 현재 범위 안의 모든 프레임을 찾고, pushUniqueFrame()화면에 보이지 않는(스크린리더에 노출 안 되는) iframe은 건너뛰며 나머지를 context.frames 큐에 쌓는다. 각 프레임은 createFrameContext()로 별도 Context를 만들어 재귀적으로 같은 파이프라인을 탄다. README의 매니페스토에 있는 "무한 깊이의 iframe을 지원한다"는 문장이 정확히 이 코드로 구현돼 있다.

같은 창(window) 안의 프레임은 lib/core/utils/respondable.jspostMessage 기반으로 부모↔자식 프레임 간에 결과를 주고받는다. 반면 Playwright·Selenium처럼 브라우저를 외부에서 원격 조종하는 자동화 도구는 postMessage 왕복이 번거로우므로, lib/core/public/run-partial.js + finish-run.js라는 별도 경로를 쓴다 — 각 프레임에서 axe.runPartial()을 개별 실행해 부분 결과를 얻고, 최상위에서 axe.finishRun()으로 그 부분 결과들을 한 번에 병합한다. "같은 결과를 얻는 두 가지 경로"가 실제로 코드에 나뉘어 있다는 것 자체가 이 엔진이 다양한 실행 환경(브라우저 내부 vs 자동화 드라이버)을 어떻게 다르게 다루는지 보여주는 좋은 예다.

용어
Context
이번 검사가 어디까지를 범위로 하는지 정의하는 객체(include/exclude 선택자, 프레임 목록 등). axe.run()에 아무것도 안 주면 문서 전체가 기본 Context가 된다.

6기술 스택·의존성

런타임 의존성 0개(전부 번들) · esbuild 빌드(Grunt 아님) · colorjs.io·css-selector-parser·doT 내장

package.json을 열어보면 놀라운 점이 하나 있다 — dependencies 필드가 아예 없다. npm으로 axe-core를 설치해도 딸려오는 런타임 패키지가 0개다. 대신 필요한 라이브러리는 전부 빌드 시점에 최종 산출물(axe.js/axe.min.js) 안으로 번들된다.

번들 내장 라이브러리역할
colorjs.io색 대비 계산의 색 공간 연산(휘도, RGB 변환)
css-selector-parser사용자가 준 CSS 선택자를 파싱
@deque/dotdoT 템플릿 엔진 — 체크 실패 메시지에 변수 끼워넣기
emoji-regex텍스트 안의 이모지를 인식(대체 텍스트 판정 등에 사용)
memoizee같은 계산을 반복하지 않도록 메모이제이션(결과 재사용)
core-js-pure구형 브라우저(IE11 등)를 위한 Array.from 등 폴리필

이 목록은 lib/core/imports/index.js에 그대로 나열돼 있고, 전부 package.json에는 devDependencies로만 등록돼 있다. 즉 "버전의 진실원"은 이 저장소의 package.jsonversion 필드(현재 4.12.1) 하나뿐이며, 이 값이 빌드 스크립트를 통해 axe.js 파일 맨 위 주석(/*! axe v4.12.1 ...*/)과 각 규칙 도움말 URL(dequeuniversity.com/rules/axe/4.12/...)에 그대로 찍힌다.

빌드 도구 — 문서와 실제가 어긋나는 지점

함정 — 문서/블로그와 실제 코드의 불일치
"Grunt로 빌드한다"는 오래된 정보는 이제 틀렸다

인터넷에 떠도는 오래된 자료·튜토리얼은 axe-core가 Grunt로 빌드된다고 설명하는 경우가 많다(실제로 예전 버전은 Gruntfile.js를 썼다). 하지만 지금 소스에는 Gruntfile이 없고, build/run-build.mjs 파일 맨 위 주석에 이렇게 적혀 있다: /** axe-core build driver (replaces Gruntfile.js). */. 즉 Grunt는 esbuild 기반의 자체 빌드 스크립트로 완전히 대체됐다. "Grunt task를 어떻게 커스터마이징하나"를 찾으러 왔다면 이 저장소에서는 헛수고다 — build/run-build/ 아래의 커맨드 파서·풀빌드·번역·검증 로직을 봐야 한다.

빌드 산출물 — IIFE 하나로 포장

최종 axe.jslib/intro.stub(라이선스 헤더 + IIFE 시작)과 lib/outro.stub(IIFE 종료)으로 모든 소스를 감싼 즉시실행함수(IIFE) 한 덩어리다. <script src="axe.js"> 태그 하나만 넣으면 전역에 window.axe가 생기는 이유가 여기 있다 — 모듈 로더가 없던 시절부터 이어진, 어떤 환경에서도 그냥 붙여넣으면 도는 설계다.

기술 스택 요약표

영역내용
언어순수 JavaScript(ESM 소스, esbuild로 번들) + 타입 정의 axe.d.ts(TypeScript 사용자용)
빌드esbuild 기반 build/run-build.mjs(Grunt 대체) — build/configure/translate/validate 서브커맨드
테스트@web/test-runner(단위) · Mocha + Selenium/Chromedriver(통합) · jsdom(Node 환경 일부 테스트)
다국어locales/*.json 20개 언어(한국어 ko.json 포함) — 규칙 메시지만 번역, 엔진 자체는 언어 무관
배포npm 패키지 axe-core(files: axe.js, axe.min.js, axe.d.ts, locales/) · release.ymlcommit-and-tag-version으로 버전 태깅
용어
IIFE (Immediately Invoked Function Expression)
"즉시 실행 함수 표현식" — 정의하자마자 바로 실행되는 함수. 전역 변수 오염을 최소화하면서도 모듈 시스템 없이 스크립트 태그 하나로 전역 객체(여기선 window.axe)를 노출하는 오래된 표준 패턴이다.

7설치·요구사항

npm install 한 줄 · 지원 브라우저 명시 · Node/jsdom 제한 사항 숙지
항목요구사항
설치npm install axe-core --save-dev — 런타임 의존성 0개라 설치가 매우 빠르고 가볍다
페이지에 삽입<script src="node_modules/axe-core/axe.min.js"></script> — 검사하려는 모든 iframe 안에도 넣어야 한다
실행axe.run().then(results => ...) — Promise 기반, 콜백 방식도 지원
Node.js 개발 환경engines.node>=4로 넓게 열려 있지만, 실제 개발·테스트는 최신 LTS + jsdom 조합을 요구(최근 커밋에서 jsdom 30이 Node 20 지원을 끊어 테스트 매트릭스를 조정)
지원 브라우저Edge 40+, Chrome 42+, Firefox 38+, Safari 7+. IE11은 지원 종료(DEPRECATED) 명시. CI에서는 Chrome·Firefox만 매 PR마다 자동 테스트
jsdom(Node 안의 가짜 브라우저)제한적 지원 — color-contrast 규칙은 공식적으로 동작 보장 안 됨
가볍게 시작하는 팁
직접 스크립트 태그로 붙이지 말고, 통합 라이브러리로 시작하라

실무에서는 axe-core를 <script> 태그로 직접 넣는 경우보다, @axe-core/playwright@axe-core/webdriverio 같은 공식 래퍼로 시작하는 편이 훨씬 편하다. 이 저장소는 "엔진"만 제공하고, 저 래퍼들이 "브라우저에 스크립트 주입 + 실행 + 결과 리포트"까지 대신 해준다.

8실제 사용 흐름

CI 테스트에 끼워 넣기 → violations 확인 → incomplete 수동 검토 → 옵션으로 범위/기준 조정

가장 흔한 실전 흐름은 "기능 테스트를 짜는 김에 접근성 검사도 같이 돌리는" 방식이다. 아래는 README에 있는 최소 예시를 뼈대로 한 사용 순서다.

1단계 — 검사 실행

axe
  .run()
  .then(results => {
    if (results.violations.length) {
      throw new Error('접근성 문제 발견');    // 위반이 있으면 테스트 실패 처리
    }
  })
  .catch(err => {
    console.error('실행 중 오류:', err.message);
  });

초보자를 위한 한 줄 풀이: "지금 화면에 대고 검사를 돌려서(run), 다 끝나면(then) 위반 목록(violations)이 하나라도 있으면 에러를 던지고, 검사 자체가 실패하면(catch) 그 이유를 콘솔에 찍어라"는 뜻이다. 복사해서 테스트 파일 안에 그대로 붙여도 동작한다.

2단계 — 결과 4묶음을 각각 다르게 다루기

const { violations, incomplete, passes, inapplicable } = results;
console.log('위반(반드시 고쳐야 함):', violations.length);
console.log('불확실(사람이 확인):', incomplete.length);   // 여기를 무시하지 말 것
console.log('통과:', passes.length);
console.log('해당없음:', inapplicable.length);

3단계 — 범위·기준을 옵션으로 좁히기

axe.run(document, {
  runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] }, // WCAG 2 A/AA만
  exclude: [['#legacy-widget']]                            // 낡은 위젯은 검사 제외
});

runOnly에 태그를 넘기면, 그 태그가 붙은 규칙 JSON만 골라 Audit이 구성된다(4번 섹션의 태그 체계가 바로 이렇게 실전에서 쓰인다). 회사 정책이 "베스트 프랙티스는 참고만, 법적 기준(WCAG 2 A/AA)만 강제"라면 이렇게 좁혀 CI를 더 엄격하지 않게 통제할 수 있다.

4단계 — 자동화 프레임워크에서는 래퍼를 통해

Playwright라면 @axe-core/playwrightAxeBuilder가 "페이지에 axe.js 주입 → runPartial/finishRun으로 프레임까지 처리 → 결과 반환"을 대신 해준다. 이 저장소 자체를 직접 만질 일은, 웹앱을 만드는 대부분의 팀에게는 거의 없다 — 대신 저 래퍼들의 옵션(포함/제외 선택자, 태그 필터)을 다루게 된다.

9함정·오해 방지

"자동 검사 = 완전한 검사"라는 착각, 동시 실행 락, 폐지된 규칙, 색 대비의 숨은 전제

오해 ① — "violations가 0이면 우리 사이트는 접근성이 완벽하다"

README가 직접 못박은 숫자가 있다: "axe-core로 평균 WCAG 이슈의 57%를 자동으로 찾을 수 있다." 나머지 43%는 애초에 기계가 판단할 수 없는 영역(문맥적 의미, 실제 사용성, 논리적 읽기 순서의 자연스러움 등)이다. axe-core는 스스로도 "제로 거짓양성(zero false positives)"을 목표로 내세우지만, 동시에 "발견하지 못하는 진짜 문제(false negative)"가 훨씬 많다는 걸 인정하는 셈이다. 자동 검사 통과는 "최소한의 바닥은 다졌다"는 뜻이지 "접근성 인증"이 아니다.

오해 ② — axe.run()을 동시에 두 번 부를 수 있다

lib/core/public/run.jsaxe._running 플래그로 동시 실행을 원천 차단한다. 이미 실행 중일 때 다시 axe.run()을 부르면 "Axe is already running..." 에러가 던져진다. 비동기 테스트 코드에서 await를 빼먹고 두 검사를 동시에 걸면 이 에러를 마주친다 — axe-core 버그가 아니라 의도된 안전장치다.

오해 ③ — 105개 규칙이 전부 항상 실행된다

lib/rules/ 안의 JSON 파일은 105개이지만, 이 중 duplicate-id·duplicate-id-active·audio-caption·aria-roledescription·landmark-complementary-is-top-level 5개는 파일 안에 "enabled": false가 박혀 있는 폐지(deprecated) 규칙이다. 기본 실행에서는 100개만 돈다. 오래된 예제 코드에서 저 규칙 이름을 보고 "지금도 기본으로 도는 규칙이겠지" 오해하면 안 된다 — 하위 호환을 위해 코드만 남겨둔 것이다.

오해 ④ — 색 대비 규칙은 "색상 값"만 보면 계산된다

실제로는 get-foreground-color·get-background-color가 텍스트 그림자, 겹친 레이어(stacking context), 반투명도까지 역추적해야 색이 확정된다. 이 과정에서 값을 확정 지을 수 없으면(예: 배경이 이미지라 실제 픽셀 색을 알 수 없을 때) 이 체크도 incomplete로 떨어진다 — "색 대비 위반이 안 뜨니 통과"가 아니라 "색을 특정 못 해 사람이 봐야 하는" 경우가 섞여 있을 수 있다.

주의
이 저장소를 고쳐도 당신 사이트의 axe 결과는 안 바뀐다

많은 개발자가 규칙 메시지나 판정 로직을 커스터마이징하고 싶어 dequelabs/axe-core 저장소를 포크해 수정한다. 하지만 실제 서비스에 쓰이는 건 npm에 배포된 빌드된 axe.js이므로, 소스를 고쳤다면 반드시 npm run build로 다시 번들링해 그 결과물을 배포 파이프라인에 반영해야 한다. 소스만 고치고 그대로 두면 아무 변화가 없다.

10대안 비교

Lighthouse a11y · Pa11y · IBM Equal Access · WAVE — "완전히 다른 엔진"이 아니라 "같은 엔진의 다른 포장"인 경우가 많다
도구axe-core와의 관계 · 특징
Lighthouse
(a11y 카테고리)
구글 크롬 개발자도구 내장 감사 도구. 접근성 카테고리의 규칙 다수가 실제로 axe-core를 내부에서 가져다 쓴다(doc/projects.md에 axe-core를 쓰는 커뮤니티 프로젝트로 Lighthouse가 직접 명시돼 있다). 성능·SEO 등 다른 카테고리까지 한 번에 보고 싶을 때 유리하지만, 접근성만 놓고 보면 axe-core의 부분집합에 가깝다.
Pa11y독자적인 CLI/Node 도구로, 내부적으로 axe-core 또는 HTML_CodeSniffer 엔진을 선택해 쓸 수 있는 구조. "명령줄에서 URL 하나 던지면 리포트가 나온다"는 접근성에 강해, CI에 붙이기 쉬운 경량 래퍼로 자주 쓰인다.
IBM Equal Access
(accessibility-checker)
IBM이 만든 독자 엔진 + 브라우저 확장. axe-core와 별개의 규칙셋을 직접 구현했고, IBM 자체 접근성 요구사항(트러스티드 접근성 체크리스트)에 특화된 규칙이 강점이다. axe-core보다 규칙 수는 적지만 대기업 내부 정책과의 정합성을 노린 도구.
WAVE
(WebAIM)
브라우저 확장/웹 서비스 형태로, 결과를 페이지 위에 아이콘으로 시각적으로 오버레이해 보여주는 게 특징. 자동 판정 엔진은 자체 개발이며, "코드가 아니라 화면으로 바로 이해하고 싶은" 디자이너·기획자에게 특히 친절하다. CI 자동화보다는 수동 점검·교육용으로 강하다.
선택 기준
"CI에 박아 자동화"할 거면 axe-core(또는 그 래퍼), "눈으로 훑어보며 배울" 거면 WAVE

axe-core는 테스트 코드·CI 파이프라인에 통합하는 용도로 가장 강하다(같은 검사를 매 배포마다 반복하는 데 최적화). Lighthouse는 "성능까지 한 번에 종합 점검"하고 싶을 때, Pa11y는 "가벼운 CLI로 URL만 검사"하고 싶을 때, IBM Equal Access는 "회사 내부 접근성 정책과 정확히 맞춰야" 할 때, WAVE는 "처음 접근성을 배우며 화면으로 직관적으로 확인"하고 싶을 때 적합하다. 이 넷은 서로 배타적이지 않다 — 실무에서는 CI에는 axe-core 계열, 수동 리뷰에는 WAVE처럼 병행하는 경우가 흔하다.

11정리 / 누가 쓰면 좋은가

"접근성 회귀를 코드로 자동 감시하고 싶은" 모든 프론트엔드 팀에게

axe-core는 "규칙(selector+matches+checks) → 우선순위 산술로 4분류 → 상위 도구가 소비"라는 단순하지만 정교한 파이프라인 하나로, 웹 접근성 자동 검사라는 분야의 사실상 표준 엔진이 됐다. 런타임 의존성 0개로 가볍게 배포되고, 105개 규칙·20개 언어·무한 깊이 iframe 지원까지 갖춘 성숙한 프로젝트다.

이 저장소를 직접 들여다볼 가치가 있는 사람은 다음과 같다.

반대로, 단순히 "우리 사이트 접근성 검사만 한 번 돌려보고 싶다"면 이 저장소 소스를 직접 읽을 필요 없이 @axe-core/playwright 같은 통합 라이브러리나 axe DevTools 브라우저 확장으로 충분하다. 이 문서는 "그 도구들 안에서 실제로 무슨 일이 벌어지는지" 궁금한 사람을 위한 것이다.