상황

하이브리드 앱의 웹뷰 이벤트 페이지에서, 사용자가 룰렛을 돌려 쿠폰을 받는 플로우의 마지막 단계 — 커스텀 스킴 발화와 iframe 내비게이션이 거의 동시에 일어나는 구간 — 에서 결과가 갈리는 레이스가 의심됐다. 스킴이 네이티브에 먼저 도달하느냐, 웹뷰 내비게이션이 먼저 커밋되느냐에 따라 쿠폰함 이동이 되기도 하고 안 되기도 했다.

이런 문제는 두 가지를 동시에 요구한다.

  1. 실제 사용자와 동일한 플로우로 레이스를 재현할 것
  2. 스킴 발화와 내비게이션 커밋의 선후 관계를 객관적으로 판정할 것

둘 다 웹 콘솔만으로는 안 된다. 스킴이 네이티브에 도달했는지는 네이티브 쪽에서만 보이기 때문이다.

문제

처음에는 ios_webkit_debug_proxy + CDP(Chrome DevTools Protocol)로 웹뷰에 붙어, 플로우 마지막 2줄(스킴 발화 + iframe 내비게이션)만 함수 오버라이드로 실행하는 재현 하네스를 만들었다. 빠르고 반복 가능해 보였다.

그런데 하네스는 4회 시도 모두 실패 케이스를 재현한 반면, 실기에서 버튼을 눌러 전체 플로우를 타면 첫 진입에 성공했다. 하네스와 실기의 결과가 정반대였다.

원인은 하네스의 충실도였다. 실제 플로우에서는 룰렛 애니메이션, confirm 다이얼로그, 서버 응답 대기 등이 두 줄 사이의 타이밍을 벌려놓는데, 마지막 2줄만 잘라 실행하면 그 타이밍 구조가 통째로 사라진다. 레이스 컨디션 디버깅에서 타이밍을 바꾼 재현은 다른 문제를 재현하는 것과 같다.

여기서 원칙 하나가 나온다. 하네스와 실기의 결과가 다르면, 결론이 아니라 하네스를 의심하라. 하네스는 도구일 뿐이고, 도구의 출력이 실측과 어긋나면 먼저 폐기 대상은 도구다.

  1. CDP 오버라이드 하네스
  2. 하네스 0/4 vs 실기 성공
  3. 하네스 폐기
  4. AX 탭 실사용 플로우
  5. log stream ms 판정

접근과 판단

하네스를 버리고 방향을 바꿨다. "플로우를 흉내 내지 말고, 실제 플로우를 기계가 수행하게 하자."

실사용 플로우 자동화: AX 트리 좌표 → 탭

시뮬레이터의 접근성(AX) 트리를 JSON으로 덤프하는 CLI를 사용했다 (사내에서는 orca라는 시뮬레이터 자동화 래퍼를 쓴다. xcrun simctl + AX 조회를 감싼 도구다).

# 화면의 AX 요소와 좌표(0..1 정규화)를 JSON으로 덤프
orca emulator ax --json

# 좌표로 탭 — CDP 없이 실제 터치 이벤트
orca emulator tap 0.5 0.72

핵심은 두 가지다.

  • CDP를 전혀 쓰지 않는다. JS 컨텍스트에 개입하지 않으므로 타이밍 구조가 실사용과 동일하다.
  • 네이티브 다이얼로그도 탭할 수 있다. JS confirm/alert은 iOS에서 네이티브 알럿으로 뜨는데, CDP로는 건드릴 수 없지만 AX 좌표 탭으로는 확인 버튼을 누를 수 있다. 룰렛 → 쿠폰 교환 → confirm까지 전체 플로우가 무인으로 돌아간다.

네이티브 판정: simctl log stream

스킴이 네이티브에 도달했는지, 내비게이션이 언제 커밋됐는지는 네이티브 로그로 판정했다.

xcrun simctl spawn <UDID> log stream --style compact \
  --predicate 'process == "MyApp"'

WKWebView의 navigation delegate가 남기는 로그 라인 — decidePolicyFor의 URL, 커스텀 스킴 처리(openWebView), didCommit — 이 ms 타임스탬프와 함께 흐른다. 웹 콘솔에서는 보이지 않던 "스킴이 실제로 발화됐는가"와 "내비게이션 커밋이 그보다 먼저였는가 나중이었는가"를 로그 타임스탬프만으로 확정할 수 있다.

구현

기계 판정 스크립트 패턴

