Skip to content

스플래시 소유권 — iOS 앱 부팅 화면 두 번 깜빡임에서 "환경당 한 장" 계약까지 (2026-08-23)

  • 기간: 2026-08-23 ~ 08-24 (세션 1개 — 오너 보고 "앱 켜면 부팅 화면이 두 번씩 깜빡이면서 여러 번 나오는데 왜 그런지 봐줄래?" → 계측 → 원인 층 분리 → 오너 결정 → 구현 → 오너 새 iOS 빌드 실기기 확인)
  • 랜딩: PR #613(수리, f333b128) · #620(BUG-008 리포트, ff352390) · #626(BUG-008 해결 확정, 83257c17). 마이그레이션·엣지 0건, 웹은 Vercel 자동 배포(app-CfEW7eKr.js), iOS는 오너 빌드 1회
  • 설계서: 없음(대화에서 결정). 별건인 루트 교체 재부팅 현황 리포트는 artifact 9096ccd5
  • 정본: 계약 docs/contracts/native-bridge.md §Splash ownership, 웹 index.html(플래그 → html.gym-app-native-splash) · src/react/vite/nativeSplash.ts(ready/stalled) · src/react/vite/main.tsx, iOS ios/App/App/MainViewController.swift(boot splash cover)
  • 도구: tmp/boot-probe/*.mjs(gitignored) — Playwright(iPhone 14 프로필)로 문서 생성 시점부터 스플래시 층의 등장/소멸·entrance 애니메이션 시작·브리지 메시지 타임라인 기록. Production 번들(로그아웃·느린 망) + 로컬 빌드·로컬 Supabase 일회용 계정(로그인 경로)
  • 게이트: tests/react/nativeSplashOwnership.test.mjs(신규 3건), backgroundTokens·controllerBoundaries 앵커 교체
  • 버그리포트: bug-report/bug-008-20260823.md
  • 계약: native-bridge.md capability splashOwnerV1 + 메시지 splashState 신설

1. 배경

iOS 앱은 Capacitor 원격 셸이다 — 네이티브가 WKWebView로 www.barbelic.com을 열고, 화면은 전부 웹이 그린다. 부팅 스플래시는 웹 쪽에서 08-19(#442)에 한 번 손봤다: index.html 정적 스플래시의 entrance를 React 호스트 스플래시가 재생하지 않고 이어받게 했다. 그 뒤로 웹만 보면 부팅은 깔끔했는데, 오너는 아이폰 앱에서 워드마크가 두 번 뜨고 앱을 열 때마다 반복된다고 보고했다.

2. 문제 제기

웹 안의 부팅은 한 번이었다

Production 번들(로그아웃·1.5Mbps/150ms/CPU 4x)과 로컬 빌드+로컬 스택(로그인 콜드·스냅샷 재부팅·visibility 복귀) 다섯 경로 모두 정적 스플래시 → React 스플래시(이어받기) → 앱 1회. 느린 망의 워드마크 페이드인도 1회(561→1091ms). 앱 복귀로 supabase가 SIGNED_IN을 2회 재방출해도 재표시 없음. 깜빡임은 페이지 바깥이었다.

스플래시를 그리는 주체가 셋이었다

iOS 콜드 스타트: LaunchScreen.storyboard(검정 + "Barbelic" + 태그라인, 앱 첫 프레임까지) → 검정 빈 WKWebView(원격 HTML 대기) → 웹 정적 스플래시가 opacity: 0에서 다시 페이드인. 세 장은 위치(centerY −25pt vs padding-bottom: 4vh)·글꼴·그라디언트가 달랐고, 넘겨주는 계약이 없었다. SplashScreen 플러그인도 없다(packageClassList: []).

로그인 전환 커버가 첫 페인트에 걷혔다

reloadHomeAfterNativeAuthCompletion이 로그인 화면 스냅샷을 커버로 띄우고 웹뷰를 about:blank → 앱 진입점 전체 재로드한 뒤, webView.isLoading == false + 0.2초에 커버를 제거했다. HTML 로드 끝 ≠ 앱 그려짐이라 밑에서 돌고 있는 웹 부팅 스플래시가 드러났다.

웹 안에서도 같은 장면이 두 층이었다

호스트 오버레이(HostLoadingScreen)가 덮고 있는 동안 GymAppController(app.tsx)가 .gym-app-device 안에 AuthLoadingGate를 또 그렸다(계측 lgSplash: 2). 안 보일 뿐 두 층.

3. 해결 방안

원칙 (오너 결정, 2026-08-23)

  • "화면 비율이 달라지면 또 문제 생기는 거 아닐까? 가장 근본적인 해결책은?" → 픽셀 맞추기(네이티브·웹 좌표·글꼴 일치) 기각.
  • "어디서 여느냐에 따라서 각자 한 장씩 그리면 되지 않을까? iOS나 안드로이드 앱이면 네이티브, 웹이면 웹" → 채택.
  • 3번(데스크톱 900px 경계 루트 교체 재부팅)은 "창 줄이면 모바일로 바뀌던 기능이 없어지는" 부팅 시 고정안을 기각하고 현황 리포트로 분리.

접근

내용판단
(a) 픽셀 맞추기웹 스플래시를 네이티브에 맞춤기각 — 안전영역·Dynamic Type·비율·폴드마다 재발
(b) 웹 단독LaunchScreen 순흑, 브랜드는 웹 한 번만보류 — 한 장이지만 검은 공백이 남음
(c) 네이티브 단독(셸이 소유권 선언)splashOwnerV1 → 웹 스플래시 0장 + ready/stalled, 셸은 런치 화면 동일 뷰 커버를 앱이 그려질 때까지채택 — 맞춰야 할 두 그림이 사라짐. 플래그 없으면 웹이 그려 구·신 조합 어느 쪽도 빈 화면 없음
(d) 웹 번들 동봉검은 공백 원천 제거범위 밖 — 원격 셸 배포 모델 자체를 바꾸는 제품 결정

4. 적용한 내용

Phase 1 — 계약 + 웹 + iOS (#613)

  • index.html: 문서 시작 시 window.__barbelicNativeCapabilities.splashOwnerV1 === truehtml.gym-app-native-splash, 정적 스플래시 display: none. app-host.css: 네이티브 모드에서 호스트 스플래시 숨김(검은 호스트 층·15초 watchdog 복구 카드는 유지).
  • src/react/vite/nativeSplash.ts + main.tsx: 호스트 로딩 종료 후 프레임 두 번 뒤 { v: 1, type: "splashState", state: "ready" }, watchdog 시점에 "stalled". 플래그 없으면 no-op. barbelicNav.ts 메시지 타입, global.d.ts 캐퍼빌리티.
  • app.tsx: remoteStatus === "loading"·!accountUiReady에서 데스크톱과 동일하게 null — 스플래시는 호스트 한 곳만.
  • MainViewController.swift: splashOwnerV1 주입 · viewDidLoad에서 LaunchScreen.storyboard 인스턴스를 커버로 · webView.isLoading 관찰로 앱 진입점(루트 경로)·about:blank 로드마다 재표시(재인증 리셋·웹 reload·웹프로세스 종료 복구) · ready/stalled/20초 타임아웃에 0.2초 페이드아웃 · 스냅샷 커버 3함수(showNativeAuthTransitionCover 계열) 삭제.
  • 테스트: nativeSplashOwnership.test.mjs 3건(플래그 판정·송신 조건·프레임 두 번 뒤 ready), 앵커 2건 교체(pending-changes 신고).
  • 문서: native-bridge.md §Splash ownership.

Phase 1-5 — BUG-008 (#620 → #626)

리포트 작성(상태 "iOS 빌드 오너 확인 대기") → 오너가 새 빌드로 확인한 뒤 같은 파일을 해결로 갱신.

주요 결정과 그 근거

  • 커버를 내리는 조건은 "첫 페인트"가 아니라 "앱 그려짐"(ready) — 첫 페인트에 내리면 다시 네이티브→웹 그림 맞추기가 필요해진다.
  • 웹은 네이티브 밑에서 스플래시를 "대기"시키지 않고 아예 안 그린다(오너: "각자 한 장씩"). 타임아웃·stalled 경로에서 보이는 것은 복구 카드(오류 UI)이지 스플래시가 아니다.
  • 재로드 감지는 웹 메시지가 아니라 네이티브 isLoading 관찰 — 웹 자체 location.reload()(스테일 청크 복구)·웹프로세스 종료 복구처럼 웹이 미리 알릴 수 없는 경로까지 한 규칙으로 덮는다. 같은 원점의 다른 경로(법률 문서)는 제외한다.

작업 중 드러난 것

  • Production 번들을 로컬 Supabase에 붙이는 시도는 Chrome Private Network Access에 막힌다 — 로그인 경로 계측은 로컬 빌드(vite preview)로.
  • Browser 패널이 숨겨져 있으면 rAF가 안 돌아 타임라인 계측이 불가하다 — Playwright 헤드리스로.
  • 본체 체크아웃을 다른 세션(입자성 캠페인)이 쓰고 있어 origin/main 워크트리에서 작업. bash의 cmd //c mklink는 경로 구문 오류 — PowerShell New-Item -ItemType Junction으로.
  • CI가 GitHub 결제 차단("recent account payments have failed")으로 3초 만에 실패 — 코드 문제가 아니라 오너 결제 정리 뒤 재실행.
  • 머지 뒤 오너가 옛 iOS 빌드로 열어 "여전히 한 번 깜빡인다"고 보고 — 계약의 폴백(플래그 없음 → 웹이 그림) 그대로이며, 새 빌드 설치로 해소. 네이티브 수리는 앱 빌드가 실려야 효과가 난다는 점을 리포트에 명시했다.

5. 적용 결과

항목결과
iOS 콜드 스타트워드마크(네이티브) → 검정 → 워드마크 페이드인(웹) → 홈 ⇒ 워드마크 한 장 정지 → 홈. 오너 새 빌드 실기기 확인(08-24)
웹 안 React 스플래시 층lgSplash 2 → 1 (브라우저 모드, 종전 순서 그대로)
네이티브 플래그 모드(로컬 계측)정적·호스트 스플래시 0장, 홈 115ms → booting 해제 240ms → splashState: ready 257ms(페인트 뒤)
stalled 경로(요청 영구 대기)15,065ms에 복구 카드 + splashState: stalled
테스트npm test 1810 통과, 정적 게이트 통과
Productionindex.html에 플래그 감지 스크립트, app-CfEW7eKr.jssplashState
미확인소셜 로그인 후 전환(같은 커버 → 홈), 비행기 모드 20초 뒤 복구 카드 — 오너 실기기 항목으로 남김

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

맞춰야 할 그림이 없다

네이티브 커버와 런치 화면이 같은 storyboard라 Auto Layout이 기기·비율을 처리하고, 웹은 네이티브 안에서 아무것도 그리지 않는다. 새 기기·폰트·안전영역 변화에 스플래시 쪽 할 일이 없다.

셸 하나 추가 = 플래그 하나

Android 앱 트랙은 splashOwnerV1 주입 + splashState 수신 + 커버만 구현하면 같은 웹 코드로 끝난다. 계약은 native-bridge.md에 있고, 플래그가 없는 셸은 웹이 그리므로 구·신 조합이 깨지지 않는다.

부팅 깜빡임은 계측으로 가른다

"페이지 안인가 밖인가"를 tmp/boot-probe 방식(스플래시 층·entrance·브리지 타임라인)으로 먼저 가른다. 이번 건은 웹을 고쳐도 해결되지 않을 문제였다.

남은 것

  • 오너 실기기 확인 2항목(소셜 로그인 후 전환, 비행기 모드 20초 복구 카드).
  • Android Chrome PWA는 OS 생성 스플래시 + 웹 두 장 구조가 남음 — Android 앱 트랙이 대체.
  • 데스크톱 900px 경계 루트 교체 재부팅(컨트롤러를 루트 위로, isDesktop 10파일 53곳 정책값화) — 현황 리포트 artifact 9096ccd5, 오너 D1~D3 대기.
  • 후속 수리(2026-08-24): iOS 커버 재표시 트리거 webView.isLoading 관찰이 스와이프 백(UI 프로세스 주도 히스토리 이동)에도 발화해 종목 상세 → 뒤로가기에서 커버가 뜨고 20초 멈춤 → didStartProvisionalNavigation(새 문서 로드 시작)으로 교체 (#629, bug-report/bug-009-20260824.md).