Skip to content

모바일 탭 전환 지연 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층 일치) · pgTAP client_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건 상한). appErrorReporterAppErrorKind/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_error kind 선례(20260821660000)가 그대로 본이 됐다 — 제약 재생성 + 함수 전체 재선언 + 클라 두 곳.
  • 리포터 contract_path 검증은 ^[A-Za-z_][A-Za-z0-9_.\[\]-]{0,255}$ — 요약 문자열의 부호·구분자를 여기에 맞췄다(음수 m, 구분 ./_/-).
  • service_roleprofiles UPDATE 권한이 없다(#1033) — 리그·테스트의 프로필 갱신은 본인 클라이언트로.

5. 적용 결과

항목결과
단위 테스트6건 통과(요약 조립·상한·응답 이름·kind 3층 일치)
pgTAPperf_tab_switch 적재 1건(CI migration-smoke)
로컬 게이트npm run check·unused 통과
Production 데이터미수집 — 릴리스 뒤 오너 평소 사용으로 쌓인다

조회 SQL(Management API read_only 또는 관리자 도구):

sql
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 전용 보기는 없음(필요 시 별도).