강프로의 실험실

직접 만들고 운영하면서 남기는 개발 기록

APNs는 200 OK인데 LiveActivity가 안 뜬다 — push-to-start 페이로드 함정 세 가지

·

APNs가 HTTP/2 200 OK를 준다. 기기의 apsd도 push를 받는다. 그런데 Live Activity가 기동되지 않는다. 서버 로그만 보면 성공이라 어디를 봐야 할지도 모른다.

push-to-start에서 같은 증상을 두 번 겪었고, 원인은 서로 달랐다. 둘 다 페이로드 구조 문제였다.

먼저: APNs 200은 아무것도 보장하지 않는다

200 OK는 “APNs가 페이로드를 접수했다”는 뜻이지 “기기가 이걸 처리했다”는 뜻이 아니다. 페이로드를 실제로 해석하는 건 기기의 liveactivitiesd 데몬이고, 얘가 거부하면 그 사실은 기기 로그에만 남는다.

그래서 push-to-start를 디버깅할 때는 Xcode Console에서 liveactivitiesd 프로세스 로그를 반드시 같이 본다. 거부됐다면 이런 줄이 뜬다.

Incoming message does not contain usable attributes
Incoming message could not be parsed

이 두 줄이 이 글의 나머지 전부에 대한 공통 증상이다. 메시지가 똑같아서 원인 구분이 안 된다는 게 함정이다.

함정 ①: attributesaps 밖에 뒀다

attributesattributes-type 키를 페이로드 최상위에 두고 있었다. 어디선가 그렇게 본 것 같아서 그렇게 했던 건데, ActivityKit 규격상 이 키들은 반드시 aps 딕셔너리 안에 중첩돼야 한다.

// ❌ 거부됨
{
  "aps": { "timestamp": ..., "event": "start", "content-state": {...} },
  "attributes-type": "IVENTActivityAttributes",
  "attributes": {...}
}

// ✅ 정상
{
  "aps": {
    "timestamp": ..., "event": "start",
    "content-state": {...},
    "attributes-type": "IVENTActivityAttributes",
    "attributes": {...}
  }
}

함정 ②: attributes에 Swift 타입에 없는 키가 있었다

위치를 고쳤는데도 같은 로그가 났다. 이번 원인이 더 고약하다.

알림 제목과 본문을 함께 실어 보내려고 attributes dict에 alert_title, alert_body를 끼워 넣고 있었다. Swift Codable은 모르는 키를 그냥 무시하니까 안전하다고 생각했다.

liveactivitiesd의 내부 디코더는 표준 Swift Codable과 다르게 동작한다. 알 수 없는 키가 하나라도 있으면 메시지 전체를 거부한다.

그래서 규칙은 이렇게 된다.

  • attributes dict에는 ActivityAttributes 구조체의 stored property만 넣는다. 하나도 더도 말고.
  • 알림 문구는 aps.alert가 원래 자리다. 거기로 옮긴다.

고칠 때 주의할 점 하나. 우리 코드베이스에서 push-to-start 페이로드를 만드는 자리가 네 군데였다 — 실제 발송 경로, 발송 실패 재시도 큐, 어드민 테스트 콘솔, 그리고 APNs 클라이언트 자체. 한 군데만 고치면 나머지에서 같은 증상이 계속 난다. 페이로드 조립을 한 함수로 모으고 파라미터로 분리하는 게 맞다.

함정 ③: 리브랜딩하면서 타입 이름 대소문자가 어긋났다

같은 계열의 세 번째 사고. 앱 이름을 바꾸면서 attributes-type 문자열의 대소문자가 Swift 타입명과 어긋났다. 이것도 증상은 똑같이 “200 OK인데 안 뜸”이다. attributes-typeSwift 타입 이름과 정확히 일치하는 문자열이어야 한다.

체크리스트

push-to-start가 200인데 안 뜬다면, 이 순서로 본다.

  1. 기기 로그에서 liveactivitiesd를 필터링한다. 서버 로그는 여기서 쓸모가 없다.
  2. attributes / attributes-typeaps 안에 있는가.
  3. attributes-type 문자열이 Swift 타입명과 대소문자까지 같은가.
  4. attributes에 구조체에 없는 키가 섞여 있지 않은가. 특히 나중에 “편의상” 얹은 필드.
  5. 페이로드를 만드는 자리가 몇 군데인지 세어본다.

세 번 다 같은 로그 두 줄을 봤다. 원인을 구분해주지 않으니 체크리스트로 훑는 수밖에 없다.


이 글에서 다룬 서비스

아이벤트

아이의 등하교와 학원 일정을 한 화면에 모아주는 iOS 앱입니다. 종이 알림장은 사진 한 장으로 정리되고, 등하원 시간이 다가오면 잠금화면에 실시간 카운트다운이 뜹니다. 완전 무료입니다.

프로젝트: