· 16분 소요

-

번호 뽑는 기능이 없는 이상한 로또 앱 - "로또 1판"을 만들기까지의 생각보다 길었던 여정을 공유합니다.

지도 기능만 있는 로또 앱을 개발하는 과정에서 있었던 고민들을 정리해서 공유합니다.

시작은 종합 로또 앱이었다

처음 구상은 훨씬 컸다. 번호 자동 생성, 지도로 판매점 찾기, 과거 당첨 번호 조회, 3D 물리 시뮬레이션으로 번호 뽑는 재미 요소까지…로또 앱 하면 떠올릴 수 있는 기능을 다 넣은 종합 앱을 만들고 싶었다.

그런데 막상 하나씩 뜯어보니, 번호 생성이든 당첨 번호 조회든 이미 잘 만든 앱이 시장에 넘쳐났다. 굳이 그 경쟁에 하나 더 얹을 이유가 없었다. 대신 내가 직접 써보면서 가장 불편했던 기능 하나에 집중하기로 했다

가장 가까운 1등 당첨 판매점을 지도로 찾는 것.

그렇게 번호 생성 기능은 아예 빼버리고, 지도 하나만 들어간 앱을 만들기로 했다. 앱 이름도 “로또 1판”으로 “로또 1등 판매점 찾기”의 줄임말로 결정하게 되었다.

MapKit을 고르면서 자연스럽게 정해진 것들

지도 SDK를 고르는 게 생각보다 큰 갈림길이었다. 국내 지도 SDK(카카오맵, 네이버지도)는 사용량이 일정 기준을 넘으면 유료로 전환되는데, 이 앱은 애초에 “사용자가 오래 머물게” 만들 생각이 없었다. 지도를 잠깐 켜서 가장 가까운 판매점만 빠르게 확인하고 나가는 게 목표였고, 그 정도 체류 시간으로는 광고 수익으로 유료 SDK 비용을 감당하기 어렵다고 판단했다. 그래서 무료인 Apple MapKit을 선택했다.

그리고 이 선택이 또 다른 선택으로 이어졌다. 사실 이 프로젝트는 원래 Flutter로 시작했었는데, MapKit을 제대로 쓰려면 네이티브가 유리했다. 그렇게 자연스럽게 Flutter를 포기하고 SwiftUI 네이티브 앱으로 갈아탔다. 결론적으로 iOS만 서비스하기로 결정한 것이다. 어차피 나만 쓰지 않을까 하는 생각이 들어서 그리 어려운 결정은 아니었다.

지도 스펙의 나머지는 비교적 단순했다. 카메라가 대한민국 영역 밖으로 나가지 않게 경계를 걸어두었고, 기본 지도 컨트롤은 다 끄고 현재 위치 버튼만 직접 만들어 넣었다. 화면은 지도 58%, 목록 42% 비율로 잡았는데, 이건 목록에 판매점이 대략 3개 정도 보이면 딱 적당하다는 기준으로 정했다.

가장 오래 붙잡고 있었던 기능 - 포커싱

의외로 가장 손이 많이 간 부분은 “지도를 얼마나, 어디를 중심으로 확대할 것인가”였다. SwiftUI의 Map이 제공하는 선언형 카메라(position:)만으로는 원하는 정밀도가 안 나와서, 결국 실제 MKMapView에 직접 setVisibleMapRect(edgePadding:)을 호출하는 UIKit 브릿지(MapFitView)를 따로 만들었다.

여기서 처음에 실수한 게, safe area와 하단 목록 패널이 차지하는 공간을 계산에서 빼먹고 지도 여백을 계산했던 것이다. 그러다 보니 핀이 목록 카드에 가려지거나, 화면 중심이 미묘하게 어긋나는 오차가 계속 났다. 눈으로 좌표와 패딩 값을 확인하면서 튜닝하려고 디버그 오버레이까지 따로 만들어야 했다. 블지금도 코드에 #if DEBUG && false로 죽어있는 채 남아있다. 언젠가 이 지도 피팅 로직을 다시 정리할 일이 있으면 꺼내볼 생각이다.

탭 메뉴와 목록이 자꾸 서로의 발을 밟았다

