AX 노트 · 측정

대비 검사기는 텍스트 조각 40%를 재지 못하고도 통과를 냈다: '몇 개를 측정했나'를 출력하게 바꾸기

세 테마 대비 검사의 통과는 글자 40%를 재지 않고 낸 결과였습니다. 이제 검사기는 잰 개수와 버린 이유를 출력하고, 잰 것이 선언보다 적으면 실패합니다.

작성 SugarMount 검증과 출시 읽는 시간 10분 최종 업데이트

라이트·다크·노을 테마에서 모두 대비 검사가 통과했다면, 화면에 보이는 글자를 빠짐없이 쟀다는 뜻이라고 믿고 있었습니다. 2026년 9월 10일 P2P 채팅 앱(Flutter)의 커밋 8dacf19에서 검사기 자체를 세어 보니, 텍스트 조각 9,318개 중 3,762개(40.4%)가 대비 수치 없이 빠져 있었고, 고치기 전 검사기는 그중 어느 것도 제외했다고 알리지 않았습니다. 같은 커밋 본문이 요약한 전수 목록에서는 화면 상태 245개 중 실제 테마로 그려 본 것이 70개였고, 조작 116개 중 58개는 테스트도 사람 손도 닿은 적이 없었습니다.

근거는 P2P 채팅 앱 저장소의 커밋 67edc8d·8dacf19·d01cc23(모두 2026-09-10) 본문과 검증 문서입니다. 화면 상태·조작·문자열 전수 목록은 67edc8d에, 하니스 무결성 보고서는 8dacf19에 들어갔습니다. 모든 숫자는 2026년 10월 21일 저장소에서 다시 확인했습니다.

목차 (7)
  1. 9,318개 중 대비 수치가 나온 조각은 5,556개였다
  2. 완결성 검사는 파일 단위라 상태 209개를 볼 수 없었다
  3. 다시 재자 SnackBar 3.28:1과 전송 버튼 36.5dp가 나왔다
  4. 검사기를 만든 AI가 놓친 것과 운영자의 작업 규칙
  5. 비활성 버튼 면제는 숨을 곳이 되지 않게 넣었다
  6. 직접 재 보려면
  7. 자주 묻는 질문

9,318개 중 대비 수치가 나온 조각은 5,556개였다

버려진 조각 대부분은 버려야 맞는 것이었지만, 버렸다는 기록이 어디에도 없었습니다. 아래 표는 고친 검사기로 3테마 × 149케이스(447회 실행)를 돌려 사유별로 센 값이고, 오른쪽 칸은 같은 조각을 고치기 전 검사기가 어떻게 처리했는지 적은 것입니다.

구분조각 수고치기 전 검사기
위젯 트리에서 찾은 텍스트 조각9,318
대비 수치가 실제로 나온 조각5,556 (59.6%)판정
모달 배리어 뒤(다이얼로그·시트가 덮은 화면)1,575보고 없이 버림
이모지·그림문자876보고 없이 버림
캡처 경계 밖(스크롤로 밀려난 줄)867보고 없이 버림
조상 클립 경계에 걸친 줄270클립 밖 픽셀까지 섞어서 잼
SnackBar·진행 스크림 아래(가려짐)174대비 실패로 보고
한 픽셀도 안 그려진 조각0(정착 프레임을 끄면 7)

커밋 본문은 3,762개(40%)를 "보고 없이 버렸다"고 적었지만, 표를 다시 더해 보면 3,318개는 조용히 사라졌고 444개는 버려지는 대신 엉뚱한 픽셀로 재졌습니다.

막대그래프: 텍스트 조각 9,318개 중 5,556개만 대비 수치가 나왔고, 배리어 뒤 1,575, 이모지 876, 캡처 밖 867개는 빠졌으며 270개는 잘못 쟀고 174개는 가려진 채 실패로 보고됐다.
대비 검사기가 처리한 텍스트 조각 9,318개의 내역. 3,762개(40.4%)가 제대로 측정되지 않았습니다.

조각이 빠지는 길은 여러 곳이었습니다. 배리어 뒤 문단은 수집 단계에서, 이모지는 continue로, 화면 밖 조각은 _evaluate의 return null로 빠졌고, 어느 길도 개수를 남기지 않았습니다. 마지막 길은 이 두 줄입니다.

final finding = _evaluate(f, bitmapA, bitmapB);
if (finding != null) findings.add(finding);

_evaluate가 null을 돌려주면 조각은 그 자리에서 없어졌고, 호출자에게는 "실패 0건"만 남았습니다.

완결성 검사는 파일 단위라 상태 209개를 볼 수 없었다

화면 목록을 지키는 메타 테스트도 같은 모양이었습니다. 이 테스트는 lib/의 화면 파일 목록과 테마 매트릭스 카탈로그를 대조하고, 카탈로그에 없는 파일이 있으면 실패합니다. 비교 단위가 파일이라, 파일이 카탈로그에 한 번 나오면 그 안에 상태가 몇 개 있든 통과했습니다. 그 무렵 카탈로그 20케이스가 그린 상태는 245개 중 36개(15%)였고, 나머지 209개는 메타 테스트가 구조상 잡을 수 없었습니다.

