스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
푸시 알림으로 실시간 현황 업데이트하기
Apple Push Notification service(APNs)를 통해 앱의 실시간 현황을 원격으로 업데이트하는 방법을 알아보세요. 첫 실시간 현황 푸시를 로컬로 구성하여 구현을 빠르게 반복하는 법을 알려 드립니다. 푸시 우선순위 지정 및 경고 업데이트 구성의 모범 사례를 살펴보고, 관련성 점수와 만료 날짜를 사용해 실시간 현황을 더욱 개선해 보세요. 이 세션을 최대한 활용하려면 ActivityKit 및 실시간 현황에 대한 이해가 필요합니다. 실시간 현황의 기초 내용은 'ActivityKit 알아보기' 세션에서 확인하세요.
챕터
- 0:00 - Intro
- 2:10 - Preparations
- 5:58 - First push update
- 11:04 - Priority and alerts
- 15:40 - Enhancements
- 17:27 - Wrap-up
리소스
- ActivityKit
- Establishing a token-based connection to APNs
- Human Interface Guidelines: Live Activities
- Sending notification requests to APNs
- Sending push notifications using command-line tools
- Starting and updating Live Activities with ActivityKit push notifications
관련 비디오
WWDC24
WWDC23
-
다운로드
♪ ♪
안녕하세요, 실시간 현황 팀의 엔지니어 제프입니다 푸시 알림으로 실시간 현황을 업데이트하는 법을 알려 드리게 되어 기쁩니다 실시간 현황은 진행 중인 활동의 정보를 간편히 제공하는 훌륭한 수단입니다 ActivityKit를 통해 앱이 실시간 현황을 시작하고 업데이트하고 종료하게 할 수 있죠 또한 WidgetKit와 SwiftUI로 사용자에게 정보를 표시하는 UI를 빌드할 수 있습니다
이러한 기술을 더 알고 싶으시다면 잔의 'ActivityKit 알아보기' 세션을 확인하세요 'ActivityKit 알아보기' 세션에서 잔이 Emoji Rangers에 새로운 실시간 현황을 추가하여 영웅의 모험 상태를 표시했죠 그런데 영웅한테 동료가 있으면 더 재미있지 않을까요? 새로운 기능을 추가해서 여러 사용자의 영웅이 파티를 만들고 함께 모험을 떠나게 해 볼게요 최상의 사용자 경험을 위해 실시간 현황을 업데이트하여 파티 내 모든 영웅에 대한 이벤트를 표시하겠습니다
그러려면 기기에서 진행하는 게 아니라 모험을 추적하는 서버를 도입해야 합니다 서버는 실시간 현황을 최신 상태로 유지하는 역할을 합니다 서버에서 계산이 이뤄지므로 실시간 현황 업데이트 시 앱의 포그라운드 런타임이 필요 없죠 그 덕분에 사용자의 배터리 수명은 영향을 덜 받게 됩니다 ActivityKit 푸시 알림을 통한 실시간 현황 업데이트는 이 기능을 구현하기에 훌륭한 방법 같군요 이 세션에서는 푸시 알림으로 실시간 현황을 업데이트할 때 필요한 준비 작업을 알아봅니다 컴퓨터에서 첫 푸시 업데이트를 보내는 방법도 알려 드리죠 그다음 업데이트 우선순위와 사용자에게 경고하는 방식의 차이점을 알아보겠습니다 마지막으로 푸시 업데이트를 향상할 수 있는 추가 개선 사항을 살펴보겠습니다 준비 작업부터 시작해 보죠 푸시 업데이트로 실시간 현황 업데이트를 시작하기 전에 앱 및 서버가 Apple Push Notification service와 상호 작용하는 방식을 이해하는 게 좋습니다 앱에서 시작합니다 새로운 실시간 현황이 시작되면 ActivityKit가 푸시 토큰을 얻죠 Apple Push Notification service 줄여서 APNs에서요 요청된 실시간 현황마다 고유한 푸시 토큰이 주어집니다 그래서 앱이 푸시 토큰을 서버에 전송해야만 푸시 업데이트를 시작할 수 있죠 실시간 현황 업데이트가 필요할 때마다 서버가 해당 토큰을 사용하여 APNs에 푸시 요청을 보냅니다 마지막으로 APNs가 기기로 페이로드를 전송하고 위젯 확장을 실행해 UI를 렌더링합니다
이러한 새 기능을 지원하기 위해 APNs가 새로운 liveactivity 푸시 유형을 도입했습니다 이 푸시 유형은 토큰 기반으로 APNs에 연결된 서버에만 사용할 수 있죠 푸시 요청을 보내는 방법에 대해서는 'APNs에 알림 요청 보내기' 문서를 참조하세요 토큰 기반 연결에 대해서는 'APNs에 토큰 기반 연결 설정하기'를 참조하세요 다음은 앱을 수정하여 실시간 현황이 푸시 업데이트를 받게 구성하는 작업입니다 Xcode에서 앱 타깃을 선택하고 '서명 및 기능' 탭에서 푸시 알림 기능을 추가하세요 그러면 ActivityKit가 앱 대신 푸시 토큰을 요청할 수 있습니다 이제 코드를 살펴보죠 다음은 Emoji Rangers에서 실시간 현황을 요청하는 코드 조각입니다 Activity request 메서드에 모험의 속성과 초기 콘텐츠 상태를 전달합니다 푸시 업데이트 수신을 지원하기 위해 pushType 매개변수를 메서드에 추가하고 값을 'token'으로 지정합니다 그러면 실시간 현황이 생성될 때 ActivityKit가 푸시 토큰을 요청하죠 활동이 생성된 후에는 앱이 푸시 토큰을 서버에 전송해야 합니다 Activity 유형에는 pushToken 프로퍼티가 있어서 푸시 토큰에 동시에 접근할 수 있죠 하지만 활동이 생성되자마자 접근하지는 마세요 대부분의 경우 값으로 nil을 얻거든요 푸시 토큰 요청이 비동기 프로세스이기 때문이죠 또한 활동의 수명 전체에 걸쳐 시스템이 푸시 토큰을 업데이트할 수도 있습니다 따라서 앱에서 이를 적절히 처리해야 하죠 푸시 토큰을 올바르게 처리하려면 먼저 비동기 Task를 생성해야 합니다 그다음 활동의 pushTokenUpdates 비동기 시퀀스에서 값을 관찰하는 for-await 루프를 시작합니다 for 루프 내부 코드는 실시간 현황에 대한 새 푸시 토큰이 있는 경우에 실행됩니다 여기서 비동기 for 루프를 써야만 첫 푸시 토큰뿐만 아니라 차후 푸시 토큰 업데이트도 처리할 수 있죠 토큰을 받으면 16진수 문자열로 변환하고 디버그 콘솔에 로깅합니다 다음 섹션의 테스트에서 유용하게 쓰일 거예요 마지막으로 앱에 필요한 다른 데이터와 함께 푸시 토큰을 서버에 보냅니다 활동마다 고유한 푸시 토큰이 주어지므로 사용자가 시작하는 실시간 현황의 토큰을 추적하는 게 중요합니다 또한 시스템이 기존 활동에 대해 새 푸시 토큰을 요청하면 앱은 이를 처리하기 위해 포그라운드 런타임을 얻죠 새 푸시 토큰을 서버에 보내고 이전 토큰을 무효화해야 후속 푸시 업데이트를 올바르게 전송할 수 있습니다 준비 작업을 마쳤으니 첫 푸시 업데이트를 보내 보죠 푸시 업데이트를 보내려면 APNs에 HTTP 요청을 보내야 합니다 요청은 두 부분으로 구성됩니다 APNs 헤더와 APNs 페이로드죠 일반 HTTP 헤더 외에 세 가지 헤더를 추가로 제공해야 하는데요 첫 번째는 apns-push-type입니다 값은 liveactivity죠 다음은 apns-topic이며 앱의 번들 ID 뒤에 .push-type.liveactivity를 붙인 값입니다 세 번째는 apns-priority로 값은 5 또는 10입니다 5는 푸시 요청의 우선순위가 낮다는 뜻이며 10은 우선순위가 높다는 뜻이죠 테스트에서 높은 우선순위를 사용하는 이유는 실시간 현황을 바로 업데이트하기 때문입니다 첫 번째 APNs 페이로드로 세 필드로 구성된 페이로드를 보냅니다 첫 번째는 'timestamp'입니다 1970년 이후의 초 단위 시간 간격이죠 시스템은 최신 콘텐츠 상태를 항상 렌더링하기 위해 타임스탬프를 사용합니다 두 번째는 'event'입니다 실시간 현황에서 수행하려는 동작이죠 값은 'update'나 'end'입니다 최초 APNs 요청에서는 update로 설정해야 합니다 세 번째 필드는 'content-state'인데요 JSON 객체로서 활동의 콘텐츠 상태 유형으로 디코딩될 수 있습니다 콘텐츠 상태를 올바른 형식으로 가져오기 위해서는 앱 내부에서 Foundation의 JSONEncoder 유형을 사용합니다 실시간 현황의 ContentState 인스턴스를 생성합니다 그다음 JSONEncoder를 인스턴스화합니다 마지막으로 콘텐츠 상태를 JSON 데이터로 인코딩하고 문자열 표현을 콘솔에 로깅합니다 예상과 똑같은 JSON 출력이 나왔군요 콘텐츠 상태 JSON은 항상 기본 디코딩 전략을 쓰는 JSONDecoder로 디코딩됩니다 그러니 콘텐츠 상태 인코딩 시 사용자 지정 인코딩 전략을 설정하지 마세요 그렇지 않으면 JSON이 불일치하여 시스템이 실시간 현황을 업데이트할 수 없습니다 푸시 요청이 어떤 내용을 포함하는지 알았으니 다음은 전송을 테스트할 차례입니다 저는 개발 중에 빠르게 반복하는 걸 좋아하는데요 실시간 현황 푸시 알림도 서버 수정 없이 테스트해 보겠습니다 터미널에서 직접 APNs로 푸시 요청을 보내면 이를 수행할 수 있죠 이러한 명령행을 설정하려면 '명령행 도구를 사용하여 푸시 알림 보내기' 문서를 참조하세요 '토큰을 사용하여 푸시 알림 보내기' 섹션의 지침을 따르는 걸 잊지 마시고요 모든 설정이 올바르게 돼 있는지 쉽게 확인하려면 인증 토큰 변수를 출력하면 됩니다 다음으로 필요한 정보는 푸시 토큰입니다 조금 전에 콘솔에 푸시 토큰을 로깅하는 코드를 추가했죠 거기서 푸시 토큰을 가져오면 됩니다 같은 방법을 쓰셨다면 앱을 기기에 배포하고 실시간 현황을 시작하세요 활동이 시작되면 금방 앱이 푸시 토큰을 로깅할 겁니다 푸시 토큰을 복사하고 활동 푸시 토큰 변수로 터미널에 설정하세요 APNs 요청을 보내기 위해 curl 커맨드를 실행합니다 다음은 제가 작성한 모험 실시간 현황용 커맨드입니다 apns-topic 헤더가 앱의 번들 ID에 설정돼 있고 푸시 유형 접미사가 뒤따르죠 apns-push-type 헤더는 liveactivity로 설정되었습니다 세 번째로 apns-priority는 10으로 설정되어 요청이 즉시 전달됩니다 마지막 HTTP 헤더인 authorization은 'bearer'로 설정되었고 인증 토큰 변수가 뒤따릅니다 데이터를 보면 APNs 페이로드를 전부 포함하죠 날짜 커맨드로 타임스탬프를 자동 생성하여 초 단위까지 정확한 숫자를 산출합니다 마지막으로 URL은 HTTP2를 사용해야 합니다 URL의 끝에는 이전 단계에서 설정한 활동 푸시 토큰 변수를 참조합니다 완성입니다 이 curl 커맨드를 실행하면 실시간 현황이 페이로드에 제공된 새로운 콘텐츠 상태로 업데이트됩니다 간혹 실시간 현황이 의도대로 업데이트되지 않는 상황이 있는데요 그럴 때는 가장 먼저 curl 커맨드 실행에서 오류 응답이 없는지 확인하세요 오류가 있다면 요청에 잘못된 필드가 있거나 환경 설정에 문제가 있을지도 모릅니다 APNs가 성공적인 응답을 반환했는데도 실시간 현황이 업데이트되지 않은 경우 콘솔 앱으로 기기 로그를 확인해서 문제를 검토해 보세요 관련 로그를 지닌 프로세스로는 liveactivitiesd, apsd chronod가 있습니다 푸시 알림을 통한 실시간 현황 업데이트가 만족스럽다면 서버를 수정하여 실제 푸시 업데이트를 보내면 됩니다 이제 사용자 경험 설계의 중요한 부분으로 넘어갈게요 우선순위와 경고입니다 최상의 사용자 경험을 위해서는 업데이트마다 올바른 푸시 우선순위를 선택해야 해요 항상 낮은 우선순위를 먼저 고려해야 합니다 낮은 우선순위 업데이트는 기회가 생길 때 전달되어 사용자의 배터리 수명이 영향을 덜 받게 되죠 하지만 푸시 요청을 보냈을 때 실시간 현황이 즉시 업데이트되지 않을 수도 있습니다 따라서 낮은 우선순위는 시간에 민감하지 않은 업데이트에 써야 하죠 모험 실시간 현황의 경우 흔한 아이템을 찾거나 영웅이 체력을 약간 회복하는 업데이트는 사용자의 즉각적인 관심이 필요 없습니다 따라서 낮은 우선순위 업데이트에 적합하죠 낮은 우선순위 사용의 또 다른 이점은 보낼 수 있는 업데이트 수에 제한이 없다는 것입니다 이를 활용하려면 실시간 현황 업데이트 대부분에 낮은 우선순위를 써야 하죠 반면 사용자의 즉각적인 관심이 필요한 업데이트도 있습니다 예를 들면 영웅이 쓰러졌거나 주요 보스를 물리쳤을 때죠 이런 경우에는 높은 우선순위 업데이트를 선택합니다 높은 우선순위 업데이트는 즉시 전달됩니다 그래서 시간에 민감한 업데이트에 적합하죠 하지만 배터리 수명에 미치는 영향 때문에 기기 상태에 따라 시스템이 예산을 부여합니다 앱이 예산을 초과하면 시스템이 푸시 업데이트를 제한하고 이는 사용자 경험에 막대한 영향을 주죠 앱을 가장 잘 아는 건 여러분인 만큼 각 업데이트에 어떤 우선순위를 사용할지 신중하게 고려하세요 저는 Emoji Rangers에 특별한 모험 유형을 도입해서 파티가 연속해서 주요 보스와 싸우게 할 겁니다 이런 집중적인 실시간 현황의 사용자 경험을 최적화하려면 서버에서 높은 우선순위 푸시를 자주 보내 최신 상태를 유지해야 하죠 이를 지원하려면 실시간 현황의 빈번한 업데이트 기능을 활성화해야 합니다 이 기능을 활성화하면 앱의 업데이트 예산이 높아져서 실시간 현황 업데이트가 제한될 가능성이 줄죠 이 기능을 도입하려면 info.plist에 새로운 키인 NSSupportsLiveActivities FrequentUpdates를 추가하고 값을 YES로 설정합니다 사용자는 실시간 현황의 빈번한 업데이트를 설정에서 개별적으로 비활성화할 수 있습니다 ActivityAuthorizationInfo frequentPushesEnabled 프로퍼티에 접근해서 빈번한 업데이트 기능의 상태를 확인할 수 있죠 서버는 이 값에 따라 업데이트 빈도를 조정합니다 그러니 푸시 업데이트를 보내기 전에 서버로 보내는 걸 잊지 마세요 활동이 시작된 후에 한 번만 이 값을 확인하면 됩니다 값이 변경되면 시스템이 진행 중인 모든 활동을 종료하므로 활동 수명 동안 빈번한 업데이트가 이뤄지는 걸 서버에서 걱정하지 않아도 되죠 저는 모험 실시간 현황에서 영웅이 쓰러졌을 때 즉시 업데이트하는 것뿐만 아니라 사용자의 주의를 끌어서 바로 앱으로 들어와 회복 물약을 사용하게 하고 싶습니다 이를 위해 필드 세 개를 포함하는 'alert' 객체를 페이로드에 추가하겠습니다 'title'은 알림의 이름입니다 'body'는 업데이트에 대한 짧은 메시지죠 'sound'는 경고가 뜰 때 재생되는 소리를 나타냅니다 Emoji Rangers는 다국어를 지원하므로 영어로만 경고를 보내는 건 이상적이지 않습니다 하지만 서버에서 현지화를 처리하는 건 매우 까다롭죠 다행히도 경고 객체의 title과 body 필드를 설정하는 방법이 또 있습니다 문자열을 전달하는 대신 현지화된 문자열 객체로 설정하는 거죠 'loc-key' 필드가 현지화 키가 됩니다 앱의 현지화 파일에서 찾을 수 있죠 'loc-args' 필드는 현지화된 문자열에 삽입할 값의 목록입니다 이제 기기가 사용자의 로케일에 따라 자동으로 알림을 현지화합니다 경고의 완성도를 높이기 위해 각각의 업데이트에 맞춤형 알림 소리를 추가하고 싶은데요 그러려면 우선 앱의 타깃에 소리 파일을 리소스로 추가해야 합니다 그런 다음 경고 객체의 sound 필드를 소리 파일명으로 설정합니다 이렇게 하면 보기에도 좋고 듣기에도 좋은 경고가 완성되죠 이제 몇 가지를 개선하여 실시간 현황 사용자 경험을 향상해 보겠습니다 모험이 끝나고 일정 시간이 지나면 실시간 현황이 종료되게 해 보죠 이벤트를 end로 설정하고 푸시 페이로드를 보냅니다 사용자 지정 'dismissal-date'를 제공하여 실시간 현황이 잠금 화면에서 사라지는 시점을 정합니다 이 필드를 생략하면 시스템이 알아서 실시간 현황을 종료하죠 dismissal-date 값은 1970년 이후의 초 단위 시간 간격이어야 합니다 또한 실시간 현황의 최종 업데이트를 위해 최종 콘텐츠 상태를 제공하겠습니다 이 또한 선택 사항이며 생략하면 활동이 종료될 때까지 이전 콘텐츠 상태를 계속 표시합니다 사용자 기기가 푸시 알림을 받지 못하는 경우도 있는데요 그러면 모험 실시간 현황이 너무 오래된 체력 값을 표시할지도 모르죠 이런 경우 실시간 현황 UI에서 표시된 정보가 부정확함을 사용자에게 경고해야 합니다 'stale-date' 필드를 페이로드에 추가하면 시스템이 이 날짜로 만료된 뷰의 렌더링 시점을 정합니다 위젯 확장에서 선언된 ActivityConfiguration으로 만료된 뷰를 제공할 수 있죠 ActivityViewContext의 isStale 프로퍼티 값에 뷰가 반응하도록 하면 됩니다 모험 실시간 현황이 동시에 여러 개 있는 경우 올바른 순서로 잠금 화면에 표시되어야 합니다 중요한 업데이트를 포함할수록 상단에 위치하고 가장 중요한 현황이 Dynamic Island에 표시돼야 하죠 이를 위해 선택적 필드인 'relevance-score'를 제공합니다 숫자가 높을수록 관련성도 커지죠 이제 푸시 알림으로 실시간 현황을 업데이트하는 방법을 알았으니 앱에 적용해 보세요 먼저 ActivityKit 푸시 알림을 지원하도록 서버와 앱을 구성합니다 그런 다음 터미널에서 푸시 업데이트 전송을 테스트하여 빠르게 반복하세요 결과가 만족스러우면 서버에서 종단 간 지원을 구현합니다 또한 사용자 경험을 고려하며 적절한 우선순위를 사용하고 필요시 경고를 보내세요 저와 실시간 현황을 알아보는 게 즐거우셨나요? 여러분이 Dynamic Island와 잠금 화면에 불러올 창의적인 아이디어가 기대됩니다
시청해 주셔서 감사합니다 ♪ ♪
-
-
3:53 - Enabling push updates
func startActivity(hero: EmojiRanger) throws { let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let activity = try Activity.request( attributes: adventure, content: .init(state: initialState, staleDate: nil), pushType: .token ) Task { for await pushToken in activity.pushTokenUpdates { let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) } Logger().log("New push token: \(pushTokenString)") try await self.sendPushToken(hero: hero, pushTokenString: pushTokenString) } } }
-
6:54 - APNs push payload: Updating
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
7:37 - Printing content state JSON
let contentState = AdventureAttributes.ContentState( currentHealthLevel: 0.941, eventDescription: "Power Panda found a sword!" ) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let json = try! encoder.encode(contentState) Logger().log("\(String(data: json, encoding: .utf8)!)")
-
9:18 - Terminal: Constructing an APNs request with curl
curl \ --header "apns-topic: com.example.apple-samplecode.Emoji-Rangers.push-type.liveactivity" \ --header "apns-push-type: liveactivity" \ --header "apns-priority: 10" \ --header "authorization: bearer $AUTHENTICATION_TOKEN" \ --data '{ "aps": { "timestamp": '$(date +%s)', "event": "update", "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }' \ --http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
-
14:21 - APNs push payload: Alerting
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": "Power Panda is knocked down!", "body": "Use a potion to heal Power Panda!", "sound": "default" } } }
-
14:56 - APNs push payload: Alert localization
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:25 - APNs push payload: Alert sound
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:52 - APNs push payload: Dismissal
{ "aps": { "timestamp": 1685952000, "event": "end", "dismissal-date": 1685959200, "content-state": { "currentHealthLevel": 0.23, "eventDescription": "Adventure over! Power Panda is taking a nap." } } }
-
16:44 - APNs push payload: Stale date
{ "aps": { "timestamp": 1685952000, "event": "update", "stale-date": 1685959200, "content-state": { "currentHealthLevel": 0.79, "eventDescription": "Egghead is in the woods and lost connection." } } }
-
16:54 - Displaying a stale Live Activity UI
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in AdventureLiveActivityView( hero: context.attributes.hero, isStale: context.isStale, contentState: context.state ) .activityBackgroundTint(Color.gameWidgetBackground) } dynamicIsland: { context in // ... } } }
-
17:19 - APNs push payload: Relevance score
{ "aps": { "timestamp": 1685952000, "event": "update", "relevance-score": 100, "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.