시작 — 리서치에서 세운 설계
공공 웹은 범정부 디자인 시스템 KRDS와, 33개 검사항목으로 된 웹 접근성 국가표준 KWCAG 2.2를 함께 지켜야 한다. 지난 글에서는 이 둘을 잇는 도구를 조사하다 방향을 바꿨다. 자동 검사 엔진 axe로 위반을 찾는 “검사 도구”가 아니라, KWCAG 충족 조건이 들어간 KRDS 마크업을 페이지를 만드는 시점에 공급하는 “적용 도구”다.
그때 세운 설계는 세 가지였다.
- 매핑표: 컴포넌트마다 KWCAG 33항목을 “자동 충족”, “조건부”, “범위 밖” 세 부류로 가른다
- 필수 입력 강제: 조건부 항목은 쓰는 사람이 채워야 하니, 안 채우면 마크업을 주지 않는다
- 3층 구조: 1층은 생성 시점 공급, 2층은 PR마다 회귀 검사, 3층은 제출 전 전수 보고서. 검사 엔진은 axe를 재사용하고, 그 위에는 KWCAG 번호 번역과 KRDS 처방만 얹는다
한 달 동안 이 설계를 MCP 서버로 구현해 v0.7.0까지 일곱 번 배포했다. 서버는 부품·템플릿을 주는 공급 도구와 완성 페이지를 다시 검사하는 도구를 낸다. 검증은 AI 에이전트에게 이 서버를 쥐여 주고 정적 HTML·JSP·React(Next.js 포함) 페이지를 만들게 하는 실사용 테스트로 네 차례 했다. 설계대로 된 것과 틀린 것을 정리했다.
매핑표 — 근거 없이 “자동 충족”이라 쓰지 않는다
43개 컴포넌트를 전수 분류한 결과는 자동 충족 130건, 조건부 89건, 범위 밖 132건, 미확정 40건이다. 문서로 먼저 쓰고, 빌드 스크립트가 코드로 변환한다. 문서와 코드가 어긋나면 테스트가 깨진다.
예상과 달랐던 건 “자동 충족”이라고 주장하기가 생각보다 어려웠다는 점이다. 판정마다 공식 킷의 파일과 행 번호를 근거로 달게 했더니 규칙이 필요해졌다.
- 화면에 안 보이는 sr-only 텍스트는 “색에 무관한 인식”의 근거가 될 수 없다
- 킷이 스스로 데모라고 표시한 코드에 기대는 동작은 자동 충족이 아니다
- 레이블은 연결 구조만 자동 충족이고, 내용은 언제나 조건부다
확신이 없으면 자동 충족으로 올리지 않고 미확정으로 남겼다. 잘못 붙인 자동 충족은 도구가 거짓 보증을 하게 만들기 때문이다.
필수 입력 강제 — 돌았지만 구멍이 있었다
부품 공급 도구에 레이블 없이 입력란을 요청하면 이렇게 거부한다.
# 텍스트 입력 필드 (krds:text_input)
## 필수 입력이 채워지지 않아 마크업을 제공하지 않는다 (3건)
- labelText — kwcag:7.3.2 레이블 제공
- inputId — kwcag:7.3.2 레이블 제공
(요약)
실사용에서도 강제는 돌았다. 에이전트들은 필수 입력 없이 부품을 불렀다가 거부 안내를 받고, 값을 채워 다시 불렀다.
다만 구멍이 세 개 있었다. 매핑표의 적용 범위 표기를 변형 이름으로 잘못 읽어, 모달 같은 몇몇 부품은 기본 호출에서 필수 입력이 강제되지 않았다. “모달을 배경 밖에 둔다” 같은 확인용 입력에 false를 줘도 통과했다. 목록 도구가 세는 필수 입력 수와 실제로 막는 수가 달라 다섯 번 지적받았다. 셋 다 v0.7.0에서 같은 판정 함수를 쓰게 고쳤다.
또 하나는 강제가 막지 못하는 경로였다. 에이전트들은 값을 넘기고도 킷 데모 마크업을 받아 id·레이블·href를 손으로 다시 쓰고 있었다. 그래서 입력값을 교정본 구조에 넣은 마크업을 도구가 직접 만들게 했고(6종), 재확인한 3세션에서는 다시 쓴 컴포넌트가 0이었다.
axe 연동 — 번역은 쉬웠고 사각지대가 컸다
axe 결과를 KWCAG 번호로 옮기는 번역표는 완전 1 · 부분 19 · 불가 13항목이다. 불가 13항목은 리포트에 수동 체크리스트로 내보낸다.
번역보다 큰 일은 axe가 못 보는 부분이었다. 공식 킷 CSS는 아이콘 경로를 206곳에서 잘못 적어 404가 났는데 axe는 조용했다. 그 404를 메우려고 넣은 교정 CSS가 태그 삭제 버튼에 마스크를 다시 씌워 버튼이 통째로 사라졌을 때도 통과했다. 셀렉트 글자색 대비 3.08:1은 판정을 보류만 했다.
그래서 직접 재는 모듈을 하나씩 붙여 왔다.
- 보류된 대비는 글자색과 배경색으로 계산한다. 셀렉트 미달을 처음 잡은 것도 이 실측이다
- 화면에 안 그려진 조작 요소는 전체 스크린샷 픽셀로 찾는다
- 키보드는 실제로 Tab을 눌러 순회하고, 탭 화살표·모달 Escape 같은 위젯 조작도 실제 키로 확인한다
3층은 어디까지 — 2층이 바뀌었다
1층(MCP 공급)과 3층(리포트 생성기)은 계획대로 만들었다. 재검사 도구는 3층 리포트 생성기를 그대로 쓴다.
바뀐 건 2층이다. “완성하면 검사하라”는 지시는 따를 때도 건너뛸 때도 있었다. 그래서 PR 시점 CI보다 먼저, AI가 작업을 끝내려는 순간을 막았다. Stop 훅이 검사 기록을 읽어, 위반이 남았거나 검사 뒤에 소스가 바뀌었으면 종료를 거부한다.
// 프로젝트 .claude/settings.json (요약)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npx -y @donghok/krds-a11y-mcp stop-hook",
},
],
},
],
},
}
판정은 gate 명령 한 곳에 두었다. 서버 쪽 CI는 사내 CI 환경이 준비될 때까지 보류했다.
설계 밖에서 틀린 예상
앞의 설계 항목 말고도 틀린 예상이 두 개 더 있었다.
- 공식 샘플을 따르면 된다: 공식 패턴 샘플 33종 모두에서 문제가 나왔다. 전 샘플에
main랜드마크가 없었고 화면 확대를 막아 두었다. 템플릿은 샘플 구조를 따르되 교정분은 고정했다 - 기능을 더하면 좋아진다: 4회차 실사용의 결함 3건이 전부 그 회차의 새 기능에서 나왔다. 결함만 고쳐 v0.7.0을 배포하고 기능 추가를 멈췄다
회고
설계의 중심은 맞았다. 필수 입력을 채우지 않으면 마크업을 주지 않으니, 도구로 받은 부품에서는 레이블 누락 같은 위반을 생성 단계에서 막을 수 있었다. 틀린 건 설계 아래 깔린 가정들이었다. 매핑표는 근거 규칙이 있어야 섰고, 강제에는 구멍이 있었고, axe에는 사각지대가 있었고, 지시는 훅이 되어야 돌았다.
공식 샘플도, axe도, 지시문도, 새 기능도 확인 전까지는 가설이었다. 믿을 수 있었던 건 직접 재고, 써 보고, 강제한 결과뿐이었다.