이번 라운드는 합계 대신 코드에서 전수 목록부터 뽑았습니다.

전수 목록전체확인된 것빈 곳
화면 상태(표면 51개의 조건 분기)245실제 앱 테마로 렌더 70테마 렌더 없음 175
조작(릴리스 빌드에서 누를 수 있는 컨트롤)116테스트나 기기에서 눌림 58한 번도 안 눌림 58
화면에 보이는 문자열464근거가 하나라도 있음 332근거 없음 132(안전 문구 30)

고친 메타 테스트는 상태 단위로 대조하고, 상태를 제외하려면 사유를 적게 합니다. 죽은 표면을 지우면서 총계는 239개로 줄었고, 카탈로그는 149케이스로 늘었습니다. (죽은 코드를 렌더하던 테스트 3건이 "3테마 통과" 개수를 부풀리고 있었다는 것도 이때 드러났습니다.)

비교 그림: 고치기 전 검사기는 재지 못한 조각을 보고 없이 버리고 카탈로그가 상태 245개 중 36개만 그렸으며, 고친 검사기는 잰 개수와 버린 이유를 출력하고 잰 것이 선언보다 적으면 실패하며 카탈로그가 149케이스로 늘었다.
고친 검사기는 잰 개수와 버린 이유를 같이 출력하고, 잰 것이 선언보다 적으면 실패합니다.

다시 재자 SnackBar 3.28:1과 전송 버튼 36.5dp가 나왔다

채팅 화면의 오류 SnackBar는 배경색만 주고 글자색을 주지 않았습니다. 그 결과 라이트·노을 테마에서 대비가 3.28:1이었습니다(기준 4.5:1). 흰 글자와 더 어두운 빨강 배경으로 바꾸자 6.54:1이 됐습니다. 전송 버튼은 탭을 받는 InkWell이 축소 애니메이션(Transform.scale) 안에 있어서, 그림과 함께 손가락이 닿는 영역까지 36.5×36.5dp로 줄어 있었습니다. InkWell을 바깥으로 옮기자 47.66×47.85dp가 됐습니다.

반대로 결함으로 올라온 3건(권한 안내 문구, 말풍선 글자, 네이티브 스플래시)은 표본을 뜨는 방식이 만든 착시라 고치지 않고 철회했습니다.

검사기를 만든 AI가 놓친 것과 운영자의 작업 규칙

검사기와 카탈로그는 바로 앞 커밋 67edc8d에서 AI 코딩 에이전트가 만든 하니스입니다. 그 커밋은 "렌더된 픽셀로 대비를 잰다"고 적었고, 실제로 픽셀을 쟀습니다. 빠진 것은 "얼마나 쟀는가"를 세는 일이었습니다.

AI가 낸 틀린 판정도 있었습니다. 카탈로그를 넓힌 에이전트는 권한 안내 다이얼로그 문구를 1.02:1, 사실상 안 보이는 결함으로 보고했습니다. 실제로는 케이스가 화면 전환 페이드의 첫 프레임(불투명도 0)을 캡처했고, 검사기는 투명한 다이얼로그 아래에 깔린 로비의 초록 상태 점을 배경으로 쟀던 겁니다. 600ms 정착 프레임을 흘리자 세 테마 모두 8.07~12.08:1로 통과했습니다.

세 커밋 모두 AI 공동 작성자 표기가 붙어 있고, 수정 보고서도 에이전트가 썼습니다. 사람 쪽에서 들어간 것은 규칙 하나입니다. 운영자는 "가드를 지워도 통과하는 테스트는 가짜 테스트로 본다"는 규칙으로 일하고, 하니스 보고서의 되돌림 실험은 그 규칙을 검사기 자체에 대 본 기록입니다. 글자가 하나도 없는 임시 화면으로 케이스를 되돌리고 새 검사를 끄면, 측정 0개·후보 0개인데도 4건이 통과했습니다. 검사를 켜자 3건이 "측정된 조각 0개 < 선언한 최소 1개"로 실패했습니다.

대비 검사 결과로 "통과"만 출력하지 마십시오. 잰 개수, 버린 개수, 버린 이유를 같이 출력하고, 잰 것이 선언보다 적으면 실패시키십시오. 가드 지워 보기의 일반적인 방법은 같은 앱의 기록 테스트 3,066개가 초록인데 DB를 못 여는 코드가 숨어 있었다: 가드 지워 보기에 따로 적었습니다.

비활성 버튼 면제는 숨을 곳이 되지 않게 넣었다

