YunDaeHyeon
← Lab

Multipeer Connectivity에 대해

스터디2025.12.22
  • SwiftUI
  • Swift
  • MultipeerConnectivity

Multipeer Connectivity | Apple Developer Documentation

MultipeerConnectivity 는 주변에 있는 Apple 기기끼리 인터넷 연결 없이도 직접 통신(P2P)를 할 수 있게 해주는 프레임워크이다.

→ SwiftUI에선 이걸 자체적으로 지원하지는 않고 ViewModel이라던지 별도의 객체로 감싸서 사용한다고 한다.

Wi-Fi와 P2P Wi-Fi, Bluetooth를 사용하므로 같은 공간에 있는 기기들끼리 채팅, 게임, 파일 전송 같은 로컬 멀티 플레이 또는 공유 기능을 만들 때 사용한다.

💁‍♂️ 짚고 넘어가기

  1. iOS에서는 프레임워크 전송을 위해 Wi-Fi , P2P Wi-Fi, Bluetooth를 사용한다.
  2. macOS 및 tvOS에서는 Wi-Fi, P2P Wi-Fi, 이더넷 을 사용한다.

→ 두 기기가 같은 Wi-Fi 네트워크에 있으면 Wi-Fi를, 그렇지 않으면 P2P Wi-Fi 또는 Bluetooth를 자동으로 선택한다.

→ 즉, 로컬 네트워크를 사용하기 때문에 Info.plist에 NSLocalNetworkUsageDescription 를 선언해야한다.

→ 또한, 이때 내부적으로 Bonjour 프로토콜을 사용하기 때문에 NSBonjourServices 도 선언해야한다.

🤔 _Bonjour 프로토콜이란 뭐고 왜 사용하는걸까?_

Bonjour은 로컬 네트워크에서 “이 포트로 어떤 서비스가 열려있어요!(ex: 해당 포트에 채팅 서버가 열려있네요?)”를 알기 위한 Apple의 zeroconf 서비스 디스커버리 프로토콜이다.

https://en.wikipedia.org/wiki/Bonjour_(software)

  • Bonjour는 mDNS(멀티캐스트 DNS)와 DNS-SD(DND 서비스 디스커버리)를 이용해 같은 네트워크에 있는 장치들이 서로의 서비스를 이름 기반으로 찾게 해준다.
  • 이러한 특성으로 인해 사용자는 IP나 포트 번호를 직접 설정하지 않고도 _http._tcp.local. 처럼 서비스 타입과 이름만으로 접근할 수 있다.

→ 그렇다면 MultipeerConnectivity 과 Bonjour는 무슨 관계냐?

→ MultipeerConnectivity는 주변 기기 서비스 검색 을 위해 내부적으로 Bonjour 위에 레이어를 올린 프레임워크이기 때문에 실제 피어 검색과 광고가 Bonjour 매커니즘을 통해 이뤄진다!

→ 따라서 Info.plist에 NSBonjourServices 를 명시하여 어떤 Bongjour 서비스 타입을 탐색하고 광고할 것인지 명시해야한다.

---

🪛 구성 요소

📌 MCPeerID

MCPeerID | Apple Developer Documentation

MCPeerID는 각 기기(기기에서 실행 중인 앱 인스턴스)를 고유하기 식별하는 ID, 혹은 세션 내에서 고유하게 식별하는 클래스를 의미한다.

보통은 사용자의 기기 이름을 사용한다고 한다.

private let peerID = MCPeerID(displayName: UIDevice.current.name)

📌 MCSession

MCSession | Apple Developer Documentation

연결된 피어들 간의 실제 데이터 송수신을 담당하는 세션 객체이다.

MCSession을 통해 메세지나 데이터 스트림, 리소스를 전송할 수 있다.

세션은 연결 상태를 MCSessionDelegate를 통해 .notConnected, .connecting , .connected 상태로 알려준다.

private let session: MCsession

override init() {
	session = MCSession(peer: myPeerId,
											securityIndentity: nil,
											encryptionPreference: .none)
	session.delegate = self
}

📌 MCNearbyServiceAdvertiser

MCNearbyServiceAdvertiser | Apple Developer Documentation

앱이 지정된 유형의 세션에 참여할 의향이 있음을 주변 피어에게 알리는 역할을 수행한다.

Advertiser Object는 단일 로컬 피어를 사용하여 장치와 해당 사용자를 식별하는 정보를 근처 다른 장치에 제공한다.

→ 쉽게 말하면 나 이런 서비스 타입으로 통신할 수 있어요! 하고 광고하는 것이다.

→ 더 쉽게 말하면 다른 기기들에게 자신의 존재를 광고한다. (애초에 클래스 이름에 광고가 붙어있네)

private let serviceAdvertiser: MCNearbyServiceAdvertiser

serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID,
																							discoveryInfo: nil,
																							serviceType: serviceType)
serviceAdvertiser.delegate = self
serviceAdvertiser.startAdvertisingPeer()

