iOS VoIP Push 구성 — APNs, PushKit, CallKit
일반 APNs 알림과 PushKit VoIP push의 token·topic·payload·처리 수명주기를 구분합니다. PushKit callback에서 CallKit 통화를 보고하고 RTC 연결을 시작하는 흐름, server-side push 인증, Apple 정책상 주의사항을 공식 문서 기준으로 설명합니다. 관리형 알림 연동과 직접 구축의 책임 범위도 비교합니다.
목차(57개 항목)
- 0. 핵심 명제 — VoIP 푸시는 APNs 위의 "특수 모드"
1. APNs 기초 — 푸시는 어떻게 동작하나
2. 일반 푸시 vs VoIP 푸시 — 5가지 분리 항목
3. APNs provider 인증 — 토큰 방식과 인증서 방식
4. PushKit 등록 코드 — 한 줄씩 분해
5. 토큰 수신 콜백 — 누가 호출하나
6. 토큰 라이프사이클 — 발급/캐시/갱신
7. VoIP 푸시 페이로드 형식
8. CallKit + PushKit + Agora RTC 통합 — 단계별
9. 자체 구현 vs Agora Chat 매니지드 푸시
10. 한 줄 결론 + 구현 체크리스트
- 한 장으로 머릿속 정리
- 관련 글
- 참고 자료
"VoIP 푸시 토큰이랑 일반 APNs 토큰이 같은 거 아닌가요?", "인증서 두 개 발급받으라는데 왜요?", "통화용 푸시는 우리가 직접 서버 만들어야 해요, 아니면 Agora가 해주나요?" — RTC 도입 고객이 자주 던지는 질문 3종 세트입니다.
#35에서 CallKit + AVAudioSession + Agora ADM의 4-레이어를 다뤘다면, 이 글은 그 위에 푸시 인프라를 올리는 작업을 정리합니다. APNs 기초부터, 일반 푸시 vs VoIP 푸시의 행정적 분리, PushKit 코드 한 줄씩, 자체 구현 vs Agora Chat 매니지드 옵션까지.
0. 핵심 명제 — VoIP 푸시는 APNs 위의 "특수 모드"
PushKit은 별도 푸시 시스템이 아니다. APNs라는 같은 인프라 위에서 동작하는 특수한 모드일 뿐이고, 그 모드를 다루는 iOS 측 프레임워크가 PushKit이다.
흔한 오해와 정정:
| 오해 | 정확한 이해 |
|---|---|
| PushKit이 푸시를 보낸다 | APNs가 보낸다. PushKit은 받는 쪽 프레임워크 |
| VoIP 푸시는 별도 Apple 푸시망이다 | 같은 APNs. 토큰·topic·push type과 수신 API가 다름 |
| 일반 토큰으로 VoIP 푸시 가능 | ❌ 거부됨. 별도 토큰 필요 |
| VoIP 푸시는 반드시 별도 인증서가 필요 | 토큰 기반 .p8 키는 권한이 부여된 여러 APNs topic에 사용할 수 있음 |
이 분리가 왜 존재하는가가 이 글의 절반입니다.
1. APNs 기초 — 푸시는 어떻게 동작하나
푸시 이전 시대
옛날 앱은 자기가 켜져 있을 때만 새 정보를 가져올 수 있었습니다. 백그라운드에 들어간 앱이 메시지를 알려면 1분마다 서버에 "새거 있어?"라고 물어봐야 함 → 배터리 폭망.
푸시의 발명
해결책: 서버가 디바이스에 먼저 말 거는 시스템.
문제: 서버가 수억 대 아이폰의 IP를 어떻게 알지? 그리고 아이폰 IP는 자주 바뀌는데?
→ Apple이 중간에 우체국 역할. 이게 APNs(Apple Push Notification service).
iOS는 APNs와의 지속 연결을 시스템 수준에서 관리합니다. 앱 서버는 APNs provider API에 요청하고, APNs가 해당 앱·기기의 토큰으로 알림을 라우팅합니다.
"APNs는 서버, PushKit은 도구함" — 위치의 차이
이 둘이 같은 거라고 헷갈리는 경우가 많아서 한 번 정리.
iOS 내부 프레임워크들은 아이폰 저장공간에 파일로 깔려 있습니다 (/System/Library/Frameworks/PushKit.framework/). 앱이 import PushKit하면 그 파일에 있는 코드를 갖다 씁니다.
그래서 "PushKit은 iOS 안에 있다" =
- APNs 서버는 Apple 데이터센터에 (멀리)
- PushKit 프레임워크는 내 폰의 iOS 안에 (가까이)
- 둘이 협업해서 VoIP 푸시 한 번이 동작
PushKit은 서버가 아닙니다. Apple 클라우드에 있는 게 아닙니다. 내 아이폰 저장공간 안에 코드 파일로 존재하는 도구함입니다. CallKit, AVFoundation도 마찬가지.
Device Token — 디바이스의 "주소"
서버가 누구한테 보낼지 알려면 식별자가 필요합니다.
토큰은 특정 앱 + 특정 디바이스 + 특정 환경의 조합 식별자. 같은 앱이라도 다른 폰이면 다른 토큰, 같은 폰이라도 다른 앱이면 다른 토큰.
2. 일반 푸시 vs VoIP 푸시 — 5가지 분리 항목
일반 APNs 푸시의 한계
메시징 앱이 백그라운드일 때 메시지 도착:
사용자가 직접 탭해야 앱이 깨어남. iOS가 배터리 보호 차원에서 강제하는 동작.
통화는 그러면 안 됨
전화는 즉시 통화 화면이 떠야 함. "탭하세요" 알림 보고 누르고 있을 시간이 없음 — 발신자는 지금 벨을 듣고 있음.
Apple의 해결책: PushKit (= VoIP 푸시)
일반 푸시는 알림만 띄우고 끝. VoIP 푸시는 앱 코드를 즉시 실행시킴. 차원이 다른 권한.
분리 항목 5종 매트릭스
APNs는 두 흐름을 token·topic·push type과 수신 API로 구분합니다.
| 항목 | 일반 푸시 | VoIP 푸시 |
|---|---|---|
| 처리 목적 | 사용자 알림·백그라운드 갱신 | 수신 VoIP 통화 보고 |
| provider 인증 | .p8 토큰 또는 TLS 인증서 | .p8 토큰 또는 VoIP topic 권한이 있는 TLS 인증서 |
| 토큰 | APNs device token | VoIP push token (별도) |
| Topic 헤더 | com.example.app | com.example.app.voip (.voip 접미사) |
| pushType 헤더 | alert 또는 background | voip |
| 도착 시 처리 | iOS가 알림 자동 표시 | 앱 코드 즉시 깨움 → PushKit 콜백 |
| 규정 위반 시 | APNs 정책과 백그라운드 실행 제한 적용 | 미보고 VoIP push는 앱 종료·향후 전달 중단 가능 |
이 5개 중 하나라도 어긋나면 푸시 거부됩니다. 서버 인프라를 짤 때 두 채널을 명확히 분리해야 함.
같은 앱이 두 토큰을 동시에 가진다
당연히 가능. 보통의 메시징/통화 앱이 그렇습니다.
발급 순서는 무관. 앱 시작 시 둘 다 등록하면 각자 별도 콜백으로 발급됨.
3. APNs provider 인증 — 토큰 방식과 인증서 방식
인증서가 왜 필요한가
서버가 APNs에 푸시를 보낼 때 APNs는 "너 진짜 이 앱 만든 사람 맞아?"를 검증해야 함. 누구나 보낼 수 있으면 스팸 천국.
→ Apple Developer 콘솔에서 인증서 발급 → 서버는 그 인증서로 서명 → APNs가 검증 → 통과 시 발송.
두 가지 인증 방식
| 방식 | 자격 증명 | 범위 |
|---|---|---|
| 토큰 기반 | APNs signing key (.p8) + JWT | 키에 허용된 팀의 여러 topic에 사용 가능 |
| 인증서 기반 | 앱/topic별 TLS 인증서와 개인키 (.p12로 내보내기도 함) | 인증서 extension에 표시된 topic으로 제한 |
VoIP 여부는 자격 증명 파일 확장자가 아니라 device token, apns-topic의 .voip 접미사, apns-push-type: voip, 그리고 자격 증명에 허용된 topic의 조합으로 결정됩니다.
Apple이 분리한 이유
VoIP 푸시는 앱을 백그라운드에서 강제 실행시키는 강력한 권한. 일반 푸시와 섞어두면:
- 앱들이 모든 알림을 VoIP로 보내서 배터리 학살
- 통화 아닌 용도(메시지, 광고)에 남용
→ APNs는 topic과 push type을 구분하고, 앱은 VoIP push를 실제 수신 통화 보고에만 사용해야 합니다.
서버 측 발송 차이 (Node.js apn 라이브러리 예시)
일반 APNs 푸시:
VoIP 푸시:
| 항목 | 일반 푸시 | VoIP 푸시 |
|---|---|---|
| Topic | com.example.app | com.example.app.voip |
| pushType 헤더 | alert / background | voip |
| provider 인증 | .p8 키 또는 topic 허용 인증서 | .p8 키 또는 VoIP topic 허용 인증서 |
| 토큰 | APNs device token | VoIP push token |
| 도착 처리 | iOS가 알림 표시 | 앱 코드 깨움 → PushKit 콜백 |
.p12 vs .p8 — 두 가지 인증 자격증명
Apple은 푸시 인증 자격증명을 두 형식으로 제공합니다.
Option A: .p12 인증서 (전통 방식)
Option B: .p8 인증키 (신형, 권장)
.p8 텍스트로 열면:
비교
| 항목 | .p12 (VoIP Services Certificate) | .p8 (Auth Key) |
|---|---|---|
| 만료 | 1년마다 갱신 | 만료 없음 |
| 인증 방식 | TLS 클라이언트 인증서 | JWT 토큰 (HTTP/2) |
| 적용 범위 | 인증서 extension의 topic | 발급 시 선택한 팀의 앱 또는 제한된 topic |
| dev/prod 분리 | 인증서 종류에 따라 확인 | 같은 키로 development/production APNs 연결에 사용 가능 |
| VoIP 푸시 | ✅ | ✅ (apns-push-type: voip 헤더로 구분) |
직접 APNs provider를 운영한다면 .p8은 인증서 만료 갱신을 줄여 줍니다. 다만 키가 유출되면 여러 topic에 영향이 갈 수 있으므로 Key ID·Team ID와 함께 비밀 저장소에서 관리하고, JWT의 발급 시각을 갱신해야 합니다. 제3자 서비스는 업로드 형식을 별도로 제한할 수 있으므로 해당 서비스의 현재 공식 문서를 확인합니다.
.p8 발급 절차
중요: 키는 Apple 계정에 저장되지 않으므로 안전한 곳에 백업. 다시 다운로드 불가. Download 버튼이 비활성화되어 있다면 이미 다운받았다는 의미.
JWT 서명 시 함께 필요한 정보:
| 항목 | 어디서 확인 |
|---|---|
| Key ID (10자리) | Keys 페이지에서 키 클릭 시 표시 |
| Team ID (10자리) | Apple Developer 계정 우측 상단 |
| Bundle ID | 앱 프로젝트 설정 |
4. PushKit 등록 코드 — 한 줄씩 분해
등록 함수
PKPushRegistry는 지역 변수로 버리지 말고 앱이 실행되는 동안 강한 참조로 유지합니다. Apple의 예제도 delegate를 지정한 registry를 장기 보관합니다.
PKPushRegistry(queue: .main)
PushKit 시스템과 대화하는 창구 객체. "내가 VoIP 푸시 받을 거야"라고 등록하는 사무소.
queue: .main → 푸시 도착 알림을 메인 스레드(UI 스레드)에서 받음. CallKit 띄우기는 UI 작업이라 메인 스레드가 자연스러움.
pushRegistry.delegate = self
여기가 senior가 놓치기 쉬운 델리게이트 패턴의 핵심.
문제: PKPushRegistry는 시스템 객체. 토큰이 발급되거나 푸시가 도착하면 누구에게 알릴지 모름.
해결: "이런 일 생기면 이 객체한테 말 걸어"라고 미리 정해둠 = delegate.
PKPushRegistryDelegate 프로토콜을 채택하고 정해진 함수들을 구현하면 iOS가 알아서 호출.
pushRegistry.desiredPushTypes = Set([.voIP])
PushKit은 원래 여러 푸시 타입을 처리하도록 설계됐으나(과거 워치 컴플리케이션 등), 현재 사실상 .voIP만 의미 있음.
desired type을 설정하면 시스템이 등록을 처리하고, 자격 증명이 갱신될 때 delegate 콜백을 호출합니다.
5. 토큰 수신 콜백 — 누가 호출하나
이 함수는 iOS가 호출함
내가 호출하는 게 아닙니다. desiredPushTypes = [.voIP] 설정 후 iOS가 알아서:
- APNs에 VoIP 토큰 요청
- 토큰 수신
- 등록된 delegate의 이 함수를 자동 호출
콜백 패턴 — "토큰 발급되는 시점은 모르겠지만, 발급되면 이 함수 실행시켜줘"라고 미리 함수를 만들어두는 것.
함수 이름의 Swift 컨벤션
자연어로 읽으면: "pushRegistry가 (특정 type을 위한) credentials를 업데이트했다"
파라미터 의미
| 파라미터 | 의미 |
|---|---|
registry: PKPushRegistry | 어떤 registry에서 발생 (보통 한 개라 무시) |
pushCredentials: PKPushCredentials | 토큰 정보 묶음 |
type: PKPushType | 어떤 타입의 푸시 토큰인지 (.voIP) |
pushCredentials.token이란
VoIP 푸시 디바이스 토큰 = 디바이스 주소.
타입은 Data (바이트 묶음). 사람이 읽으라고 만든 게 아니라 컴퓨터가 식별자로 쓰라고 만든 것이라 그냥 바이트 덩어리.
[UInt8](pushCredentials.token)은 Data를 UInt8 배열(0~255 부호 없는 정수 배열)로 변환. 보통은:
Data→ 16진수 문자열 ("4a9cff...")- 그 문자열을 HTTP POST 본문에 담아 서버 전송
서버는 받은 토큰을 (사용자, 토큰) 페어로 DB에 저장.
6. 토큰 라이프사이클 — 발급/캐시/갱신
토큰은 언제 발급되나
짧은 답: registry가 등록된 뒤 시스템이 현재 자격 증명을 delegate로 전달하며, 토큰 값은 바뀔 수 있다고 가정해야 합니다.
긴 답:
didUpdate에서 현재 값을 서버에 반영
콜백이 오면 현재 토큰을 서버에 upsert합니다. Apple은 토큰을 앱에 캐시해 재사용하지 말고 시스템이 전달한 값을 사용하라고 안내합니다.
멀티 디바이스 토큰 관리
한 사용자가 아이폰 + 아이패드 + 안드로이드를 동시에 쓰는 경우가 흔합니다. 그래서 보통은 (user_id, device_id) 조합으로 관리:
설계 포인트:
- 한 디바이스가 APNs 토큰 + VoIP 토큰을 둘 다 가질 수 있음 → token_type으로 구분
- vita에게 전화 걸 때 → vita의 모든 device 행을 조회 → 각 디바이스에 푸시 발송
- 어느 기기에서든 받으면 통화 시작, 다른 기기에서는 "다른 곳에서 받음" 처리
- 여러 기기에서 수신할 수 있는 제품은 동일한 중복 수신·응답 정리 정책이 필요
토큰 무효화 콜백
이 콜백 받으면 서버 DB에서 그 토큰을 지워야 함. 구토큰으로 푸시 보내봐야 헛수고.
7. VoIP 푸시 페이로드 형식
일반 푸시 페이로드
aps 안에 시스템이 표시할 정보를 넣음 → iOS가 자동으로 알림 배너 표시.
VoIP 푸시 페이로드
특징:
aps는 거의 비어있어도 됨 — iOS가 알림을 자동 표시 안 하니까- 개발자가 정한 키를 추가할 수 있지만 민감한 RTC 토큰이나 개인정보는 싣지 않음
- 이 페이로드 전체가
didReceiveIncomingPushWith콜백의payload.dictionaryPayload로 들어옴
콜백에서 받기 + CallKit 띄우기
iOS 13+ 결정적 룰: VoIP push를 받으면
reportNewIncomingCall로 통화를 보고해야 합니다. 보고하지 않으면 시스템이 앱을 종료할 수 있고, 반복하면 향후 VoIP push 전달을 중단할 수 있습니다.
8. CallKit + PushKit + Agora RTC 통합 — 단계별
여기서 #35의 4-레이어가 실제 코드로 만나는 모습.
8-1. CallKit Provider 설정
8-2. 사용자가 "받기" 눌렀을 때 — 정확한 순서
8-3. CallKit 컨텍스트에서의 Restriction 패턴
setAudioSessionOperationRestriction은 SDK의 AVAudioSession 조작을 제한하는 공개 API지만, CallKit을 쓴다고 항상 .all이 정답인 것은 아닙니다.
| 책임 모델 | 선택 기준 |
|---|---|
| SDK가 세션 구성 | 기본 제한을 유지하고 CallKit 활성화 콜백과 SDK 오디오 API를 연결 |
| 앱이 category/mode/options 구성 | 현재 SDK가 지원하면 .configureSession처럼 필요한 조작만 제한 |
| 앱이 전체 수명 주기 관리 | .all을 쓰는 대신 활성화·비활성화·route 변경을 앱이 모두 구현 |
이유:
제한 범위가 넓을수록 SDK 대신 앱이 책임질 코드가 늘어납니다. 사용하는 Agora SDK 버전의 API 레퍼런스와 CallKit 샘플을 기준으로 가장 좁은 제한을 선택하고, 통화 시작·종료·인터럽션·블루투스 route를 회귀 테스트합니다.
8-4. 발신 흐름 — 세 단계 라이프사이클
발신은 수신과 다르게 세 가지 시점을 CallKit에 명시적으로 보고해야 통화 시간 카운트가 정확해집니다.
① 발신 시작 요청:
② 델리게이트 콜백 — 실제 발신 작업:
③ 상대방이 받았을 때:
보고 누락 시 영향:
| 누락 | 결과 |
|---|---|
startedConnectingAt 미보고 | CallKit UI가 "연결 시도 중" 표시 안 됨 |
connectedAt 미보고 | 통화 시간 카운트 시작 안 됨, 통화 기록의 duration이 0 |
CXEndCallAction 미보고 | 통화가 끝나도 시스템 UI가 잠금 화면에 남아있음 |
8-5. 그 외 주요 콜백
| 콜백 | 처리 |
|---|---|
CXSetMutedCallAction | agoraKit.muteLocalAudioStream(action.isMuted) |
CXSetHeldCallAction | 통화 보류(hold) |
CXAction.timedOut | 위 액션이 시간 내 fulfill 안 되면 호출 → 정리/롤백 |
CallKit 액션은 제한 시간 안에 fulfill() 또는 fail()로 끝내야 합니다. 네트워크 작업을 무한정 기다리지 말고 앱 상태를 정리할 수 있는 실패 경로를 둡니다.
9. 자체 구현 vs Agora Chat 매니지드 푸시
자체 구현 시 해야 할 일들
- provider 자격 증명 관리 —
.p8signing key 또는 TLS 인증서, topic 권한과 교체 정책 - 푸시 발송 서버 — APNs HTTP/2 연결 풀, JWT 토큰 갱신, 재시도, 큐
- 토큰 라이프사이클 — 디바이스별 토큰 DB, 환경별 분리(dev/prod), 갱신 추적
- Android FCM — 멀티플랫폼이면 별도 인프라
- 운영 모니터링 — 발송 성공률, 인증서 만료 알람
Agora Chat의 매니지드 푸시
현재 공개된 Agora Chat iOS 오프라인 푸시 문서는 일반 APNs device token 등록과 메시지 알림 흐름을 설명합니다.
API명 주의: 흔히
registerForRemoteNotifications로 잘못 알려져 있는데, 그건 Apple UIKit 메서드. Agora Chat의 정확한 API는bindDeviceToken:.
Agora Chat 매니지드 푸시의 실제 범위
| 푸시 종류 | Agora Chat이 매니지드? |
|---|---|
| 메시지 알림 (일반 APNs) | ✅ 콘솔에 APNs 자격 증명 등록 → SDK token binding |
| VoIP 푸시 (콜 초대) | 공개 iOS 오프라인 푸시 가이드에서 PushKit 통화 흐름을 제공하지 않음 |
| Signaling/RTM 콜 초대 VoIP 푸시 | 제품 문서에 별도 기능이 없다면 앱 백엔드에서 APNs VoIP provider 구현 필요 |
의사결정 매트릭스
| 측면 | 자체 구현 | Agora Chat 매니지드 (메시지) |
|---|---|---|
| 초기 개발 시간 | APNs provider·토큰 DB·CallKit 구현 필요 | SDK/콘솔 통합 중심 |
| 백엔드 인프라 | 필요 | 불필요 |
| 인증서 관리 | 직접 관리 | 콘솔 업로드 1회 |
| iOS/Android 통합 | 따로 구현 | 단일 API |
| 발송 성공률 모니터링 | 직접 구축 | 대시보드 제공 |
| 운영 비용 | 인프라 + 인력 | Agora 사용료 |
| 커스터마이징 자유도 | provider 정책을 직접 설계 | 제품이 제공하는 설정 범위 |
| VoIP 푸시 처리 | 포함 | 별도 인프라 필요 |
SA 입장의 정리
고객이 "VoIP 푸시 어떻게 해요?" 물어보면 두 가지를 깔끔히 분리해서 안내:
- 메시지 알림 푸시: Agora Chat 통합 시 매니지드로 해결 가능
- VoIP 통화 푸시: 현재 사용 중인 Agora 제품의 공식 문서에 PushKit provider 기능이 없다면 자체 APNs provider + PushKit + CallKit 통합 필요
이 두 트랙을 합쳐 풀스택 통화 앱을 만들 때, 보통:
- 메시지/채팅: Agora Chat (매니지드 푸시 포함)
- 통화 시그널링: Agora Signaling/RTM
- 통화 미디어: Agora RTC
- 통화 VoIP 푸시 인프라: 자체 구축 (PushKit + 백엔드 + 인증서)
이 4가지를 묶어 설계해야 합니다.
10. 한 줄 결론 + 구현 체크리스트
PushKit은 APNs VoIP push를 받는 iOS 프레임워크다. 일반 알림과 device token·topic·push type·수신 API가 다르지만, provider 인증은
.p8키 또는 허용된 TLS 인증서를 사용할 수 있다.
PushKit 등록
VoIP 푸시 수신
인증서 / 서버
CallKit 통합
엣지 케이스
한 장으로 머릿속 정리
관련 글
- iOS VoIP 통합의 4-레이어 모델 — CallKit · PushKit · AVAudioSession · Agora ADM — 본 글의 전편
- WebRTC란? ICE? STUN? NAT? TURN? — 시그널링/미디어 분리
- 시그널링과 미디어의 분리 — RTSP/SIP vs RTP — VoIP 평면 분리 일반론
- 오디오 파이프라인 해부 — ADM 내부 흐름
- AI 콜봇 멀티콜 — SIP B2BUA vs Bridge — 서버 측 콜 시그널링/푸시 트리거가 맞물리는 백엔드 관점
참고 자료
- Apple — PushKit Framework —
PKPushRegistry/PKPushRegistryDelegate/PKPushPayload공식 레퍼런스 - Apple — Responding to VoIP Notifications from PushKit — iOS 13+에서
didReceiveIncomingPushWith안에서reportNewIncomingCall을 호출해야 하는 규칙 - Apple — Sending Notification Requests to APNs —
apns-topic(.voip접미사),apns-push-type등 HTTP/2 헤더 명세 - Apple — Establishing a Token-Based Connection to APNs —
.p8인증키 + JWT(Key ID/Team ID) 기반 인증 방식 - Apple — CallKit Framework —
CXProvider/CXProviderDelegate/reportNewIncomingCall/reportOutgoingCall - Apple — Establishing a connection to APNs — 인증서 topic extension과 provider 연결 방식
- Agora — Integrate and test offline push on iOS — Agora Chat의 APNs 자격 증명 등록·
bindDeviceToken흐름