첫 화면 값을 한 응답으로 묶기: home-summary
첫 화면 값을 한 응답으로 묶기: home-summary. 다루는 범위: 한 번에 받는 첫 화면 값, 네 묶음으로 나눈 응답, 기존 앱의 새 응답 호환성. 실제 구현과 운영 기록을 기준으로 설명한다.
한강자리 첫 화면은 여러 종류의 데이터를 동시에 보여준다. 주차장, 현재 값, 예측, 혼잡도, 행사, 시설 요약, 길찾기 후보, 출처 확인 결과가 한 화면에 들어온다.
처음에는 클라이언트가 여러 응답을 호출해서 조합해도 된다고 생각하기 쉽다. 하지만 화면을 실제로 만들면 금방 어긋남이 생긴다.
주차 섹션은 방금 갱신됐는데 행사 섹션은 오래된 값일 수 있다. 예측은 다른 horizon을 보고 있고, 위젯은 일부 값만 받아 전혀 다른 인상을 줄 수도 있다.
그래서 한강자리에서는 첫 화면에 필요한 값을 서버가 한 번에 묶어 준다. 이 결정은 클라이언트 코드를 편하게 만들기 위한 것만은 아니었다.
첫 화면은 사용자가 앱을 평가하는 첫 몇 초다. 그 몇 초 안에서 값들이 서로 다른 시간을 말하면 안 됐다.
한 번에 받는 첫 화면 값
home-summary는 한강자리의 첫 화면용 응답이다. 목표는 “한 공원에 대해 앱과 위젯이 처음 볼 값을 한 번에 받는다”이다.
flowchart LR API["공원별 첫 화면 응답"] --> Park["공원"] API --> Parking["주차 집계<br/>주차장 상태"] API --> Forecast["주차 예측<br/>공원 혼잡 예측"] API --> Outing["나들이 요약<br/>신호 · 시설 그룹 · 길찾기"] API --> Freshness["갱신 상태<br/>출처 확인"]
이 첫 화면 응답이 없으면 앱은 주차장 목록, 현재 값 목록, 예측 overview, 나들이 overview, 출처 확인 결과를 따로 불러야 한다. 그러면 첫 화면에서 다음 문제가 생긴다.
- 호출별 성공/실패가 서로 어긋난다.
- 화면 일부가 이전 공원 값으로 남을 수 있다.
- 위젯은 여러 네트워크 호출을 감당하기 어렵다.
- cache와 갱신 상태 확인이 복잡해진다.
- API가 바뀔 때 iOS decoding에서 방어해야 할 범위가 넓어진다.
서버가 첫 화면 값을 묶어 주면 이 문제가 한 응답 안으로 줄어든다.
물론 home-summary가 커질수록 조심할 점도 생긴다. 모든 것을 넣는 응답이 되면 다시 느려지고 깨지기 쉽다.
그래서 이 API에 넣을지는 “첫 화면에서 바로 필요한가”로 정했다. 상세 화면에서만 필요한 긴 목록이나 원문 payload는 여기로 끌어오지 않는다.
네 묶음으로 나눈 응답
iOS에서 받는 값은 크게 네 부분으로 나뉜다.
| 필드 | 역할 |
|---|---|
park | 현재 summary가 어떤 공원을 말하는지 확인 |
parking | 주차 aggregate, 추천 주차장, lots, statuses |
forecast | horizon과 주차/공원 혼잡 예측 overview |
outing | 일반 화면 신호, 시설 그룹, 길찾기, 출처 확인 결과 |
HomeSummary는 앱 화면만을 위한 값이 아니다. 기존 주차 화면은 여기서 주차 overview를 꺼내 쓸 수 있고, 일반 화면은 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에 빠른 응답용 cache와 마지막 성공 응답 cache를 둔다. 정상적으로 다시 만들면 두 cache를 같이 갱신한다. 다시 만들기에 실패하면 마지막 성공 응답을 반환할 수 있다.
HTTP 응답에는 Cache-Control: private와 ETag도 붙는다. 이것은 공개 캐시로 오래 두겠다는 의미가 아니라, 앱 인스턴스가 짧은 시간 안에 같은 첫 화면 값을 반복 요청할 때 304로 낭비를 줄이기 위한 처리다.
여기서도 숨기면 안 되는 것은 오래됐다는 사실이다. stale backup을 쓰더라도 payload 안의 갱신 상태와 출처 확인 결과는 그대로 유지되어야 한다.
첫 몇 초의 값 동기화
첫 화면은 여러 호출을 조합하기보다 한 응답으로 받는 편이 단순했다. 서버가 묶어 준 첫 화면 값은 앱 화면, 위젯, cache가 함께 쓰는 출발점이 됐다.
iOS decoding 안정성은 “새 필드 추가”보다 “기존 의미 유지”에 달려 있었다. 요청한 공원과 응답 공원 ID를 검증하면 cache 오염을 줄일 수 있고, stale cache는 장애 숨김이 아니라 마지막으로 확인한 값을 보여주기 위한 선택으로 남길 수 있었다.
home-summary를 적용한 뒤 앱, 위젯과 cache가 같은 첫 화면 값을 사용했고 호출 수도 줄었다.
사용자가 보는 첫 몇 초를 안정시키려면 서버와 클라이언트가 같은 값을 말해야 한다.
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중