📌 MCNearbyServiceBrowser

MCNearbyServiceBrowser | Apple Developer Documentation

앱이 특정 유형의 세션을 지원하는 앱이 있는 주변 장치를 검색한다.

Advertiser는 자기 자신을 알리는 역할이라면 Browser는 자기 자신을 알리는 피어를 찾고 연결 초대를 보낸다.

→ 즉, 근처의 다른 피어(기기)들을 찾는 역할이다.

private let serviceBrowser: MCNearbyServiceBrowser

serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers()

📌 MCBrowserViewController, MCAdvertiserAssistant

MCBrowserViewController | Apple Developer Documentation

MCAdvertiserAssistant | Apple Developer Documentation

UIKit 기반 기본 UI를 제공하는 도우미로 사용자가 직접 피어를 선택하거나 초대를 처리할 수 있게 해준다.

  • MCBrowserViewController의 경우 사용자가 세션에 추가할 주변 피어를 선택할 수 있는 표준 인터페이스를 제공해준다.
  • MCAdvertiserAssistant 의 경우 MCBrowserViewController와 같이 사용자가 초대를 수락할 수 있는 표준 사용자 인터페이스를 제공해주지만 고유한 사용자 인터페이스를 사용하거나 표시되는 초대를 개발자

🙌 쉽게 이해하는 작동 순서

💡 1. MCPeerID를 통해 장치를 고유하게 식별한다.

  1. MCNearbyServiceAdvertiser 를 사용하여 내 서비스를 주변에 알린다.

  2. MCNearbyServiceBrowser 를 통해 네트워크에서 근처 서비스를 찾는다.

  3. 그 후 MCSession을 통해 연결된 모든 장치(피어)를 관리하고 메세지 송수신을 허용한다.

  1. MCPeerID를 통해 장치를 고유하게 식별한다.
  2. MCNearbyServiceAdvertiser 를 사용하여 내 서비스를 주변에 알린다.
  3. MCNearbyServiceBrowser 를 통해 네트워크에서 근처 서비스를 찾는다.
  4. 그 후 MCSession을 통해 연결된 모든 장치(피어)를 관리하고 메세지 송수신을 허용한다.

1단계 (검색 단계)

  1. A기기는 advertiser.startAdvertisingPeer()를 자신을 근처에 알린다.
  2. B기기는 browser.startBrowsingForPeers()로 근처 기기를 검색한다.
  3. B기기가 A기기를 발견하면 foundPeer Delegate를 호출한다.

2단계 (연결 요청)

  1. B기기에서 browser.invitePeer(A기기, to: session)으로 초대를 전송한다.
  2. A기기에서 didReceiveInitationFromPeer Delegate를 호출한다.

3단계 (연결 승인 및 거절)

  1. A기기에서 invitationHandler(true, session)을 호출하면 연결이 시작된다.
  2. 양쪽 기기에서 session:peer:didChange: delegate로 연결 상태에 따른 변화를 감지한다.

❕ 주의사항 : 각 단계마다 실패 가능성이 있으며 이를 모두 처리해야한다!!!

→ 대표적으로 발견된 기기가 사라질 경우 lostPeer delegate를 구현하여 따로 처리를 해야한다.

대표적인 연결 단계는 아래와 같다.

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
    DispatchQueue.main.async {
        switch state {
        case .notConnected:
            print("연결 해제: \(peerID.displayName)")
            // 실패한 연결 시도인지, 정상적인 연결 해제인지 구분 필요
            
        case .connecting:
            print("연결 시도 중: \(peerID.displayName)")
            // 사용자에게 "연결 중" 표시
            
        case .connected:
            print("연결 완료: \(peerID.displayName)")
            // 연결 성공 시 대기 중인 데이터 전송 시작
        }
    }
}

❕주의사항

UIDevice.current.name 등으로 몽이의 iPhone 과 같이 사용자 지정 기기명을 그대로 가져올 수 없다.

iOS 16+ 부터는 기본값이 바뀌었으며 사용자 지정 기기명을 가져오기 위해선 Apple의 별도 entitlement 승인이 필요하다…ㅠ

→ 무슨 의미냐면 iOS 16+ 부터 UIDevice.name은 “사용자 이름이 붙은 기기의 이름”이 아닌 단순히 모델명(iPhone이나 iPad)을 반환하는 API로 바뀌었다.

→ 관련 자료 : https://developer.apple.com/videos/play/wwdc2022/10096/

→ 따라서 “사용자가 설정한 기기의 이름”을 얻기 위해선 com.apple.developer.device-information.user-assigned-device-name entitlement가 필요하다.

  • 다만, 디바이스의 이름이 앱의 핵심 기능에 반드시 필요한 경우에만 승인 및 신청이 가능하며 거절될 가능성이 크고, 심사 기간이 불규칙적이니… 꼭 고려하자..

https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.device-information.user-assigned-device-name

https://developer.apple.com/contact/request/user-assigned-device-name/