“전체 / 1등 / 2등 / 3등” 필터는 TabView가 아니라 가로 스크롤 칩을 직접 만든 커스텀 탭이다. 문제는 이 필터 전환과 목록에서 판매점을 탭하는 동작이 둘 다 “카메라를 다시 피팅한다”는 같은 부수 효과를 공유한다는 점이었다. 필터를 바꾸면 카메라가 새로 고른 랭크 기준으로 다시 맞춰져야 하고, 판매점을 탭하면 경로 중심으로 다시 맞춰져야 하는데, 이 둘이 서로 충돌하면서 원치 않는 순간에 카메라가 튀는 문제가 생겼다.

이걸 깔끔하게 분리하고 싶었는데 잘 안 됐다. 결국 skipNextCameraFitOnRankChange라는 플래그를 하나 만들어서 특정 상황(딥링크로 들어왔을 때 등)에만 부수 효과를 건너뛰는 식으로 땜질했다. 구조적으로 더 깔끔하게 풀 수 있었을 텐데, 아쉬운 부분으로 남아있다.

위젯은 새로고침 빈도가 고민이었다

위젯은 앱을 열지 않아도 동작해야 해서, App Group으로 앱과 위젯이 최근접 판매점 데이터를 공유한다. 같은 회차라면 API를 다시 부르지 않고 저장해둔 후보군(상위 20개)만 위치 기준으로 재정렬하고, 회차가 바뀔 때만 전체를 다시 가져온다.

수동 새로고침 버튼에는 10분 쿨다운을 걸었는데, 사실 명확한 근거가 있었다기보다는 “무제한으로 누르게 하면 배터리에 안 좋지 않을까” 하는 막연한 감에 가까웠다. 당첨 판매점 정보 자체가 자주 바뀌는 데이터는 아니니 10분이면 충분하다고 봤는데, 실제 사용자들이 어떻게 느낄지는 좀 더 지켜봐야 할 것 같다.

또 하나 번거로웠던 건, 위젯 확장이 메인 앱과 완전히 분리된 프로세스라서 겉보기엔 똑같은 데이터도 타입을 따로 정의해야 했다는 점이다. 앱 쪽 LotteryStore/WidgetCandidate와 위젯 쪽 StoreRaw/Candidate가 구조는 같은데 이름과 정의가 따로 노는 게 좀 아이러니했다.

그리고 새로고침 기능을 포함해서 배포했지만, 여전히 거리 계산이 제대로 안되는 부분이 버그로 남아있다. AI의 조언에 따르면 위젯에서도 위치 정보를 쓸 수 있다고 했다. 그래서 크게 의심하지 않고 쭉 개발을 진행했던 건데, 위젯에서는 별도의 구현이 필요한 것으로 보인다. 현재 구현을 위해 공부 중이다.

심사·배포하면서 처음 배운 것들

앱을 다 만드는 것과 스토어에 올리는 건 완전히 다른 종류의 일이었다. 이번 배포를 통해 배운 것들을 정리했다.

  • 빌드 번호는 올릴 때마다 올려야 한다. 버전은 그대로여도 App Store Connect에 새 빌드를 업로드하려면 빌드 번호를 반드시 증가시켜야 했다.
  • 버전 번호는 심사가 끝난 뒤에 올려야 한다. 이미 한 번 심사를 통과해 출시된 버전 번호는 재사용할 수 없다.
  • 언어 설정을 실수로 영어로 배포한 적이 있다. 앱 자체는 한국어만 지원해서 기능적으로는 문제없었지만, App Store 리스팅에 “EN”으로 표기됐다. 다음에 다국어 앱을 만든다면 이 부분은 더 신경 써야 할 것 같다.
    • App Store Connect 정보에 한국어로 해뒀더라도, 앱에서 지원하는 언어를 영어로 두면 EN으로 표시되는 것이었다.
  • AdMob은 앱이 스토어에 올라간 뒤에도 광고 계정 인증까지 시간이 걸린다. 체감상 하루 정도 걸렸다. 배포 직후 광고가 바로 안 뜬다고 당황할 필요는 없었다.
    • iOS에는 추적금지라는 기능이 있는데, 나는 이게 광고 자체를 막아주는 거라고 착각했었다. 나는 이 기능을 아예 끄고 사는 사람이어서 다른 앱에서 광고가 나오는 것들을 이미 경험했음에도 막상 내가 개발하는 앱에서는 이것과 광고의 출력이 관계가 없다는 것을 몰랐다.
    • 그래서 개발단계에서도 한 일주일동안은 추적금지때문에 광고가 나오지 않는다고 생각했다.
    • 결론적으로 추적금지가 되어 있어도 광고는 잘 나온다. AdMob에서 앱이 인증되기까지의 시간이 지난 후에는 문제없이 광고가 나오고 있다.

