· 14분 소요
-로또 1판 위젯 삽질기 - 1.0.1부터 1.0.4까지의 삽질 과정
최초 출시(1.0.0) 이후, 업데이트를 거듭하며 겪은 일들을 정리했습니다. 하이라이트는 위젯 위치 삽질기라고 할 수 있겠습니다.
목차
1.0.1 — 출시 직후의 부끄러운 버그들
첫 출시 직후 가장 먼저 알게 된 건, 솔직히 좀 부끄러운 버그들이었다.
- 기본 언어가 영어로 배포됨. 앱은 한국어만 지원하는데 실수로 영어로 올려서 App Store 리스팅에 “EN”으로 박혔다. 기능상 문제는 없었지만 다국어 앱을 만들 땐 조심해야 한다는 걸 배웠다.
- 토요일 오후 8시 이후 회차가 꼬임. 로또는 토요일 20:00에 추첨하지만, API로 최신 회차에 정보가 들어오기까지는 시간이 더 걸렸다. 나는 단순히 회차만 체크해서 최신 회차가 올라오면 업데이트가 되도록 개발을 했는데, 토요일 밤에 앱을 켜면 빈 정보가 나오는 현상이 발견된 것이다. 2주간 반복된 테스트 결과, 특정 시간대에는 빈 회차 정보가 전달되는 것을 알게 되었고, 회차 번호가 올라갔더라도 내용이 비어있으면 업데이트하지 않도록 수정하게 되었다.
- 위젯에 거리가 제대로 안 뜨던 문제를 수정했는데, 이 또한 위 문제와 관련된 것이었다. 그리고 그 기능이 잘 동작한다고 착각하게 되었다. (이게 1.0.3~1.0.4 대장정의 서막이 될 줄은 이때 몰랐다.)
1.0.2 — 포커싱을 다듬고, 업데이트 안내를 붙이다
지도의 카메라 이동(포커싱)은 이 앱의 핵심 사용성이다. SwiftUI의 선언형 Map(position:)만으로는 부족해서, UIKit 브릿지(MapFitView)를 따로 만들어 MKMapView.setVisibleMapRect(edgePadding:)을 직접 호출했다. 경로 카드가 뜨기 전/후의 지도 여백을 실측해서 동적으로 재계산하는 부분이 특히 까다로웠다.
문제는 탭 전환과 목록 탭이 둘 다 포커싱을 건드려서 빠르게 누르면 서로 충돌했다는 것. 결국 skipNextCameraFitOnRankChange 같은 플래그로 예외를 피해가는 코드가 들어갔는데, 지금 봐도 아쉬운 부분이다. 나중에 앱이 더 복잡해지면 리팩토링 해볼 생각이다.
또 하나, 위젯을 눌러 앱을 처음(로딩부터) 실행할 때 해당 판매점으로 포커싱되지 않던 문제를 고쳤다. 로딩 중에는 포커싱을 하지 않는 문제가 있었던 것이었다.
새 버전이 나오면 실행 시 업데이트를 안내하는 기능(하루 1회, 선택 사항)을 붙였다. App Store lookup API로 스토어 버전을 조회해 설치 버전보다 높으면 알림을 띄우는 방식이다.
1.0.3~1.0.4 — 위젯 새로고침 시 위치가 변경되지 않는 문제
여기서부터가 진짜 이야기다. “위젯이 현재 위치 기준 거리를 보여준다”는 이 앱의 존재 이유인데, 이게 안 됐다. 앱이 마지막으로 저장한 위치만 써서, 이동 후 위젯을 새로고침해도 거리가 그대로였다. 위젯의 가치는 앱을 안 열고 바로 보는 것인데, 사실상 쓸모없는 상태였다. 나는 주변을 돌아다니면서 이 앱을 계속 사용했지만, 대체로 앱을 켠 뒤에 새로고침을 시도했던 터라 거리 계산이 잘 되고 있는 것처럼 보였던 것이다.
결정적으로 아주 동떨어진 위치에서 새로고침을 오랜만에 했을 때, 전혀 동작하지 않는다는 것을 알게 되었고, 위젯 위치 권한에 대한 큰 오해를 하게 되면서 지난 몇 주 동안 이 버그 하나만 고치는 데 시간을 쓰게 되었다.
이 문제를 푸는 과정에서 나는 세 번의 삽질을 하게 된다.
1막: “NSWidgetWantsLocation 한 줄”
처음엔 위젯의 AppIntent 안에서 CLLocationManager.requestLocation()을 직접 불렀다. 항상 nil. 백그라운드 스레드에서 매니저를 만들면 델리게이트 콜백이 안 온다는 것까지 고쳤는데도 nil. 그러다 “위젯은 When In Use로 위치를 못 받는다”고 잘못 단정하기에 이른다. AI가 그렇게 알려줬기 때문이다.
그래서 처음에는 Always 권한 + 백그라운드 위치 모니터링을 구현했지만, 다른 위치를 사용하는 위젯이 있는 앱들을 설치해서 사용했을 때 잘 작동하는 것을 알게 되었다. 심사 사유·개인정보처리방침까지 모두 수정해 둔 상태였는데, 결국 첫 주에 수정한 작업은 모두 롤백하게 되었다.
다시 내가 만든 코드의 문제라는 점에 집중하게 되었는데, 여기서 완전히 새로운 오해를 하게 된다. 진짜 원인은 위젯 Info.plist에 NSWidgetWantsLocation 키가 없었던 것 때문이라 여기게 된 것이다. 이게 없으면 iOS가 위젯에 위치를 아예 안 준다. 권한을 아무리 올려도 소용없던 이유였다.
결국 1.0.3에는 이 플래그를 켜서 배포를 진행하게 되었다. 그러나 위젯의 새로고침은 여전히 동작하지 않았다.
교훈: “안 된다”고 단정하기 전에 공식 문서의 설정 키부터 확인하자. 한 줄이 원인이었는데 권한 모델을 통째로 갈아엎으려던 게 가장 큰 시간 낭비였다.
2막: “역시 Always가 필수다” (틀림)
NSWidgetWantsLocation을 켠 버전을 설치해서 일주일간 사용하게 되었다. 사실 이 단계에서도 앱이 잠시 포그라운드에 있을 때만 위치를 받아올 수 있다는 점을 알게 된다.
이때 나는 완전히 Always 권한이 있어야 한다고 오해하게 되었고, 또다시 Always 권한을 주도록 구현해서 테스트하기에 이른다. 당연히 Always로 구현한 결과물은 잘 작동했다. 그러나 이 권한은 여전히 뭔가 과하다는 생각이 들었다. 이것도 틀렸다.
3막: 숨어 있던 “앱 또는 위젯을 사용하는 동안” (정답)
진짜 원인은 테스트할 때 기기 권한이 “앱 사용 중”(구버전 grant)이었던 것. 위젯용 하위 권한이 없어서 isAuthorizedForWidgetUpdates가 false였고, 그래서 timeout이 났다. 권한을 잘못 준 상태로 테스트하며 “안 된다 → Always 필요”로 오판한 것이다.
NSWidgetWantsLocation이 켜져 있으면 iOS의 위치 권한 옵션이 “앱 또는 위젯을 사용하는 동안”으로 바뀐다. 이걸 허용하면 When In Use로도 위젯이 위치를 받는다 (Always 불필요). 이 권한으로 바꾸자 timeout이 사라지고 loc:OK가 안정적으로 나왔다.
그래서 최종 구조는:
- 위젯이
getTimeline에서 직접 현재 위치를 요청 (NSWidgetWantsLocation+ “앱 또는 위젯 사용 중” 권한). 정확도 100m + 최근 캐시 우선으로 타임아웃 최소화. - Always·백그라운드·심사 사유·개인정보처리방침 백그라운드 문구는 전부 제거.
- 앱은 권한 점검·안내 UI만 제공.
교훈: “된다”를 한 번의
loc:OK로 판단하면 안 된다. A↔B를 여러 번 오간 근본 이유는 매번 “한 번의 관측”으로 단정했기 때문. 조건(권한 상태·포그라운드 여부)을 통제한 재현이 답이었다.
4막: 같은 값을 가진 두 권한을 구분하기
정답을 찾고 나니 새 문제가 보였다. 앱의 권한 점검 UI가 “앱을 사용하는 동안”과 “앱 또는 위젯을 사용하는 동안”을 구분하지 못했다. 둘 다 CLAuthorizationStatus.authorizedWhenInUse(값 4)로 완전히 같은 값이기 때문이다. 사용자가 “앱을 사용하는 동안”만 켜놨는데도 UI는 “권한 있음”이라고 표시했고, 위젯은 여전히 안 되는데 왜 안 되는지 알 길이 없었다.
이 둘을 가르는 유일한 신호는 authorizationStatus가 아니라 CLLocationManager.isAuthorizedForWidgetUpdates였다. 그래서:
- 앱은 권한을 저장할 때 이 값을 함께 읽어 별도로 추적하고,
- 설정 시트는 “권한 있음/없음”을 이 값 기준으로 판정해 “앱을 사용하는 동안만 (위젯에는 표시 안 됨)“을 정확히 표시하고 바꾸는 법을 안내하며,
- 위젯도 가드를
isAuthorizedForWidgetUpdates로 바꿔 “앱 사용 중”만이면 즉시 종료해 8초 타임아웃을 회피하도록 했다.
교훈: iOS 권한은 열거형 값 하나로 다 표현되지 않는다. “앱 사용 중”이라는 같은 상태 안에 위젯용 하위 권한이 숨어 있고, 그건 별도 API로만 확인된다. 사용자에게 “권한 있음”이라고 뭉뚱그리지 말고, 기능이 실제로 동작하는 조건을 그대로 보여줘야 한다.
곁가지로 함께 고친 것들
- 위젯에 2·3등이 안 뜨던 문제: 2·3등 데이터 로드가 앱 내 광고 잠금 뒤에 묶여 있었다. 로드는 잠금과 무관하게 항상 하고, 앱 화면 표시만 잠그도록 분리했다. (위젯은 광고와 무관하게 표시).
- 온라인 판매점에 거리가 뜨던 문제: “동행복권”(온라인 전용) 판매점을 후보군 저장 단계에서 제외. 온라인 판별 기준을
LotteryStore.isOnlineSeller한 곳으로 모았다. - 회차 표시 불일치: 새 회차 발표 전, 목록은 이전 회차인데 상단 회차 번호만 최신으로 뜨던 문제를 목록·회차 표시가 일치하도록 수정했다.
- 권한 설정 버튼 추가: 첫 설치 후 권한 설정을 변경하는 것은 정말 귀찮고 어렵다는 점을 알았기 때문에 설정 버튼을 통해 권한을 쉽게 변경할 수 있게 기능을 추가했다.
마치며
세 번 뒤집은 위젯 위치 문제가 결국 가르쳐준 건 하나였다. “안 된다”의 원인이 코드가 아니라 내 테스트 환경일 수 있다는 점이다. 설계를 갈아엎기 전에, 조건을 통제하고 재현부터 하는 습관을 들이자. 이번 일은 초심자가 치러야 할 큰 비용 중 하나였다고 기억하려고 한다.
앱
로또 1판
이번 주 로또 1등 당첨 판매점을 쉽고 빠르게 확인하는 앱입니다.
번호는 못 뽑아드리지만, 가장 가까운 1등 당첨 판매점은 확실하게 찾아드립니다 - 로또 1판
