Live Activity (위젯) 트러블슈팅
러닝 앱에 Live Activity(잠금 화면 위젯)를 붙였더니 자잘한 버그가 한꺼번에 쏟아져 나왔다. 위젯 타이머가 일시정지해도 계속 도는 것부터, 백그라운드에서 auto-pause가 한 번만 먹히는 것까지. 하나씩 풀면서 SwiftUI 위젯과 React Native 사이의 경계에서 어떤 가정이 깨지는지 적어둔다.
부딪힌 증상들
- 일시정지 버튼을 눌러도 위젯 타이머가 계속 흘렀다
- auto-pause가 걸린 순간 앱은 5초인데 위젯은 4초였다
- 위젯의 일시정지 버튼을 눌러도 앱 쪽에서는 아무 일도 일어나지 않았다
- 러닝 중과 일시정지 중에 타이머 폰트가 다르게 보였다
- 위젯에 들어갈 로고 이미지가 표시되지 않았다
- pause/resume을 여러 번 반복하면 누적 시간이 어긋났다
- 백그라운드 상태에서 첫 auto-pause는 작동했지만, 그 다음부터는 트리거되지 않았다
원인을 하나씩 추적한 과정
일시정지해도 타이머가 계속 도는 이유
LiveActivityManager.update()를 보니 isPaused=true인 경우에도 startTimeInMilliseconds를 그대로 두고 있었다. SwiftUI의 Text(timerInterval:)는 시작 시각이 설정되어 있으면 알아서 카운트를 굴리는 구조라, 일시정지 의도가 위젯에 전달되지 않았다.
auto-pause 시간 어긋남
JS 쪽 스톱워치가 시간을 가지고 있는데, Native에서 그 값을 받지 않고 자체 계산했다. 두 시계가 따로 흘러서 1초 단위로 어긋났다.
위젯 버튼이 앱에 닿지 않는 이유
딥링크 핸들러에서 isRunningRef.current와 workoutState 두 가지 조건을 AND로 묶고 있었다. 둘이 미묘하게 어긋나 있을 때 분기가 통째로 빠졌다.
폰트 불일치
러닝 중에 보여주던 Text(timerInterval:)에는 .monospacedDigit()을 줬는데, 일시정지 시 보여주는 Text(duration)에는 안 붙였다. 모노스페이스 처리가 한쪽만 되어 있어서 자릿수가 바뀔 때마다 미묘하게 출렁였다.
위젯에서 이미지가 안 보이는 이유
Image(imageName)을 그대로 썼는데, 위젯 확장은 메인 앱과 별도 프로세스라 Asset 카탈로그 접근이 자유롭지 않았다.
누적 시간 오류
pauseStartDate를 resume 시에 초기화하지 않아서, 다음 일시정지에서 잘못된 시작 시점이 잡혔다.
백그라운드 auto-pause 실패
setTimeout 같은 JS 타이머는 앱이 백그라운드로 들어가면 suspend된다. 첫 auto-pause는 앱이 포그라운드일 때 트리거됐기 때문에 동작했지만, 그 다음부터는 JS 코드 자체가 멈춰 있어서 어떤 콜백도 돌지 않았다.
해결 과정
isPaused=true일 때 startTimeInMilliseconds = nil로 두고, 위젯이 타이머 대신 고정된 duration 문자열을 그리도록 바꿨다. 이렇게 하면 SwiftUI가 카운트를 멈춘다.
JS-Native 시간 어긋남은 JS에서 계산한 경과 시간을 Native로 명시적으로 넘기는 식으로 풀었다. WorkoutSessionManager.pauseWorkout(elapsedTimeMs) 파라미터를 추가하고, 브릿지 파일(WorkoutSessionManager.m)도 같이 수정해서 정확한 값이 한쪽에서 한쪽으로 흐르게 했다.
딥링크는 isRunningRef.current 조건을 빼고 workoutState 하나만 보도록 바꿨다. 신뢰할 수 있는 상태 하나만 두는 편이 동기화 어긋남보다 훨씬 안전했다. 딥링크 URL 형식도 action=pause / action=resume으로 통일했다.
폰트는 모든 타이머 Text에 .monospacedDigit()을 적용했다. LiveActivityView.swift와 LiveActivityWidget.swift 양쪽을 수정해서 어떤 상태든 같은 폰트가 나오게 했다.
이미지 로딩은 resizableImage(imageName:) 헬퍼를 썼다. Image.dynamic()으로 Asset과 파일 경로 양쪽을 모두 지원하게 만들었다.
누적 시간 추적은 totalPausedDuration(누적 일시정지 시간)과 pauseStartDate(현재 일시정지 시작 시점)를 분리해서 관리했다. resume 시점에 정확히 초기화했다.
백그라운드 auto-pause는 결국 JS에서 처리하는 걸 포기하고 Native로 옮겼다. GPS 위치 업데이트를 받는 LocationTracker.swift에서 직접 처리하는 구조다.
checkAutoPause(currentSpeed:): 속도가 0.3 m/s 미만이면 5초 후 자동 일시정지triggerAutoPause(): Live Activity 즉시 업데이트 + JS에 이벤트 전송triggerAutoResume(): 속도가 다시 잡히면 3초 후 자동 재개- JS는
autoPauseStateChanged이벤트만 받아서 상태 동기화