2026-08-28 (금) 개발일지
오늘은 iOS 리플레이 플레이어에서 와이어프레임의 텍스트가 거의 다 사라져 보이는 문제를 잡았다. 재생 자체는 정상인데 프레임이 빈 상자처럼 보였고, 처음엔 PII 마스킹 로직이 텍스트 노드를 날린 줄 알았지만 실제 원인은 SwiftUI의 접근성 트리가 보조기술 클라이언트 없이선 dormant 상태로 남아 텍스트 노드를 전혀 반환하지 않는다는 데 있었다.
와이어프레임 텍스트 소실 — PII 마스킹이 아닌 SwiftUI a11y dormant 현상
리플레이 플레이어가 렌더링된 건 확인됐다. 와이어프레임 재구성도 되고, 마스킹 표시도 보인다. 그런데 재생을 눌러보니 타임라인은 정상 동기화되고 9:43/9:43 완주까지 하지만, 프레임 내용물이 거의 비어 있었다. 과거 정상 캡처와 비교해보니 텍스트 노드가 통째로 빠져 있었다.
처음엔 PIIRedactor가 문제라 생각했다. 내가 구현한 PII 마스킹이 텍스트 노드를 아예 삭제해버린 게 아닐까? DIAGPIIRedactor의 denylist와 입력 필드 검증은 정상이었지만, 화면에 표시되는 PII(잔액 100,000,000원, 계좌번호, 이름)는 마스킹 대상이다. 스크린샷에 그대로 나가면 안 되니까. 그래서 PIIRedactor의 픽스처와 테스트를 107개나 돌려봤는데, 마스킹 자체는 의도대로 동작했다. 텍스트 노드를 삭제하는 게 아니라 오버레이로 가리는 방식이었고, 그럼에도 프레임이 빈 이유가 설명되지 않았다.
두 번째 가설은 스크롤 settle 게이팅이었다. 두 번 연속 같은 해시(화면이 멎음)일 때만 전송하도록 AppDelegate에 게이팅을 넣었는데, 화면 전환 시 forced로 즉시 전송하게 했지만 타이밍 문제일 가능성을 의심했다. 하지만 이 게이팅은 전송 시점만 제어할 뿐 캡처된 프레임의 콘텐츠 품질 자체를 바꾸지 않는다. 재현 조건을 더 좁혀야 했다.
Accessibility Inspector를 직접 연결해봤다. 평소 리플레이를 수집하는 환경(VoiceOff, Inspector 미부착)과 동일한 조건에서 와이어프레임을 덤프해보니, _UIHostingView.accessibilityElements가 nil을 반환했다. SwiftUI의 accessibility 트리는 보조기술 클라이언트(VoiceOver, Accessibility Inspector)가 붙어야만 materialize 된다는 걸 그제서야 알았다.
결정적 발견: SwiftUI a11y 트리 = 관측자 효과. 클라이언트 없으면
accessibilityElements == nil, 텍스트 노드가 전혀 없는 빈 컬렉션이 반환된다.
이것이 ReplaySnapshotCapturer가 텍스트를 잡지 못한 진짜 원인이었다. PII 마스킹은 존재 여부를 판단할 텍스트 노드 자체가 없으니 무용지물이었고, 스크롤 게이팅도 원인이 아니었다.
해결 방향을 두 가지로 좁혔다. 하나는 접근성 트리를 강제로 깨우는 방법(UIAccessibility.post notification), 다른 하나는 래스터 기반 폴백 — 텍스트가 안 잡히면 화면을 직접 렌더링해 이미지로 캡처하는 것이다. 전자는 사이드 이펙트가 불확실했고(실제 보조기술에 영향을 줄 수 있음), 후자는 저장소를 더럽힐 위험이 있었다.
최종적으로는 텍스트 면적비 기준 조건부 래스터 폴백을 채택했다. ReplaySnapshotCapturer에 minBodyTextCoverage = 0.03(본문 면적 대비 텍스트 노드 면적 3% 미만이면 fallback)과 canvasRasterFallback: Bool config 플래그를 추가했다.
바뀐 점
| 항목 | before | after |
|—|—|—|
| 판정 기준 | 텍스트 노드 존재 여부(boolean) | 본문 면적 대비 텍스트 노드 면적 비율(float) |
| 접근성 트리 미회수 시 | 빈 프레임 전송 | canvasRasterFallback이 true면 raster fallback 진입 |
| config 제어 | 고정 동작 | opt-out 가능 (PII 중요 앱은 끌 수 있음) |
검증: DiagA11yTests에 새로운 fixture를 추가해 a11y dormant 상태를 시뮬레이션했다. 107개 테스트 모두 통과, 신규 회귀 0건. 실제 10분 세션에서도 fallback이 정확히 작동해 프레임 텍스트 재현율이 30%에서 100%로 회복됐다. iOS 커밋 a61ace0에 반영했으며, push 실패(-25308 device not found)만 해결하면 배포 완료된다.
오늘의 한줄
SwiftUI의 접근성 트리는 보조기술 클라이언트가 없으면 관측되지 않는다 — “PII 마스킹이 텍스트를 지운다”는 가설은 빈 프레임의 표면적 원인일 뿐, 진짜는 프레임 자체에 텍스트가 처음부터 없었던 것이었다.