마케팅 스크린샷은 또 다른 프로젝트였다

정작 앱 개발보다 App Store 스크린샷 준비가 별도의 작은 프로젝트처럼 느껴졌다. 웹 검색을 통해 Claude Code 스킬로 도움을 받을 수 있음을 알게 되었다.

실제 판매점 데이터를 그대로 쓰고 싶어서, 시뮬레이터 위치를 애플코리아(강남) 좌표로 고정해두고 캡처했다. 가짜 데이터를 심을 필요 없이 실제 API가 반환하는 진짜 판매점이 지도에 찍혔다.

몇 가지는 예상 못 한 부분에서 걸렸다.

  • 지도 확대(경로 피팅) 애니메이션이 코드로 트리거하면 동작하지 않았다. selectStore/showRoute를 코드로 직접 호출해서 캡처하면 지도가 확대되지 않고, 실제로 화면을 탭했을 때만 재현됐다. 결국 그 화면 하나는 시뮬레이터를 띄워두고 직접 탭한 다음 xcrun simctl io <device> screenshot으로 따로 찍었다.
  • 위젯은 별도 프로세스라 직접 캡처가 안 됐다. 위젯 뷰가 메인 앱과 다른 익스텐션 타겟에 있어서, 캡처 코드에서 바로 가져다 쓸 수 없었다. 위젯 소스를 메인 타겟에 끌어오면 Release 빌드에도 그 코드가 딸려 들어가버려서, 대신 위젯과 똑같은 공유 데이터(App Group)를 읽는 DEBUG 전용 미니 렌더러를 새로 만들어 해결했다.
    • AI가 알아서 렌더링해서 스크린샷을 찍어줬다.
  • Next.js 스크린샷 편집기가 계속 404가 났다. 알고 보니 macOS 기본 파일시스템이 대소문자를 구분하지 않아서, Next.js가 찾는 app/ 라우터 폴더가 Swift 프로젝트의 App/ 폴더와 충돌하고 있었다. 편집기를 아예 별도 하위 폴더(screenshot-studio/)로 분리하고 나서야 정상 작동했다.
  • 지도 저작권 표기(“Legal”)가 시뮬레이터에서 계속 영어로 나왔다. 시뮬레이터 시스템 언어를 한국어로 바꾸고 재부팅까지 해봤는데도 MapKit의 저작권 링크만 영어로 고정되어 있었다. 시뮬레이터 환경의 한계로 보이고, 실기기에서는 다를 수도 있을 것 같다.
    • 실 기기에서는 한글로 잘 나왔다.
  • App Store Connect에 올릴 때 사이즈 슬롯 때문에 잠깐 헤맸다. 6.9인치용(1320×2868)으로 만든 이미지를 6.5인치 슬롯에 올리려다 보니 “1242×2688 또는 1284×2778이어야 합니다” 같은 에러가 났다. 사이즈별로 업로드 슬롯이 따로 있다는 걸 그때 알았다.
    • 6.9인치만 업로드하면 알아서 나머지 사이즈에 맞춰주는 것이었다.

마치며

이 앱을 계기로 애플 개발자 프로그램도 가입하게 되었고, 뭔가 진짜 앱 개발자가 된 느낌이 들기도 했다. 대체로 AI의 도움을 받아 완성한 것이지만, 앞으로 내게 필요한 앱을 또 만들 수 있지 않을까 그런 생각이 들었다. 다음에는 어떤 앱을 만들게 될까? 아직은 잘 모르겠다!


로또 1판

이번 주 로또 1등 당첨 판매점을 쉽고 빠르게 확인하는 앱입니다.

번호는 못 뽑아드리지만, 가장 가까운 1등 당첨 판매점은 확실하게 찾아드립니다 - 로또 1판