home-summary API로 첫 화면을 묶기
주차, 나들이, 예측, 출처 상태를 한 응답에 담아 앱과 위젯의 첫 값을 맞췄다
한강자리 첫 화면은 여러 종류의 데이터를 동시에 보여준다. 주차장, 현재 값, 예측, 혼잡도, 행사, 시설 요약, 길찾기 후보, 출처 확인 결과가 한 화면에 들어온다.
처음에는 클라이언트가 endpoint를 여러 번 호출해서 조합해도 된다고 생각하기 쉽다. 하지만 화면을 실제로 만들면 금방 어긋남이 생긴다. 주차 섹션은 방금 갱신됐는데 행사 섹션은 오래된 값이고, 예측은 다른 horizon을 보고 있고, 위젯은 일부 값만 받아 전혀 다른 인상을 줄 수 있다.
그래서 한강자리에서는 첫 화면에 필요한 값을 서버가 한 번에 묶어 준다. 이 결정은 클라이언트 코드를 편하게 만들기 위한 것만은 아니었다. 첫 화면은 사용자가 앱을 평가하는 첫 몇 초이고, 그 몇 초 안에서 값들이 서로 다른 시간을 말하면 안 됐다.
첫 화면에 필요한 값을 한 번에 받는다
home-summary는 한강자리의 첫 화면용 응답이다. 목표는 “한 공원에 대해 앱과 위젯이 처음 볼 값을 한 번에 받는다”이다.
flowchart LR
API["GET /v2/parks/{park_id}/home-summary"] --> Park["공원"]
API --> Parking["주차 집계<br/>주차장 상태"]
API --> Forecast["주차 예측<br/>공원 혼잡 예측"]
API --> Outing["나들이 요약<br/>신호 · 시설 그룹 · 길찾기"]
API --> Freshness["갱신 상태<br/>출처 확인"]
이 endpoint가 없으면 앱은 주차장 목록, 현재 값 목록, 예측 overview, 나들이 overview, 출처 확인 결과를 따로 불러야 한다. 그러면 첫 화면에서 다음 문제가 생긴다.
- endpoint별 성공/실패가 서로 어긋난다.
- 화면 일부가 이전 공원 값으로 남을 수 있다.
- 위젯은 여러 네트워크 호출을 감당하기 어렵다.
- cache key와 갱신 상태 확인이 복잡해진다.
- API가 바뀔 때 iOS decoding에서 방어해야 할 범위가 넓어진다.
서버가 첫 화면 값을 묶어 주면 이 문제가 한 응답 안으로 줄어든다.
물론 home-summary가 커질수록 조심할 점도 생긴다. 모든 것을 넣는 endpoint가 되면 다시 느려지고 깨지기 쉽다. 그래서 이 API에 넣을지는 “첫 화면에서 바로 필요한가”로 정했다. 상세 화면에서만 필요한 긴 목록이나 원문 payload는
여기로 끌어오지 않는다.
응답은 네 묶음으로 나눴다
iOS에서 받는 값은 크게 네 부분으로 나뉜다.
| 필드 | 역할 |
|---|---|
park | 현재 summary가 어떤 공원을 말하는지 확인 |
parking | 주차 aggregate, 추천 주차장, lots, statuses |
forecast | horizon과 주차/공원 혼잡 예측 overview |
outing | 일반 화면 신호, 시설 그룹, 길찾기, 출처 확인 결과 |
HomeSummary는 앱 화면만을 위한 값이 아니다. parkingOverview로 기존 주차 화면이 쓸 수 있고, partialOutingOverview로 일반 화면이 compact data를 재사용할 수 있다.
flowchart TB HomeSummary --> ParkingOverview["ParkingOverview 값"] HomeSummary --> OutingOverview["일부 OutingOverview 값"] HomeSummary --> WidgetParking["주차 위젯 스냅샷"] HomeSummary --> WidgetGeneral["일반 위젯 스냅샷"]
여기서는 서버가 묶어 준 값을 클라이언트가 화면별로 나눠 쓴다.
이렇게 나누면 중복 호출이 줄었다. 같은 응답에서 주차 overview, 일반 공원 summary, 위젯 snapshot이 출발하므로, 화면 전환 때 사용자가 본 값이 갑자기 다른 시점의 값으로 바뀌는 일을 줄일 수 있다.
이미 설치된 앱도 새 응답을 읽어야 한다
iOS 앱은 서버보다 느리게 업데이트된다. 이미 설치된 앱이 새 서버 payload를 읽는 기간이 반드시 생긴다. 그래서 home-summary에서는 새 값보다 오래된 앱도 읽을 수 있게 두는 일을 먼저 봤다.
지켜야 할 것은 다음이다.
- 기존 required field를 쉽게 제거하지 않는다.
- 새 값은 optional field 또는 nested object 추가로 시작한다.
- enum은 알 수 없는 값을 만났을 때의 동작을 갖는다.
- 날짜와 최신성 관련 field는 의미를 바꾸지 않는다.
- park ID가 요청한 값과 다르면 payload를 믿지 않는다.
위젯 로더도 이 방식을 따른다. 네트워크에서 home-summary를 받아도 공원 ID가 요청과 다르면 빈 데이터로 다룬다. 이런 작은 guard가 캐시 오염을 막는다.
호환성은 지루한 주제처럼 보이지만, 출시된 iOS 앱에서는 매우 현실적인 문제다. 서버는 이미 새 필드를 내려주는데 사용자는 오래된 앱을 쓰고 있을 수 있다. 이 기간을 견디지 못하는 API는 값이 늘수록 불안해진다.
iOS는 첫 화면 응답을 캐시에 나눠 저장한다
iOS는 받은 HomeSummary를 SwiftData에 저장한다. 단순히 payload 하나만 저장하는 것이 아니라, 내부의 공원, 주차장, status, forecast overview도 각 cache에 반영한다.
sequenceDiagram
autonumber
participant Store as 앱 상태 스토어
participant Repo as 주차 저장소
participant API as 한강자리 API
participant Cache as SwiftData 캐시
Store->>Repo: 저장된 첫 화면 값 확인
alt 캐시 최신
Repo-->>Store: 캐시된 첫 화면 값 반환
else 오래됐거나 없음
Repo->>API: 첫 화면 응답 요청
API-->>Repo: 첫 화면 응답 반환
Repo->>Cache: 첫 화면 값 저장
Repo->>Cache: 공원/주차/상태/예측 캐시 갱신
Repo-->>Store: 새 첫 화면 값 반환
end
이 덕분에 첫 화면과 상세 화면이 같은 응답에서 출발한다. 사용자가 주차 상세로 들어가도 방금 받은 값을 다시 버리지 않는다.
서버 cache도 오래된 값을 숨기지 않는다
서버는 home-summary에 hot cache와 stale backup cache를 둔다. 정상적으로 다시 만들면 hot과 stale cache를 같이 갱신한다. 다시 만들기에 실패하면 stale backup을 반환할 수 있다.
HTTP 응답에는 private cache header와 ETag도 붙는다. 이것은 공개 캐시로 오래 두겠다는 의미가 아니라, 앱 인스턴스가 짧은 시간 안에 같은 첫 화면 값을 반복 요청할 때 낭비를 줄이기 위한 처리다.
여기서도 숨기면 안 되는 것은 오래됐다는 사실이다. stale backup을 쓰더라도 payload 안의 갱신 상태와 출처 확인 결과는 그대로 유지되어야 한다.
첫 몇 초를 같은 값으로 맞췄다
첫 화면은 여러 endpoint를 조합하기보다 한 응답으로 받는 편이 단순했다. 서버가 묶어 준 첫 화면 값은 앱 화면, 위젯, cache가 함께 쓰는 출발점이 됐다.
iOS decoding 안정성은 “새 필드 추가”보다 “기존 의미 유지”에 달려 있었다. 요청한 공원과 응답 공원 ID를 검증하면 cache 오염을 줄일 수 있고, stale cache는 장애 숨김이 아니라 마지막으로 확인한 값을 보여주기 위한 선택으로 남길 수 있었다.
home-summary를 만들면서 얻은 가장 큰 효과는 호출 수 감소만이 아니었다. 앱, 위젯, cache가 같은 첫 값을 보게 된 것이 더 컸다. 사용자가 보는 첫 몇 초를 안정시키려면 서버와 클라이언트가 같은 값을 말해야
한다.