- 두 살 딸이 비행기에 빠진 뒤, 주변 항공기를 휴대폰 레이더처럼 보여주는 iOS 앱 Aviator를 몇 저녁 동안 사이드 프로젝트로 만듦
- FlightRadar24는 항공기 위치를 지도에 보여주지만, 실제 하늘에서 어느 방향을 봐야 하는지와 아이가 찾기 쉬운 비행기가 무엇인지는 따로 판단해야 했음
- 앱은 Core Location 방향 정보, OpenSky Network의 비상업용 REST API, MapKit 주석, SwiftUI UI를 묶어 항공기를 기기 방향에 맞춰 표시함
- MVP 테스트에서 실제 비행기와 앱 표시가 맞는 것을 확인한 뒤, 지도 숨김, 낮은 고도 항공기 확대, 레이더 애니메이션, CRT 효과, 사운드·햅틱, 색상 설정을 더함
- 최종 앱은 App Store에 공개됐고, 이후 개선 후보로 줌 레벨, OpenSky 고급 API, 출발·도착 국가 표시, Metal 셰이더 개선, 거리·고도 필터, “zany mode”가 남아 있음
비행기 관찰에서 앱 요구사항으로
- 딸은 여름 해외여행에서 3시간 비행을 앞두고 비행기 여행을 기대했고, 탑승 후 승무원의 안내로 조종석을 구경한 뒤 비행기에 더 빠짐
- 이후 하늘의 비행기를 찾아 달라고 자주 요청했고, 정원에서 한 시간 동안 어깨에 태운 채 저녁 하늘의 비행기를 찾는 일까지 생김
- FlightRadar24는 지도 위 항공기 위치를 보여줬지만, 실제 하늘에서 어느 방향을 봐야 하는지는 사용자가 직접 맞춰야 했음
- 2차원 지도에서는 40,000피트의 Learjet과 London City Airport에서 막 이륙한 AirBus가 비슷하게 보일 수 있지만, 실제 하늘에서는 낮은 고도의 큰 항공기가 더 찾기 쉬움
- 두 살 아이에게 필요한 것은 지도 해석이 아니라 단순한 비행기 찾기였음
- 초기 문제는 세 가지로 정리됨
- 방향 맞춤: 기기 방향에 따라 항공기 위치가 실제 하늘 방향과 대응해야 함
- 크기 표현: 항공기 고도에 따라 화면 크기가 달라져야 함
- 사용성: 업무용 비행 추적 앱이 아니라 복고풍 장난감 같은 앱이어야 함
Aviator의 초기 설계
- 앱 아이디어는 “주변 항공편을 레이더에 표시”하는 것이었고, 프로젝트 이름은 Aviator로 정함
- 핵심 요구사항은 세 가지임
- 기기가 회전하면 화면도 함께 돌아 항공기가 올바른 방향에 표시됨
- 항공기 고도에 따라 화면상 크기가 달라짐
- 아이가 쓰기 좋은 재미있는 복고풍 장난감 느낌을 냄
- 방향 처리는 iOS Core Location API의 heading 업데이트를 사용함
CLLocationManagerDelegate의didUpdateHeading에서 기기 방향을 받아옴- Combine의
CurrentValueSubject로 SwiftUI 뷰에 방향 값을 전달함
- 항공 데이터는 OpenSky Network를 사용함
- 단순 REST API로 특정 위도·경도 범위의 실시간 항공 데이터를 제공함
- 비상업적 사용은 무료임
- 현실적인 레이더 스윕을 위해 몇 초마다 endpoint를 호출하려는 구상임
- 위치 데이터는 Core Location에서 얻고, 사용자 위치 주변의 위도·경도 범위를 쿼리함
- 초기 구상에서는 위도 ±1도, 위치 정밀도 0.1도 약 10km로 사용자 위치를 충분히 흐리게 하려 함
- 실제 API 예시 코드에서는 영국 위도에서 대략 정사각형이 되도록 위도 0.5도, 경도 1도 범위를 사용함
- OpenSky Network API가 비상업용으로 제한되어 있고 앱도 상업 서비스가 아니어서, SwiftUI에서는 단순한 MV 구조를 쓰고 API·Location 같은 핵심 서비스만 분리함
PoC: 방향, 항공 데이터, 지도 검증
- 방향 PoC는
LocationManager싱글턴으로 위치 권한 요청, delegate 설정, heading 업데이트 시작을 처리함 - SwiftUI 뷰는
.onReceive로rotationAngleSubject를 구독하고, 사각형들을 원형으로 배치해 나침반처럼 회전시키는 효과를 구현함 - 테스트 기기에서는 실제 위치 변화에 잘 반응했지만, 0도와 360도를 별개 값으로 다루는 애니메이션 때문에 진북을 지날 때 사각형들이 한 바퀴 도는 시각적 글리치가 생김
- OpenSky REST API는 위도·경도 범위를 지정한
GET요청으로 지역 항공편 배열을 반환함- 예시 endpoint는
states/all에lamin,lamax,lomin,lomax파라미터를 붙이는 방식임 - REST API 문서는 잘 되어 있지만, 응답이 key 없는 구조라 JSON 필드를 순서대로 파싱해야 했음
- Swift의
UnkeyedContainer로icao24,callsign,origin_country,time_position,last_contact,longitude,latitude등을 디코딩함
- 예시 endpoint는
- 항공기 그리기의 첫 시도는 사용자 위치와 항공기 위도·경도 차이에 고정 화면 배율을 곱해 SF Symbol
airplane이미지를 배치하는 방식이었음- 위도·경도 1도의 실제 거리는 위치에 따라 달라져 정확할 수 없음
- 그래도 시작점으로는 유용했음
- 정확도 검증을 위해 SwiftUI
Map을 배경에 깔고, 위에 항공기와 나침반을 겹침- FlightRadar 투영과 비교했을 때 항공기 수와 클러스터는 대략 맞았지만, 위치는 꽤 어긋남
- 이후 항공기를 직접 지도 위 주석(annotation) 으로 그리는 방식으로 전환함
MVP: MapKit 주석과 사용자 테스트
- iOS 17을 타깃으로 하면서 MapKit의 지도 주석 기능을 사용해 항공기 모양 마커를 실제 지리 좌표에 표시함
FlightMapView는Map(position:)위에 항공기 주석을 그리고, 레이더 용도에 맞게.allowsHitTesting(false)로 지도 상호작용을 막음- 고도 기반 크기 조정은 초기에는 높은 항공기를 더 크게 보이도록 로그 스케일을 적용함
- 항공기의
true_track과 Core Location의 사용자 방향을 합쳐 비행기 아이콘 방향을 맞춤
- 항공기의
- 첫 MVP 사용자 테스트에서 딸과 실제로 비행기를 찾았고, 앱에 표시된 항공기가 실제 하늘에서도 보이는 것을 확인함
- 테스트 뒤 두 가지 수정점이 드러남
- 크기 조정이 반대였음: 하늘에서 눈으로 찾기 쉬운 항공기를 보여주려면 낮은 고도의 항공기가 더 커야 했음
- 딸은 지도가 아니라 비행기만 원했으므로, 잡음을 줄이기 위해 지도 제거가 필요했음
- 수정된 스케일 식은
min(2, max(4.7 - log10(flight.geo_altitude + 1), 0.7))임- 로컬 상공 스캔에서 0.7부터 2.0까지의 값이 나왔고, 화면상 분포가 잘 맞았음
레이더 UI 만들기
- OpenSky API는 timeout,
502 Bad Gateway, null 데이터가 담긴 200 응답을 종종 반환함- 무료 비상업 API이고 SLA가 없기 때문에 클라이언트에서 기본 재시도 로직을 추가함
- 다음 날에는 하루 종일 잘 동작했고, 특정 고트래픽 시간대 외에는 대체로 괜찮아 보였음
- 실제 지도를 보이지 않게 하기 위해
MapPolygon오버레이를 사용함- 원래 지도 영역 강조용 오버레이지만, 항공기 주석만 남기고 지도를 덮는 용도로 활용함
- Apple의 오버레이가 지도 위, 주석 아래에 그려져 원하는 결과가 나옴
- 레이더 화면은 선, 동심원, 20도 폭의 회전 angular gradient로 구현함
- 이후 레이더 라인 효과는 360도 angular gradient로 단순화함
- 초록색에서 투명, 검정으로 이어지는 gradient를 회전시켜 스윕과 잔상처럼 보이게 함
- “grug-brained” 방식이 더 잘 맞는 경우가 많다고 평가함
- 기기를 빠르게 돌릴 때 지도 조각이 화면 모서리에 보이는 시각적 아티팩트가 있었고, 레이더 뷰 바깥을 검은색으로 덮는 reverse mask로 해결함
제품화: CRT 효과, 소리, 설정, App Store
- 복고풍 장난감 느낌을 위해 CRT 화면 효과를 추가함
- iOS 17의
colorEffect에 내장된 Metal 셰이더 지원을 사용함 - 셰이더는 검정색이 아닌 픽셀에 scanline 값을 적용함
time파라미터를 넣어 scanline이 빠르게 위로 움직이도록 해 동적인 느낌을 줌
- iOS 17의
- OpenSky Network 정책상 App Store 등록이 괜찮은지 확인하기 위해 문의했고, 20분 안에 답장을 받음
- 레이더 경험과 접근성을 위해 항공편 업데이트 시 시스템 사운드와 햅틱을 추가함
AudioServicesPlaySystemSound(1052)로 beep-boop 소리를 냄- SwiftUI의
sensoryFeedback(.levelChange, trigger:)를 사용함
- 소리가 거슬릴 수 있어
@AppStorage기반 설정을 추가함silent: 무음 모드showMap: 레이더 오버레이를 끄고 아래 지도를 볼 수 있음userColor: SwiftUI color picker로 레이더 색상을 선택함
- Midjourney 구독을 중단한 상태라 Gencraft의 비상업 무료 생성기를 사용해 항공모자를 쓴 딸 느낌의 Aviator 마스코트를 만듦
- Apple Developer Program에 다시 가입하면서 £79를 지불하고 App Store 배포를 준비함
- iOS 17만 타깃하지만, 6.5인치와 5.5인치 iPhone 스크린샷을 제출해야 했음
- 최신 5.5인치 iPhone인 iPhone 8 Plus는 최대 iOS 16이라, AppScreens로 두 크기 스크린샷을 내보냄
- 앱은 App Store에 올라갔고, 다운로드 링크는 Aviator — Radar on your Phone임
다음 버전 후보
- 몇 저녁 동안 만든 사이드 프로젝트 결과에 만족했고, 딸을 위한 장난감을 만든 코딩 경험이 오랜만에 가장 즐거웠음
- 다음 릴리스 후보 기능은 다음과 같음
- 지도 줌 레벨을 추가해 더 가까운 항공기만 레이더에 제한
- OpenSky Network 고급 API로 헬리콥터, 위성, 비행기 크기 등급 표시
- 항공기에 출발·도착 국가 표시 토글 추가
- 더 고급 Metal 셰이더로 CRT 화면 효과 개선
- 컨트롤을 크기 조정 가능한 progressive-disclosure pull-out modal로 리팩터링
- 거리·고도 슬라이더 필터를 구현해 낮고 먼 항공기 등을 숨김
- UFO, 거대 벌레, 외계인을 레이더에 렌더링하는 zany mode 구현