사람이 로그를 눈으로 보는 대신, 매 시도를 기계적으로 판정하는 패턴으로 묶었다.

#!/bin/bash
# fixlog2.sh — 1회 플로우 실행 + 성공/실패 기계 판정

LOG=/tmp/schemetest/run-$(date +%s).log

# 1) 네이티브 로그를 백그라운드로 파일에 수집
xcrun simctl spawn "$UDID" log stream --style compact \
  --predicate 'process == "MyApp"' > "$LOG" &
LOGPID=$!

# 2) 실사용 플로우 구동 (AX 좌표 탭 시퀀스)
orca emulator tap 0.5 0.62   # 룰렛 시작
sleep 6                       # 애니메이션 + 결과 대기
orca emulator tap 0.5 0.80   # 쿠폰 교환
sleep 2
orca emulator tap 0.68 0.55  # confirm 다이얼로그 확인
sleep 4

# 3) 마커 카운트로 판정
kill $LOGPID
echo "iframe_nav : $(grep -c 'iframe_nav'  "$LOG")"
echo "openWebView: $(grep -c 'openWebView' "$LOG")"
echo "nav_coupon : $(grep -c 'nav_coupon'  "$LOG")"

grep -c로 마커(스킴 처리 라인, 내비게이션 라인)의 등장 횟수를 세면, 각 시도의 성공/실패가 사람 판단 없이 결정된다. 반복 실행하면 레이스의 발생률과 선후 패턴이 통계로 쌓인다.

🎯

구동

AX 좌표 → tap, CDP 무개입

📡

관측

log stream → 파일, ms 타임스탬프

⚖️

판정

grep -c 마커 카운트

곁가지: 시뮬레이터 한국어 IME

자동화 중 텍스트 입력이 필요한 단계에서, 시뮬레이터의 한국어 IME가 ASCII 입력을 한글로 변환해 입력을 오염시켰다. 시뮬레이터의 .GlobalPreferences에서 AppleKeyboards를 en_US@sw=QWERTY;hw=US로 교체하고 앱을 재시작하면 해결된다. 자동화 파이프라인에서 IME는 입력 충실도를 깨는 흔한 변수다.

결과

  • CDP 하네스: 4회 시도 전부 실패 재현 → 실기 플로우: 첫 진입 성공. 이 불일치 자체가 "하네스가 타이밍 구조를 파괴했다"는 증거가 됐다.
  • AX 탭 + log stream 조합으로, 스킴 발화와 내비게이션 커밋의 선후 관계를 네이티브 로그의 ms 타임스탬프로 확정 — 웹 콘솔 추측이 아닌 로그 기반 판정으로 전환.
  • 판정 스크립트로 반복 실행이 가능해져, 레이스 재현이 "운"에서 "측정"이 됐다.

CDP 하네스

플로우 마지막 2줄만 함수 오버라이드로 실행. 빠르지만 타이밍 구조가 사라짐 → 0/4 실패 (실기와 정반대).

AX 탭 실사용 플로우

실제 터치 이벤트로 전체 플로우 구동. JS 컨텍스트 무개입. 네이티브 confirm 다이얼로그까지 탭 가능. 실기와 동일 결과.

배운 것

웹뷰-네이티브 하이브리드 디버깅 전반에 재사용할 수 있는 패턴 세 가지.

  1. 하네스-실기 불일치는 하네스를 의심하라. 재현 하네스가 실측과 다른 결과를 내면, 그 하네스로 얻은 결론은 전부 보류한다. 특히 레이스 컨디션은 재현 방식이 타이밍을 바꾸는 순간 다른 문제가 된다. 플로우의 일부만 잘라 실행하는 하네스는 충실도 검증(실기와 같은 결과를 내는가) 없이는 신뢰할 수 없다.
  2. AX 좌표 → 탭이면 CDP 없이 전체 플로우를 구동할 수 있다. JS 컨텍스트에 개입하지 않으므로 충실도가 유지되고, CDP가 닿지 못하는 네이티브 다이얼로그(confirm/alert)까지 다룰 수 있다.
  3. log stream을 파일로 받아 마커를 카운트하면 판정이 기계화된다. simctl log stream --predicate + grep -c 조합은 프레임워크 없이도 성공/실패를 수치로 만든다. 웹 레이어에서 보이지 않는 네이티브 수신 여부는, 네이티브 로그의 타임스탬프가 유일하게 신뢰할 수 있는 판정 근거다.