모바일 탭 전환 지연 Production 계측 — 실험실로 못 잡은 실기기 원인을 오너 폰에서 표로 받기까지 (2026-09-03)
- 기간: 2026-09-03 (세션 1개, 오너 결정 D3 "오케이 넣어줘" — 이슈 #1152 Phase 0 결과에 대한 답)
- 랜딩: PR #1161(Phase 1~3,
a4795ad4) — 마이그레이션20260904000100_perf_tab_switch_kind.sql, Vercel 자동 배포. Production 적용 = 릴리스 레인 - 설계서: 없음(이슈 #1160 본문의 Phase 계획·"예상 효과·개선사항" 절이 정본)
- 정본:
src/react/services/tabSwitchLatencyProbe.ts(계측·요약 문자열 범례는 파일 머리 주석) ·client_error_events.kind = 'perf_tab_switch' - 도구: 조회 SQL(아래 5절)
- 게이트:
tests/react/tabSwitchLatencyProbe.test.mjs(요약·상한·kind 3층 일치) · pgTAPclient_error_events.test.sql(+1) - 버그리포트: 없음(계측 트랙 — 결함은
bug-063-20260902.md가 정본) - 계약:
client_error_events계약 v1 유지(새 필드 없음, kind 1개 추가)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 서버: kind check 제약 + 인입 RPC kind 목록에 perf_tab_switch(함수 전체 재선언, 본문은 최신과 동일) + pgTAP | ✅ #1161 |
| Phase 2 | 클라이언트 계측 tabSwitchLatencyProbe — 300ms 초과 전환만, 5초 1건·세션 20건 상한, 리포터 kind 등록, 부팅 시 설치 | ✅ #1161 |
| Phase 3 | 게이트·기록 | ✅ 이 문서 |
1. 배경
이슈 #1152에서 오너 보고 "탭을 누르면 0.7~1.0초 뒤에야 뜬다(특히 메인 탭)"를 실험실(Chromium·CPU 4배 느리게)로 재현하지 못했다(전환 20~70ms). 탭바 강조를 즉시 그리게는 고쳤지만, 화면이 늦게 뜨는 실제 원인은 실기기에서만 난다 — (가) 누르는 순간 메인 스레드가 도착 데이터 처리로 바쁨 (나) iOS WebKit 고유 비용(사진 재디코딩 등) (다) 실제 데이터 규모. 실기기 확인을 오너에게 부탁하지 않는 원칙(§14) 아래, 앱이 스스로 재서 보내게 했다.
2. 문제 제기
실기기에서 무슨 일이 있는지 볼 창이 없었다
클라이언트 오류 수집(client_error_events)은 오류만 받는다. 성능 신호(누름이 큐에서 기다린 시간, 메인 스레드가 막힌 구간, 직전에 도착한 응답)는 어디에도 남지 않아, 다음 수리도 추측이 될 판이었다.
필드 규칙이 엄격한 수집망에 새 정보를 실어야 했다
인입 RPC는 알 수 없는 키를 거부하고 계약 v1 키 목록이 고정이다. 새 컬럼·계약 v2는 RPC·pgTAP·리포터·관리자 화면까지 번지는 큰 변경이라, 기존 필드에 압축 요약을 싣는 쪽을 택했다.
3. 해결 방안
원칙 (오너 결정 D3, 2026-09-03)
"오케이 넣어줘" — Production 계측 채택.
접근
| 안 | 판단 |
|---|---|
새 컬럼 detail jsonb + 계약 v2 | 기각 — RPC 키 검증·pgTAP·리포터·관리자 화면 동반, 비용 대비 과함 |
| kind 1개 추가 + 기존 필드에 압축 요약 | 채택 — 마이그레이션 1개(제약·함수 재선언), 클라 ~200줄, 계약 v1 유지 |
| Long Tasks API만으로 막힌 구간 측정 | 기각 — iOS WebKit 미지원. 누름 큐 대기(event.timeStamp 차)와 100ms 하트비트 지연으로 대신하고, 지원 브라우저에서는 Long Tasks도 함께 |
4. 적용한 내용
Phase 1 — 서버 (#1161, 20260904000100)
client_error_events_kind_check 재생성(+perf_tab_switch) · record_client_error_event 전체 재선언(최신 본문 20260821660000 기준, kind 목록 한 곳만 추가) · postcheck(제약·함수 본문에 값 존재) · pgTAP 1건(적재 성공).
Phase 2 — 클라이언트 (#1161)
tabSwitchLatencyProbe.ts: 문서 레벨 pointerdown(캡처)으로 하단 탭 누름을 잡고, 탭바 강조·판 표시 전환 DOM 변경 뒤 첫 프레임(rAF→setTimeout 0)을 페인트 근사로 쓴다. 누름→판 칠함이 300ms를 넘으면 1건 보고(5초 1건·세션 20건 상한). appErrorReporter의 AppErrorKind/EVENT_KINDS에 kind 등록, vite/main.tsx 부팅 시 설치(탭바 없는 데스크톱은 무동작).
필드 범례 (kind = 'perf_tab_switch', surface = 'mobile_tabbar', diagnostic_key = 'tab_switch_slow')
duration_ms= 누름 → 판(화면) 칠함 ms ·component_frame_count= 막힌 구간 수 ·operation_name=tab:<from>/<to>contract_path=ed<큐 대기ms>.hl<탭바 칠함ms>.b<시작>-<길이>_…(길이순 3).rx_<응답이름>-<도착>-<kB>_…(최신순 3).v<방문 탭 수>.n<판 DOM 노드 수>— 시작·도착은 누름 기준 오프셋 ms, 음수는m접두(m120 = 120ms 전). 항목 없음 =b0/rx0, 탭바 칠함 미관측 =hlx.- 읽는 법:
ed가 크면 누름 자체가 큐에서 기다렸다(메인 스레드 바쁨 = 원인 가).b가 누름 직전·직후에 길게 있고rx에 큰 응답이 있으면 "도착 데이터 처리".ed·b가 작은데duration_ms가 크면 그리기 자체(원인 나·다).
작업 중 드러난 것
client_contract_errorkind 선례(20260821660000)가 그대로 본이 됐다 — 제약 재생성 + 함수 전체 재선언 + 클라 두 곳.- 리포터
contract_path검증은^[A-Za-z_][A-Za-z0-9_.\[\]-]{0,255}$— 요약 문자열의 부호·구분자를 여기에 맞췄다(음수m, 구분./_/-). service_role은profilesUPDATE 권한이 없다(#1033) — 리그·테스트의 프로필 갱신은 본인 클라이언트로.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 단위 테스트 | 6건 통과(요약 조립·상한·응답 이름·kind 3층 일치) |
| pgTAP | perf_tab_switch 적재 1건(CI migration-smoke) |
| 로컬 게이트 | npm run check·unused 통과 |
| Production 데이터 | 미수집 — 릴리스 뒤 오너 평소 사용으로 쌓인다 |
조회 SQL(Management API read_only 또는 관리자 도구):
select occurred_at, operation_name, duration_ms, component_frame_count, contract_path, release
from public.client_error_events
where kind = 'perf_tab_switch'
order by occurred_at desc
limit 50;6. 이번 개선으로 향상된 것
실기기 지연을 추측이 아니라 표로 본다
느린 전환 한 건마다 큐 대기·막힌 구간·직전 응답이 남는다. 하루치면 (가)/(나)/(다) 판별과 다음 수리 대상이 정해진다.
구조적으로 남는 것
- 성능 계측도 오류 수집망을 탄다(kind만 추가) — 다음 성능 계측(저장 지연 등)도 같은 길.
- 요약 문자열 범례가 코드 머리 주석과 이 문서에 있다.
남은 것
- Production 데이터 수집 후 원인 확정 → 후속 수리 이슈(작업 분할·워커·사진 축소 중 데이터가 가리키는 것).
- 관리자 화면에
perf_tab_switch전용 보기는 없음(필요 시 별도).