마지막까지 남은 실패 3건은 지문 확인 다이얼로그가 저장하는 동안의 비활성 버튼이었고, 1.83~1.91:1이었습니다. WCAG 1.4.3은 비활성 UI 컴포넌트를 대비 최소 기준의 예외로 둡니다. Material의 비활성 색도 원래 1.9:1 정도입니다. 33분 뒤 커밋 d01cc23은 면제를 넣으면서 이번 라운드가 고친 실패 방식이 다시 들어오지 못하게 막았습니다.

149케이스 중 면제가 걸린 곳은 정확히 1곳이었습니다. 이 커밋 뒤 flutter test는 2,475 통과, 4 skip, 실패 0이었습니다.

직접 재 보려면

렌더된 픽셀로 대비를 재는 검사기라면 프레임워크와 상관없이 이 순서로 점검합니다.

  1. 검사 함수에서 조각이 빠지는 길을 모두 찾습니다. return null, continue, 필터 조건이 그 자리입니다. 빠지는 자리마다 사유를 붙여 목록에 넣습니다. 8dacf19 시점에 사유는 10가지였습니다(화면 밖, 배리어 뒤, 가려짐, 클립, 그림문자, 안 그려짐 등).
  2. 케이스마다 한 줄 회계를 찍습니다. 후보 수가 잰 수와 버린 수의 합과 다르면 검사기 버그입니다.
  3. 측정 하한을 단언합니다. 케이스마다 최소 1개는 실제로 재야 통과하게 하고, 0을 허용하는 케이스에는 20자 이상의 사유를 요구합니다. 정확한 조각 수를 적지는 않습니다. 문구가 한 글자 바뀌면 빨개지고, 다음 사람이 숫자를 내려 맞추게 됩니다.
  4. 되돌림 실험을 합니다. 한 케이스를 글자 없는 화면으로 바꾼 뒤 검사를 끄고 한 번, 켜고 한 번 돌립니다. 끄면 통과하고 켜면 실패해야 새 검사가 일을 하는 겁니다. 정착 프레임도 꺼 보고 "안 그려짐"이 되살아나는지 봅니다.
  5. 완결성은 상태 목록으로 잽니다. 코드의 조건 분기에서 화면×상태를 뽑아 id를 붙이고, id마다 그것을 그리는 케이스나 제외 사유가 있는지 대조합니다.

이 앱에서는 --dart-define 스위치 세 개로 2번과 4번을 돌립니다.

flutter test test/theme_matrix/ --dart-define=Y2_TRACE=true
# #Y2# <case>|<theme>|measured=..|candidates=..|fail=..|inactivePass=..|discard=offScreen:..,behindBarrier:..

flutter test test/theme_matrix/ --dart-define=Y2_NO_INTEGRITY=true
# 측정 하한과 "안 그려짐 0" 단언을 끈다. 숨은 미측정 케이스가 없으면 통과 수가 같다

flutter test test/theme_matrix/ --dart-define=Y2_NO_SETTLE=true
# 케이스 공통 600ms 정착 프레임을 끈다

8dacf19의 하니스 보고서에서 정착 프레임과 새 검사를 모두 켜고 돌린 결과는 903 통과·3 실패였고, 검사만 끈 실행도 903·3이었습니다(3 실패는 앞의 비활성 버튼). 그 시점에 숨은 미측정 케이스는 없었습니다. 출시 전에 거치는 다른 검사는 AI가 만든 Android 앱, 출시 전에 무엇을 검사하나: 릴리스 게이트와 R8 크래시에 정리했습니다. 남은 일은 배리어 뒤 1,575개가 시트가 화면 절반만 덮어도 전부 버려진다는 점과, 모든 수치가 위젯 테스트 픽셀이라 기기 스크린샷과는 아직 대조하지 않았다는 점입니다.

자주 묻는 질문

자동 대비 검사가 통과했는데도 글자가 안 보일 수 있나요?

있습니다. 검사기가 재지 못한 조각을 조용히 버리면 결과에는 "실패 0건"만 남습니다. 이 앱에서는 텍스트 조각 9,318개 중 3,762개가 대비 수치 없이 빠져 있었고, 고치기 전에는 제외했다는 보고가 하나도 없었습니다.

WCAG 대비 기준은 비활성 버튼에도 적용되나요?

적용되지 않습니다. WCAG 1.4.3은 비활성 UI 컴포넌트의 글자에 대비 최소 기준을 두지 않습니다. 다만 비활성 여부는 콜백이 없는지로 판정해야 면제가 숨을 곳이 되지 않습니다.

다이얼로그 뒤 글자나 이모지를 대비 검사에서 빼도 되나요?

빼도 됩니다. 다만 제외한 개수와 사유를 결과에 같이 출력해야 합니다. 이 앱에서도 배리어 뒤 1,575개와 이모지 876개는 빼는 것이 맞았습니다.

이 글은 SugarMount가 직접 측정한 값으로 쓰고, 초안 작성에 AI 코딩 에이전트를 썼습니다.

검증과 출시 글 전체 보기 · AX 노트 전체 보기