첫 화면 값을 한 응답으로 묶기: 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
forecasthorizon과 주차/공원 혼잡 예측 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: privateETag도 붙는다. 이것은 공개 캐시로 오래 두겠다는 의미가 아니라, 앱 인스턴스가 짧은 시간 안에 같은 첫 화면 값을 반복 요청할 때 304로 낭비를 줄이기 위한 처리다.

여기서도 숨기면 안 되는 것은 오래됐다는 사실이다. stale backup을 쓰더라도 payload 안의 갱신 상태와 출처 확인 결과는 그대로 유지되어야 한다.

첫 몇 초의 값 동기화

첫 화면은 여러 호출을 조합하기보다 한 응답으로 받는 편이 단순했다. 서버가 묶어 준 첫 화면 값은 앱 화면, 위젯, cache가 함께 쓰는 출발점이 됐다.

iOS decoding 안정성은 “새 필드 추가”보다 “기존 의미 유지”에 달려 있었다. 요청한 공원과 응답 공원 ID를 검증하면 cache 오염을 줄일 수 있고, stale cache는 장애 숨김이 아니라 마지막으로 확인한 값을 보여주기 위한 선택으로 남길 수 있었다.

home-summary를 적용한 뒤 앱, 위젯과 cache가 같은 첫 화면 값을 사용했고 호출 수도 줄었다.

사용자가 보는 첫 몇 초를 안정시키려면 서버와 클라이언트가 같은 값을 말해야 한다.

Comments

댓글

    이미지 확대