iOS 빌드 서명 및 위젯 중복 등록 트러블슈팅
EAS Production 빌드를 돌렸더니 빌드 자체가 깨졌다. 위젯(Live Activity 익스텐션)을 추가한 뒤로 한 번도 정상적으로 Archive를 만들지 못했고, 빌드 외에도 위젯 상태 동기화·다국어 음성 안내 같은 자잘한 버그가 같이 따라붙었다.
부딪힌 문제
처음 마주한 에러는 EAS Production 빌드 단계에서 발생한 LiveActivity has conflicting provisioning settings 였다. 다음으로는 Archive가 진행되는 도중 Unexpected duplicate tasks 로그를 남기며 멈췄다. 빌드는 그렇다 치고, 앱 내부에서도 위젯을 누르면 일시정지/재개 상태가 앱 쪽 state와 어긋나는 동기화 버그가 있었고, 러닝 중 언어를 바꾸면 음성 안내가 옛 언어 그대로 나가는 문제까지 겹쳤다.
원인을 하나씩 들여다본 과정
서명 설정 충돌
app.plugin.js에서 CODE_SIGN_STYLE = 'Automatic'로 두면서도 CODE_SIGN_IDENTITY = 'iPhone Distribution'을 따로 적어놨던 게 문제였다. Xcode가 자동 관리 모드인데 ID를 수동 지정해놨으니 충돌로 판단해서 빌드를 거부한 것이다.
appex 중복 등록
Unexpected duplicate tasks의 정체는 LiveActivity.appex가 Copy Files와 Embed App Extensions 두 빌드 페이즈에 모두 들어가 있었다는 것이었다. npx expo prebuild --clean 후 project.pbxproj를 grep해서야 보였다.
위젯 동기화 버그 — Stale Closure
handleWidgetPause / handleWidgetResume 핸들러가 workoutState를 클로저로 캡처하고 있어서, 위젯에서 신호가 와도 옛 값으로 분기되고 있었다. (Stale Closure는 useEffect나 콜백이 옛 state·prop을 붙들고 있는 React 흔한 함정이다.)
다국어 음성 동기화 누락
i18n에서 언어를 바꿔도 그 변화를 LocationTracker나 WorkoutSessionManager가 알 길이 없었다. JS-Native 사이에 알림 채널을 만들어두지 않은 게 원인이었다.
해결 과정
서명 충돌은 app.plugin.js에서 CODE_SIGN_IDENTITY = '"-"'로 바꿔 Xcode 자동 관리에 맡겼다. 메인 앱 타겟과 LiveActivity 타겟 양쪽에 동일하게 적용했다.
중복 빌드 페이즈는 같은 플러그인 안에서 Copy Files 페이즈에 들어간 LiveActivity.appex를 자동으로 빼는 로직을 추가했다. Embed App Extensions 한 곳에만 남기니 충돌이 사라졌다.
Stale Closure는 useRef로 우회했다. run/index.tsx에서 workoutState를 직접 참조하던 부분을 모두 workoutStateRef.current로 바꿨다. 이렇게 두면 핸들러가 항상 최신 값을 본다.
언어 동기화는 i18n 모듈에 updateLanguage 메서드를 더해서 언어가 바뀔 때 Native 모듈에 명시적으로 알리도록 했다. 위젯에서도 같은 언어를 보려면 UserDefaults까지 같이 동기화해야 했다.