View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 머신 러닝 및 AI
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 주도 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • App Store Connect 도움말
    • 새로 추가될 요구 사항
    • 계약 및 지침
    • 시스템 상태
  • 빠른 링크

    • 이벤트
    • 뉴스
    • 포럼
    • 샘플 코드
    • 비디오
 

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 전체 비디오
  • 소개

더 많은 비디오

  • 소개
  • 요약
  • 코드
  • gRPC와 Swift로 실시간 앱과 서비스 빌드하기

    Swift 앱과 백엔드에서 gRPC로 매력적인 실시간 경험을 빌드하세요. gRPC는 고성능, 양방향 스트리밍 API를 위해 설계된 오픈 소스 RPC 프레임워크입니다. gRPC Swift 패키지가 Swift 동시성을 활용하여 빌드된 안전한 최신 런타임을 제공하는 방법을 살펴보세요. 통합된 도구로 어떻게 워크플로를 간소화하고 손쉽게 실시간 기능을 제공할 수 있는지 알아보세요.

    챕터

    • 0:00 - Introduction
    • 1:39 - Meet gRPC
    • 2:13 - App overview and demo setup
    • 3:30 - Defining the ListRaces RPC
    • 4:30 - Setting up Xcode to generate gRPC code
    • 7:50 - Managing the gRPC client lifecycle
    • 9:36 - Protobuf message format and binary efficiency
    • 12:33 - Implementing a bidirectional streaming RPC
    • 20:11 - Deploying the service
    • 23:11 - Next steps

    리소스

    • About gRPC
    • gRPC Swift Extras
    • gRPC Swift Protobuf
    • gRPC Swift NIO Transport
    • gRPC Swift
    • Swift on Server
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC25

    • 컨테이너화 만나보기

    WWDC24

    • Swift on Server 생태계 살펴보기

    WWDC23

    • Swift OpenAPI 생성기 알아보기
  • 비디오 검색…
    • 3:38 - ListRaces RPC definition

      edition = "2024";
      
      import "google/protobuf/timestamp.proto";
      
      service SwiftKartService {
        rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);
      }
      
      message ListRacesRequest {
        int32 limit = 1 [default = 100];
      }
      
      message ListRacesResponse {
        repeated Race races = 1;
      }
      
      message Race {
        string name = 1;
        string location = 2;
        google.protobuf.Timestamp start_time = 3;
        int32 laps = 4;
        string championship = 5;
      }
    • 5:55 - grpc-swift-proto-generator-config.json

      {
          "generate": {
              "clients": true,
              "servers": false,
              "messages": true
          }
      }
    • 6:24 - Add gRPC imports

      import GRPCCore
      import GRPCNIOTransportHTTP2
      import SwiftProtobuf
    • 6:38 - Create a gRPC client connected to a local server

      .task {
          do {
              try await withGRPCClient(
                  transport: .http2NIOTS(
                      address: .ipv4(host: "127.0.0.1", port: 8080),
                      transportSecurity: .tls
                  )
              ) { client in
                  <#code#>
              }
          } catch {
              print("gRPC error: \(error)")
          }
      }
    • 7:14 - Call the ListRaces RPC and update the view

      .task {
          do {
              try await withGRPCClient(
                  transport: .http2NIOTS(
                      address: .ipv4(host: "127.0.0.1", port: 8080),
                      transportSecurity: .tls
                  )
              ) { client in
                  let kart = SwiftKartService.Client(wrapping: client)
                  let request = ListRacesRequest()
                  let response = try await kart.listRaces(request)
                  self.races = response.races.map { race in
                      RaceInfo(
                          name: race.name,
                          location: race.location,
                          startTime: race.startTime.date,
                          championship: race.championship,
                          laps: Int(race.laps),
                          drivers: race.drivers
                      )
                  }
              }
          } catch {
              print("gRPC error: \(error)")
          }
      }
    • 8:30 - ClientManager.swift

      import GRPCCore
      import GRPCNIOTransportHTTP2
      import Synchronization
      import SwiftUI
      
      @Observable
      final class ClientManager: Sendable {
          fileprivate let state = Mutex(State.disconnected)
      
          static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {
              try .http2NIOTS(
                  target: .ipv4(address: "127.0.0.1", port: 8080),
                  transportSecurity: .plaintext
              )
          }
      
          func withClient(
              body: (_ client: GRPCClient<HTTP2ClientTransport.TransportServices>) async throws -> Void
          ) async throws {
              let client = try connectIfNecessary()
              try await body(client)
          }
      
          private func connectIfNecessary() throws -> GRPCClient<HTTP2ClientTransport.TransportServices> {
              try self.state.withLock { state in
                  try state.connectIfNecessary()
              }
          }
      
          func disconnect() {
              let client = self.state.withLock { state in
                  state.disconnect()
              }
      
              client?.beginGracefulShutdown()
          }
      }
      
      extension ClientManager {
          enum State {
              case connected(GRPCClient<HTTP2ClientTransport.TransportServices>, Task<Void, any Error>)
              case disconnected
          }
      }
      
      extension ClientManager.State {
          mutating func connectIfNecessary() throws -> GRPCClient<HTTP2ClientTransport.TransportServices> {
              switch self {
              case .connected(let client, _):
                  return client
      
              case .disconnected:
                  let client = try GRPCClient(transport: ClientManager.makeTransport())
                  let task = Task { try await client.runConnections() }
                  self = .connected(client, task)
                  return client
              }
          }
      
          mutating func disconnect() -> GRPCClient<HTTP2ClientTransport.TransportServices>? {
              switch self {
              case .connected(let client, _):
                  self = .disconnected
                  return client
              case .disconnected:
                  return nil
              }
          }
      }
    • 8:39 - Propagate ClientManager to child views

      import SwiftUI
      
      @main
      struct SwiftKartApp: App {
          let manager = ClientManager()
      
          var body: some Scene {
              WindowGroup {
                  RaceScheduleView()
                      .environment(manager)
              }
          }
      }
    • 8:52 - Disconnect ClientManager when the scene enters the background phase

      import SwiftUI
      
      @main
      struct SwiftKartApp: App {
          let manager = ClientManager()
          @Environment(\.scenePhase) private var scenePhase
      
          var body: some Scene {
              WindowGroup {
                  RaceScheduleView()
                      .environment(manager)
              }
              .onChange(of: scenePhase) { _, newPhase in
                  switch newPhase {
                  case .background :
                      manager.disconnect()
                  case .inactive, .active:
                      break
                  @unknown default:
                      break
                  }
              }
          }
      }
    • 9:12 - Inject ClientManager into the view via @Environment

      @Environment(ClientManager.self) var manager
    • 9:21 - Replace withGRPCClient with manager.withClient

      .task {
          do {
              try await manager.withClient { client in
                  let kart = SwiftKartService.Client(wrapping: client)
                  let request = ListRacesRequest()
                  let response = try await kart.listRaces(request)
                  self.races = response.races.map { race in
                      RaceInfo(
                          name: race.name,
                          location: race.location,
                          startTime: race.startTime.date,
                          championship: race.championship,
                          laps: Int(race.laps),
                          drivers: race.drivers
                      )
                  }
              }
          } catch {
              print("gRPC error: \(error)")
          }
      }
    • 9:41 - Using SwiftProtobuf

      var race = Race()
      race.name = "Duck Pond Dash"
      race.location = "Apple Park, Cupertino"
      race.startTime = .init(roundingTimeIntervalSince1970: 1_781_198_600)
      race.laps = 6
      race.championship = "Corporate Cup"
      race.drivers = ["Monty", "Pepper", "Mycroft", "Pancakes", "Duke", "Kiko", "Sissi", "Bo"]
      
      try race.serializedBytes()
    • 12:32 - Server

      let server = GRPCServer(
          transport: .http2NIOPosix(
              address: .ipv4(host: "127.0.0.1", port: 8080),
              transportSecurity: .plaintext
          ),
          services: [Service()]
      )
      try await server.serve()
    • 12:45 - Service

      struct Service: SwiftKartService.SimpleServiceProtocol {
          private let database = RaceDB()
      
          func listRaces(
              request: ListRacesRequest,
              context: ServerContext
          ) async throws -> ListRacesResponse {
              var response = ListRacesResponse()
              response.races = await database.listRaces(atMost: request.limit)
              return response
          }
      }
    • 13:20 - swift_kart_service.proto

      edition = "2024";
      
      import "google/protobuf/duration.proto";
      import "google/protobuf/timestamp.proto";
      
      service SwiftKartService {
        rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);
        rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse);
      }
      
      message ListRacesRequest {
        int32 limit = 1 [default = 100];
      }
      
      message ListRacesResponse {
        repeated Race races = 1;
      }
      
      message Race {
        string name = 1;
        string location = 2;
        google.protobuf.Timestamp start_time = 3;
        int32 laps = 4;
        string championship = 5;
        repeated string drivers = 6;
      }
      
      message FollowRaceRequest {
        string race_name = 1;
        repeated RaceEventType event_types = 2;
      }
      
      enum RaceEventType {
        RACE_EVENT_TYPE_UNSPECIFIED = 0;
        RACE_EVENT_TYPE_KART_LOCATIONS = 1;
        RACE_EVENT_TYPE_STANDINGS = 2;
      }
      
      message FollowRaceResponse {
        oneof event {
          KartLocations locations = 1;
          Standings standings = 2;
        }
      }
      
      message KartLocations {
        message Kart {
          int32 number = 1;
          double latitude = 2;
          double longitude = 3;
          google.protobuf.Timestamp recorded_at = 4;
        }
        repeated Kart karts = 1;
      }
      
      message Standings {
        message Entry {
          int32 kart_number = 1;
          google.protobuf.Duration gap_to_leader = 2;
          int32 position = 3;
          int32 lap = 4;
        }
      
        repeated Entry entries = 1;
      }
    • 14:16 - FollowRace stub

      func followRace(
          request: RPCAsyncSequence<FollowRaceRequest, any Error>,
          response: RPCWriter<FollowRaceResponse>,
          context: ServerContext
      ) async throws {
          throw RPCError(code: .unimplemented, message: "FollowRace is unimplemented")
      }
    • 14:38 - Implement the FollowRace RPC

      func followRace(
          request: RPCAsyncSequence<FollowRaceRequest, any Error>,
          response: RPCWriter<FollowRaceResponse>,
          context: ServerContext
      ) async throws {
          try await withThrowingTaskGroup { group in
              var iterator = request.makeAsyncIterator()
              guard let first = try await iterator.next() else { return }
              let eventTypes = Mutex(Set(first.eventTypes))
      
              group.addTask {
                  let events = tracker.events(forRace: first.raceName).filter { event in
                      eventTypes.withLock { $0.contains(event.type) }
                  }
      
                  for await event in events {
                      var message = FollowRaceResponse()
                      switch event {
                      case .locations(let locations):
                          message.locations.karts = locations.map { location in
                              var kart = KartLocations.Kart()
                              kart.number = Int32(location.number)
                              kart.latitude = location.latitude
                              kart.longitude = location.longitude
                              return kart
                          }
                      case .standings(let standings):
                          message.standings.entries = standings.map { standing in
                              var entry = Standings.Entry()
                              entry.gapToLeader = .init(rounding: standing.delta, rule: .towardZero)
                              entry.kartNumber = Int32(standing.kartNumber)
                              entry.lap = Int32(standing.lap)
                              entry.position = Int32(standing.position)
                              return entry
                          }
                      }
      
                      try await response.write(message)
                  }
              }
      
              while let next = try await iterator.next() {
                  eventTypes.withLock { $0 = Set(next.eventTypes) }
              }
      
              group.cancelAll()
          }
      }
    • 16:39 - swift_kart_service.proto

      edition = "2024";
      
      import "google/protobuf/duration.proto";
      import "google/protobuf/timestamp.proto";
      
      service SwiftKartService {
        rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);
        rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse);
      }
      
      message ListRacesRequest {
        int32 limit = 1 [default = 100];
      }
      
      message ListRacesResponse {
        repeated Race races = 1;
      }
      
      message Race {
        string name = 1;
        string location = 2;
        google.protobuf.Timestamp start_time = 3;
        int32 laps = 4;
        string championship = 5;
        repeated string drivers = 6;
      }
      
      message FollowRaceRequest {
        string race_name = 1;
        repeated RaceEventType event_types = 2;
      }
      
      enum RaceEventType {
        RACE_EVENT_TYPE_UNSPECIFIED = 0;
        RACE_EVENT_TYPE_KART_LOCATIONS = 1;
        RACE_EVENT_TYPE_STANDINGS = 2;
      }
      
      message FollowRaceResponse {
        oneof event {
          KartLocations locations = 1;
          Standings standings = 2;
        }
      }
      
      message KartLocations {
        message Kart {
          int32 number = 1;
          double latitude = 2;
          double longitude = 3;
          google.protobuf.Timestamp recorded_at = 4;
        }
        repeated Kart karts = 1;
      }
      
      message Standings {
        message Entry {
          int32 kart_number = 1;
          google.protobuf.Duration gap_to_leader = 2;
          int32 position = 3;
          int32 lap = 4;
        }
      
        repeated Entry entries = 1;
      }
    • 16:40 - swift_kart_service.proto

      edition = "2024";
      
      import "google/protobuf/timestamp.proto";
      
      service SwiftKartService {
        rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);
      }
      
      message ListRacesRequest {
        int32 limit = 1 [default = 100];
      }
      
      message ListRacesResponse {
        repeated Race races = 1;
      }
      
      message Race {
        string name = 1;
        string location = 2;
        google.protobuf.Timestamp start_time = 3;
        int32 laps = 4;
        string championship = 5;
        repeated string drivers = 6;
      }
    • 16:56 - Navigation link to LiveStreamView

      NavigationLink(destination: LiveStreamView(race: race)) {
          Text("Live stream")
      }
    • 17:32 - Call the FollowRace RPC in the LiveStreamView

      import SwiftUI
      import GRPCCore
      import GRPCNIOTransportHTTP2
      import SwiftProtobuf
      
      struct LiveStreamView: View {
          private let race: RaceInfo
      
          @Environment(ClientManager.self) var manager
          @State private var tracking: KartTrackingViewModel
          @State private var standings: [StandingsEntry] = []
          @State private var showLeaderboard = false
          @State private var continuation: AsyncStream<Bool>.Continuation?
      
          init(race: RaceInfo) {
              self.race = race
              self.tracking = KartTrackingViewModel(race: race)
          }
      
          var body: some View {
              VStack {
                  KartTrackingMapView(viewModel: tracking)
                      .ignoresSafeArea()
                      .onAppear { tracking.start() }
                      .onDisappear { tracking.stop() }
              }
              .onChange(of: showLeaderboard) { _, newValue in
                  continuation?.yield(newValue)
              }
              .sheet(isPresented: $showLeaderboard) {
                  LeaderboardView(race: race, standings: standings)
                      .presentationDetents([.fraction(0.3), .medium, .large])
                      .presentationBackgroundInteraction(.enabled)
              }
              .toolbar {
                  Toggle(isOn: $showLeaderboard) {
                      Label("Leaderboard", systemImage: "list.number")
                  }
              }
              .toolbarBackgroundVisibility(.visible, for: .navigationBar)
              .task {
                  do {
                      let (stream, continuation) = AsyncStream.makeStream(of: Bool.self)
                      self.continuation = continuation
                      continuation.yield(showLeaderboard)
      
                      try await manager.withClient { client in
                          let kart = SwiftKartService.Client(wrapping: client)
                          try await kart.followRace { requestStream in
                              for await showLeaderboard in stream {
                                  var message = FollowRaceRequest()
                                  message.raceName = race.name
                                  message.eventTypes = [.kartLocations]
                                  if showLeaderboard {
                                      message.eventTypes.append(.standings)
                                  }
                                  try await requestStream.write(message)
                              }
                          } onResponse: { responseStream in
                              for try await message in responseStream.messages {
                                  if let event = message.event {
                                      await handleEvent(event)
                                  }
                              }
                          }
      
                      }
                  } catch {
                      print("gRPC error: \(error)")
                  }
              }
          }
      
          @MainActor
          private func handleEvent(_ event: FollowRaceResponse.OneOf_Event) {
              switch event {
              case .locations(let locations):
                  self.tracking.updateKartCoordinates(
                      locations.karts.map {
                          TrackedKart(number: $0.number, latitude: $0.latitude, longitude: $0.longitude)
                      }
                  )
              case .standings(let standings):
                  self.standings = standings.entries.map {
                      StandingsEntry(
                          kartNumber: $0.kartNumber,
                          secondsToLeader: $0.gapToLeader.timeInterval,
                          position: $0.position,
                          lap: $0.lap
                      )
                  }
              }
          }
      }
      
      #Preview {
          NavigationStack {
              LiveStreamView(race: .example4)
                  .environment(ClientManager())
          }
      }
    • 20:55 - Containerfile

      FROM swift:latest AS builder
      
      # Copy sources into /app
      WORKDIR /app
      COPY Package.swift Package.resolved .
      COPY Sources/ Sources/
      
      # Build the server
      RUN swift build -c release --product server
      RUN cp "$(swift build -c release --show-bin-path)/server" /usr/bin/server
      
      # Copy the binary from the builder into a smaller runtime image.
      FROM swift:slim
      COPY --from=builder /usr/bin/server /usr/bin/server
      
      EXPOSE 8080
      ENTRYPOINT ["/usr/bin/server"]
    • 21:56 - Deploy service

      gcloud run deploy wwdc-demo-server \
        --image us-central1-docker.pkg.dev/wwdc26/wwdc-demo-server/wwdc-demo-server:latest \
        --region us-central1 \
        --use-http2 \
        --allow-unauthenticated
    • 22:22 - Target deployed service

      static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {
          try .http2NIOTS(
              target: .dns(host: "wwdc-demo-server-863666503339.us-central1.run.app"),
              transportSecurity: .tls
          )
      }
    • 0:00 - Introduction
    • Why hand-crafting networking code is error-prone, and how generating code from a service specification saves time and eliminates mistakes — setting up gRPC Swift as the approach for real-time experiences.

    • 1:39 - Meet gRPC
    • gRPC is explained as a CNCF-standard remote procedure call framework that uses Protocol Buffers to define APIs as typed functions rather than HTTP endpoints.

    • 2:13 - App overview and demo setup
    • A go-karting iOS app demo is introduced, showing how gRPC will replace static mock data with live server-fetched content.

    • 3:30 - Defining the ListRaces RPC
    • The ListRaces RPC and its request/response messages are defined in a .proto file, covering fields, field numbers, types, and Protobuf Well Known Types.

    • 4:30 - Setting up Xcode to generate gRPC code
    • The grpc-swift-nio-transport and grpc-swift-protobuf packages are added to the Xcode project, and the GRPCProtobufGenerator build plugin is configured to auto-generate Swift code from the proto file.

    • 7:50 - Managing the gRPC client lifecycle
    • A shared ClientManager is introduced to reuse connections across views and disconnect the client when the app enters the background, reducing unnecessary latency.

    • 9:36 - Protobuf message format and binary efficiency
    • The Protobuf binary serialization format is explained — using field numbers instead of names makes messages roughly half the size of equivalent JSON, benefiting mobile apps and service-to-service communication.

    • 12:33 - Implementing a bidirectional streaming RPC
    • The FollowRace bidirectional streaming RPC is defined, implemented on the Swift server using async sequences and task groups, and wired up in the iOS app to stream live kart positions and standings.

    • 20:11 - Deploying the service
    • The Swift server is containerised and deployed, then the app is updated to connect over TLS to the live production service.

    • 23:11 - Next steps
    • Recap of the full gRPC workflow, with pointers to prototype your own integrations, explore the open-source GitHub repository, and contribute to the project.

Developer Footer

  • 비디오
  • WWDC26
  • gRPC와 Swift로 실시간 앱과 서비스 빌드하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • Apple Intelligence
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • Mini Apps Partner Program
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    최신 뉴스 읽기.
    Apple Developer 앱 받기.
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침