Skip to content

모바일 탭 전환 즉시 반응 — 눌러도 0.7~1.0초 멈추던 탭바에서 "강조 먼저, 화면은 한 렌더 뒤"까지 (2026-09-02)

  • 기간: 2026-09-02 (세션 1개, 오너 보고 "모바일에서 탭이동을 하면 반응속도가 느린 경우가 있어 0.7~1.0초가 지나야 뜨는 경우가 있는데 (특히 메인화면 탭) … 무조건 누르자마자 바로 반응이 나오게")
  • 랜딩: PR #1156(Phase 0~2, c32aa9c2) — 마이그레이션·엣지 없음, Vercel 자동 배포
  • 설계서: 없음(이슈 #1152 코멘트의 분석·Phase 계획·"예상 효과·개선사항" 절이 정본)
  • 정본: src/react/mobileApp.tsx MobileTabsRegion(pressedTab / tab = React.useDeferredValue(pressedTab)) · 셸 골격 계약 docs/contracts/shell-frames.md §2(토글 구조 불변)
  • 도구: e2e/profile/tabSwitchPaint.profile.mjs(레포 안, 수동 리그 — 로컬 스택 + dev 서버 필요)
  • 게이트: tests/react/tabSwitchInstantResponse.test.mjs(매 CI)
  • 버그리포트: bug-report/bug-063-20260902.md
  • 계약: 변경 없음(골격 CSS 구조 변경은 실측 근거 없어 철회)

Phase 현황

Phase내용상태
Phase 0브라우저 쪽 시간 측정 리그(누름→탭바/화면 칠함 마커 + 트레이스 카테고리 ms, 방문 탭 누적·CSS 실험·큰 사진 조건)✅ #1156
Phase 1탭바 강조는 누른 즉시, 판·크롬·시작/복귀 바는 한 렌더 뒤(useDeferredValue)✅ #1156
Phase 2판 안 프로필 사진 <img decoding="async"> 8곳 (골격 CSS 구조 변경 2-1·2-2는 철회)✅ #1156
Phase 3게이트·기록·버그리포트✅ 이 문서
(D3)Production 계측 perf_tab_switch — 실기기 원인 확정용⬜ 오너 결정 대기

1. 배경

8월 24일 탭 렌더 개편(#653·#657·#658)으로 방문 탭을 keep-alive로 두고 재방문 전환의 React 비용을 36.9→3.3ms로 줄였다. 그 뒤 데이터 워밍(#1013)·부팅 캐시(#1016)로 탭 진입 데이터도 미리 받아둔다. 그런데 9월 2일 오너가 "탭을 눌러도 0.7~1.0초 뒤에야 뜬다, 특히 메인 탭"을 보고했다 — React도 데이터도 아닌 곳에 지연이 남아 있었다.

2. 문제 제기

탭바 강조와 화면 교체가 한 번의 그리기에 묶여 있었다

MobileTabsRegion이 탭 구독값 하나로 탭바 강조·판 표시 전환·하단 크롬·시작/복귀 바를 한 렌더에 바꿨다(mobileApp.tsx). 브라우저는 그 커밋 뒤 스타일 계산·레이아웃·페인트를 한 번에 하므로, 화면 교체가 무거우면 강조까지 같이 늦는다. 유저 입장에서는 "눌린 건지 모르는" 상태가 된다.

종전 측정 리그는 React 커밋만 쟀다

tabSwitch.profile.mjs는 React Profiler의 커밋 시간만 집계했다. 브라우저 쪽(스타일·레이아웃·페인트·이미지 디코딩)은 애초에 보이지 않았고, "3.3ms로 해결"이라는 판정도 그 범위 안의 것이었다.

실험실에서는 0.7~1.0초가 재현되지 않았다

새 리그(Chromium, CPU 4배 느리게, 개발 React, 세션 24개 시드)에서 모든 전환이 20~70ms였다. 방문 탭을 2→5개로 누적해도(DOM 272→1,039 노드), CSS 실험 2종(하단 크롬 변수 고정·숨은 판 content-visibility)을 넣어도, 2.9MB·12MP 프로필 사진을 심어도 변화가 없었다(재디코딩 0). 즉 남은 지연은 실기기에서만 나는 원인이다 — (가) 누르는 순간 메인 스레드가 도착 데이터(볼륨 716kB·카탈로그 913kB·PR 287kB) 처리로 바쁨 (나) iOS WebKit의 사진 재디코딩 등 고유 비용 (다) 실제 데이터 규모.

3. 해결 방안

원칙 (오너 결정, 2026-09-02)

  • D1 "전부 다 하고 랜딩" — Phase 0~2 완주 후 PR 1개·CI 1회.
  • D2 "건드려도 됨" — 셸 골격 계약(Codex 경유 조항)을 직접 개정해도 좋다. → 실측 결과 골격 변경이 무의미해 사용하지 않았다.
  • D3 Production 계측 추가 여부 — 대기.

접근

판단
강조/화면 두 렌더 분리(useDeferredValue)채택 — 화면 비용이 얼마든 강조가 먼저. 토글 구조 자체는 불변이라 셸 계약 개정 불요
숨은 판 content-visibility:hidden·크롬 변수 게시 위치 변경기각 — 실험 ±5ms, 회귀 위험만
판 사진 decoding="async"채택(예방) — WebKit 재디코딩 가설에 대한 무해한 대비
프로필 사진 업로드 축소·Storage 변환보류 — 변환은 Pro 플랜 전용, 업로드 UI는 데스크톱 전용, 별도 트랙
Production 계측오너 결정 D3

4. 적용한 내용

Phase 0 — 측정 리그 (#1156)

e2e/profile/tabSwitchPaint.profile.mjs: 페이지 안 마커(pointerdown → 탭바 data-lg-state 변경 → rAF+setTimeout(0) 페인트 근사 / 판 class 변경) + Playwright startTracing 트레이스 버킷(UpdateLayoutTree·Layout·PrePaint·Paint·FunctionCall·Decode Image…). 환경 LG_PROFILE_EXPERIMENTS·LG_PROFILE_BIG_PHOTO(캔버스로 12MP JPEG 생성·업로드).

Phase 1 — 렌더 분리 (#1156)

MobileTabsRegion: pressedTab = useMobileNavTab()BottomNav의 강조에만, tab = React.useDeferredValue(pressedTab)이 판(activeTabvisitedTabs·bottomChrome·startBar/resumeBar에. 기존 잠금 문자열(const startBar = … tab === "home", resume={resumeBar})은 tab 이름을 유지해 무변경. 운동 플로우의 하단 탭바 배선은 판 전환이 없으므로 분리하지 않았다.

Phase 2 — 판 사진 (#1156)

홈 히어로 2곳·피드 3곳·그룹 3곳의 프로필 사진 <img decoding="async">.

주요 결정과 그 근거

  • 골격 CSS 변경 철회: 분석 단계에서 지목한 원인 A~D(하단 크롬 상속 변수 변경·.screen:has() 재배치·display:none 재레이아웃·:has() 가드 21개)는 실험에서 이득 0. 근거 없는 구조 변경은 회귀 위험만 남긴다.
  • 실기기 원인은 계측 없이는 추측 — D3로 오너에게 올렸다.

작업 중 드러난 것

  • 측정 중 src 수정 금지 — dev 서버 HMR이 실험 컨텍스트마다 새로 페이지를 열어 후반 실험이 수정본을 잰다. 기준선은 패치를 빼고(git diff > patchgit checkout) 측정한 뒤 다시 적용했다.
  • 프로필 사진은 원본 그대로(최대 5MB, 축소 없음) 저장·서명 URL로 표시된다. 모바일에는 업로드 UI가 없고(데스크톱 onUploadPhoto), 로그인 제공자 사진은 avatar_data_url.
  • 리그의 프로필 갱신은 본인 클라이언트(authClient)로 — service_role은 profiles UPDATE 권한이 없다(42501, #1033 신원 전달 트랙의 결과).
  • 순수 노이즈 12MP JPEG는 12MB라 버킷 한도(5MB) 413 — 그라데이션+원+질감으로 2.9MB.

5. 적용 결과

항목결과
누름 → 탭바 강조 칠함 (CPU 4배 느리게, 3회 평균)31~66ms → 17~20ms (프로필→메인은 57→39)
누름 → 화면 칠함22~66ms → 35~80ms (두 번째 렌더로 넘어가 5~14ms 늦음, 한 프레임 안)
React 커밋4~6ms → 6~8ms (커밋 2회)
방문 탭 누적(2→5개) 영향없음 → 없음
골격 CSS 실험(변수 고정·content-visibility)±5ms — 미채택
12MP 사진 재디코딩(Chromium)0ms → 해당 없음(예방 조치만)
로컬 게이트npm run check 2,375건·unused 통과
미검증실기기 체감(§14 자동 증거로 닫음) · iOS WebKit 재디코딩 여부 · 오너 보고의 0.7~1.0초 실제 원인(D3 계측 대기)

6. 이번 개선으로 향상된 것

누르면 즉시 반응한다

탭바 강조가 화면 비용과 무관하게 첫 렌더에 칠해진다 — "눌린 건지 모르겠는" 구간이 구조적으로 사라진다.

구조적으로 남는 것

  • "반응(탭바)과 내용(판)은 다른 렌더"가 계약 테스트로 잠겼다.
  • 탭 전환 성능 판정 도구가 브라우저 시간까지 포함한다 — 다음 렌더 트랙은 React 커밋만 보고 닫지 않는다.

남은 것

  • D3: Production 계측(perf_tab_switch) — 오너 go(2026-09-03) → 이슈 #1160·PR #1161로 랜딩(2026-09-03-tab-switch-latency-probe-1160.md). 실기기 원인 (가)~(다)의 확정과 후속 수리(작업 분할·워커·사진 축소)는 수집 결과로.
  • 프로필 사진 원본 저장 구조(업로드 축소·변환) — 범위 밖.