SwiftUI 앱과 WidgetKit의 공유 상태
SwiftUI 앱, WidgetKit, 캐시와 딥링크가 같은 공원 상태를 이어받는 구조
한강자리의 iOS 구현은 앱, 위젯, SwiftData 캐시, App Group snapshot, 딥링크와 외부 지도 앱을 한 흐름으로 연결한다. App Group snapshot은 앱과 위젯이 함께 읽는 작은 표시값 묶음이다.
이 앱은 한강에 가기 전과 한강 안에서 짧게 확인하는 앱이다. 자주 가는 공원은 위젯에서 바로 보여야 하고 위젯을 눌렀을 때는 사용자가 보던 공원과 화면으로 이어져야 했다.
사용자는 집을 나서기 전, 지하철을 타기 전, 주차장 입구에 가까워졌을 때와 공원 안에서 시설을 찾을 때처럼 짧은 순간에 앱을 본다. 클라이언트는 API 값을 배치하는 일과 함께 이 순간 사이의 상태를 이어야 했다.
네트워크가 잠깐 느리거나 위젯이 오래된 snapshot을 보고 있어도, 화면은 사용자가 오해하지 않게 상태를 설명해야 한다.
iOS 구현에서는 같은 상태를 어디에 남기고 어떻게 다시 읽을지 먼저 정했다. API 응답은 SwiftData에 저장되고, 앱 상태 저장소는 화면이 쓰기 좋은 모양으로 바꾼다.
위젯은 App Group snapshot을 읽고, 딥링크는 다시 앱의 특정 공원과 화면으로 돌아온다. 두 구성요소는 사용자가 위젯에서 보던 공원과 상태를 앱으로 넘긴다.
앱과 위젯이 공유하는 코드
현재 클라이언트는 세 target으로 나뉜다.
Hangangjari: SwiftUI 기반 메인 앱.HangangjariWidget: 주차 위젯과 일반 공원 위젯.HangangjariTests: model, networking, cache, widget 로직 검증.
앱과 위젯은 모델, 네트워킹, repository, localization, widget snapshot 코드를 공유한다. WidgetKit extension은 앱과 같은 process가 아니므로, 함께 읽어야 할 값은 App Group 영역에 따로 써둔다.
flowchart LR View["SwiftUI 화면"] --> Store["앱 상태 스토어\n@Observable @MainActor"] Store --> ParkingRepo["주차 저장소"] Store --> ForecastRepo["예측 저장소"] ParkingRepo --> API["API 클라이언트"] ForecastRepo --> API ParkingRepo --> Cache["SwiftData 캐시"] ForecastRepo --> Cache Store --> Snapshot["위젯 스냅샷 스토어"] Widget["WidgetKit 프로바이더"] --> Loader["위젯 항목 로더"] Loader --> Snapshot Loader --> API Loader --> Location["위젯 위치 리졸버"] Settings["위젯 / 즐겨찾기 설정"] --> AppGroup["App Group UserDefaults"] Widget --> AppGroup API --> Backend["FastAPI /v2"]
한곳에서 조율하는 화면 상태
앱 상태 저장소는 화면의 확인 항목을 한곳에서 정리하고 네트워크와 저장소 세부사항을 바깥으로 밀어내는 객체다.
현재 상태에는 다음 그룹이 들어 있다.
- 공원 목록.
- 공원별 주차장 목록과 주차장별 최신 상태.
- 공원별 나들이 overview.
- 첫 화면용
HomeSummary. - 주차 예측 overview와 timeline.
- 공원 혼잡 예측 overview와 timeline.
- 즐겨찾기, 위젯, 딥링크에서 파생되는 화면 상태.
앱 상태 저장소는 HTTP 세부 구현을 직접 다루지 않는다. API 호출과 SwiftData 저장은 repository가 맡고, Store는 화면에 필요한 상태 전환과 표시할 모양을 만든다.
Store에는 화면 상태 조율만 남겼다. URL 구성, request proof, decoding과 SwiftData row upsert는 별도 구성 요소가 맡는다.
Store 안에는 “이 공원을 보고 있다”, “이 값은 로딩 중이다”, “이 snapshot은 화면에 보여도 된다”처럼 UI가 바로 써야 하는 상태만 남기려고 했다.
캐시와 API 사이의 저장소
주차 repository는 parks, lots, statuses, outing overview, home summary를 다룬다. 예측 repository는 forecast config, 주차 예측, 공원 혼잡 예측, timeline payload를 다룬다.
Repository는 API 호출과 함께 로컬 캐시 최신성 판단, 응답 저장과 위젯 snapshot 갱신을 맡았다.
sequenceDiagram
autonumber
participant View as SwiftUI 화면
participant Store as 앱 상태 스토어
participant Repo as 저장소
participant Cache as SwiftData 캐시
participant API as API 클라이언트
participant Snapshot as 위젯 스냅샷 스토어
View->>Store: 첫 화면에 보여줄 값 요청
Store->>Repo: 저장된 첫 화면 값 확인
Repo->>Cache: 캐시에 남은 응답 읽기
alt 캐시 최신
Cache-->>Repo: 쓸 수 있는 첫 화면 값
Repo-->>Store: 캐시에서 만든 화면 모델
else 오래됐거나 없음
Repo->>API: 첫 화면 응답 요청
API-->>Repo: 해석된 첫 화면 응답
Repo->>Cache: 첫 화면/주차/예측 값 저장
Repo-->>Store: 새로 받은 화면 모델
end
Store->>Snapshot: 위젯용 주차/일반 표시값 갱신
Store-->>View: 화면 상태 갱신
이 덕분에 첫 화면은 여러 API를 따로 기다리지 않는다. 서버가 조합한 화면용 응답을 받고, 앱은 SwiftData에 보존한 뒤 화면과 위젯에 맞게 나눈다.
Repository, cache와 snapshot을 거치면서 코드는 늘어난다.
앱을 다시 열거나 위젯의 네트워크 요청이 실패하거나 서버에 새 필드가 생겼을 때는 repository, cache와 snapshot 가운데 확인할 위치를 고를 수 있다.
앱 밖에서 실행되는 WidgetKit
한강자리에는 두 종류의 위젯이 있다.
- 주차 위젯: 추천 주차장, 잔여 대수, 상태, 근처 정렬.
- 일반 공원 위젯: 혼잡도, 날씨, 행사, 시설 신호, 오늘 가도 될지 보는 정보.
위젯은 저장된 snapshot을 먼저 본다. 신선하면 그대로 사용한다. 없거나 오래되었으면 API를 호출하고 실패하면 stale snapshot이나 placeholder로 내려간다.
flowchart TB Provider["위젯 타임라인 프로바이더"] --> Request["위젯 로드 요청"] Request --> Loader["위젯 항목 로더"] Loader --> Cached["App Group 스냅샷"] Loader --> API["home-summary / 예측 / 상태 API"] Loader --> Location["선택한 위젯 위치"] API --> Builder["스냅샷 빌더"] Cached --> Entry["위젯 표시 항목"] Builder --> Entry Entry --> Timeline["타임라인\n새로고침 정책"]
주차 위젯과 일반 위젯은 snapshot key도 나뉜다. 같은 공원을 보더라도 먼저 보여줘야 할 말이 다르기 때문이다.
앱과 위젯의 공통 API 구조
API client는 v2 앱 API를 호출한다. 쓰기성 요청이나 민감한 요청에는 앱 접근 토큰과 request proof를 붙이는 길이 있다.
앱 접근 검증에서는 이 선을 지켰다.
- 앱과 위젯은 같은 API client 구조를 공유한다.
- bootstrap 흐름과 보호된
v2앱 API를 구분한다. - 실패 시 unsigned fallback을 허용할지 여부는 실행 정책으로 분리한다.
- telemetry와 push subscription 같은 쓰기 요청은 읽기 요청보다 강한 검증을 거친다.
App Group과 딥링크로 이어지는 상태
WidgetKit extension은 앱과 별도로 실행된다. App Group snapshot, repository, 서버의 화면용 응답과 딥링크는 사용자가 방금 보던 공원과 상태를 함께 이어받아야 했다.
서버와 앱은 fresh, stale, unavailable을 같은 UI 상태로 해석한다. 값의 최신성, 재조회 가능 여부와 딥링크의 공원 선택이 맞아야 이동 전후의 짧은 확인이 이어진다.
iOS 쪽에서는 앱, 위젯, 캐시와 딥링크가 같은 공원과 값을 이어받게 만드는 데 시간이 많이 들었다. 이 공유 상태가 위젯에서 확인한 정보를 앱의 상세 화면까지 잇는다.
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중