안녕하세요! 피피아노입니다 🎵
이번 포스팅에서는 Apple의 Multipeer Connectivity 프레임워크에 대해서 정리를 해보려고 합니다.
최근 프로필 카드 교환 앱을 개발하면서 Apple의 Multipeer Connectivity 프레임워크를 사용하게 되었습니다. 처음에는 그냥 단순히 데이터를 보내고 받으면 되겠지라고 생각했는데, 실제로는 예상보다 복잡한 동작 과정과 고려해야 할 점들이 많았습니다. 이번 글에서는 Multipeer Connectivity의 동작 원리와 실제 구현 경험을 공유하고자 합니다.
그럼 바로 시작하겠습니다!
Multipeer Connectivity
Apple의 Multipeer Connectivity 프레임워크는 P2P(peer-to-peer) 연결성과 근처 기기 발견을 지원하는 프레임워크입니다.
https://developer.apple.com/documentation/multipeerconnectivity
Multipeer Connectivity | Apple Developer Documentation
Support peer-to-peer connectivity and the discovery of nearby devices.
developer.apple.com
이 프레임워크의 특징은 인터넷 연결 없이도 Wi-Fi, Bluetooth, Peer-to-peer Wi-Fi를 통해 iOS 기기들 간의 직접적인 데이터 통신을 가능하게 해줍니다.
우리가 가장 많이 사용하는 예시는 바로 AirDrop입니다.
같은 Wi-Fi에 연결되지 않은 기기끼리도 파일을 주고받을 수 있는 기능이 이 프레임워크를 기반으로 만들어진 것입니다.
Multipeer Connectivity의 핵심 특징
Multipeer Connectivity 프레임워크는 Bonjour 프로토콜 위에 레이어를 제공하며, 두 기기가 같은 Wi-Fi 네트워크에 있으면 Wi-Fi를, 그렇지 않으면 Peer-to-peer Wi-Fi 또는 Bluetooth를 자동으로 선택합니다.
네트워킹 기술을 선택할 필요 없이 프레임워크가 자동으로 최적의 연결 방식을 결정해주기 때문에 생각보다 구현이 어렵지가 않습니다.
지원하는 데이터 타입
지원하는 데이터 타입으로는 텍스트나 작은 크기의 구조화된 데이터나 실시간 오디오, 비디오, 파일 전송 등이 가능하다고 합니다.
주요 구성 요소
MCPeerID
MCPeerID는 각 기기를 세션 내에서 고유하게 식별하는 클래스입니다.
보통은 사용자의 기기 이름을 사용합니다.
private let myPeerId = MCPeerID(displayName: UIDevice.current.name)
MCSession
MCSession은 연결된 모든 피어 기기들을 관리하고 메시지 송수신을 가능하게 하는 클래스입니다.
private let session: MCSession
override init() {
session = MCSession(peer: myPeerId,
securityIdentity: nil,
encryptionPreference: .none)
session.delegate = self
}
MCNearbyServiceAdvertiser
MCNearbyServiceAdvertiser는 다른 기기들에게 자신의 존재를 알려주는 서비스입니다.
private let serviceAdvertiser: MCNearbyServiceAdvertiser
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerId,
discoveryInfo: nil,
serviceType: serviceType)
serviceAdvertiser.delegate = self
serviceAdvertiser.startAdvertisingPeer()
MCNearbyServiceBrowser
MCNearbyServiceBrowser는 근처의 다른 피어(기기)들을 찾는 서비스입니다.
private let serviceBrowser: MCNearbyServiceBrowser
serviceBrowser = MCNearbyServiceBrowser(peer: myPeerId, serviceType: serviceType)
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers()
연결 과정 흐름
실제 두 기기가 연결되는 과정은 아래 방식으로 진행이 됩니다.
1단계: 서로 찾기
- A기기는 advertiser.startAdvertisingPeer()로 자신을 알림
- B기기는 browser.startBrowsingForPeers()로 근처 기기 검색
- B기기가 A기기를 발견하면 foundPeer 델리게이트 호출
2단계: 연결 요청
- B기기에서 browser.invitePeer(A기기, to: session)로 초대 전송
- A기기에서 didReceiveInvitationFromPeer 델리게이트 호출
3단계: 연결 승인/거절
- A기기에서 invitationHandler(true, session) 호출하면 연결 시작
- 양쪽 기기에서 session:peer:didChange: 델리게이트로 연결 상태 변화 알림
이 과정을 실제 구현하면서 깨달은 것은, 각 단계마다 실패 가능성이 있고 이를 모두 처리해야 한다는 점이었습니다.
구현에서 중요했던 부분들
구현을 하면서 고려해야 했던 중요한 포인트를 몇 가지 이야기해보자면 우선 같은 기기를 여러 번 발견할 수 있다는 점입니다. 그래서 중복 체크가 필수였습니다. 또한 발견된 기기가 사라질 수도 있으므로 lostPeer 델리게이트도 구현해야 했습니다.
또한 연결 상태 관리가 생각보다 복잡했습니다.
연결 과정에는 여러 단계가 있었고, 각 단계마다 다른 처리가 필요했습니다.
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)")
// 연결 성공 시 대기 중인 데이터 전송 시작
}
}
}
구현하면서 겪은 문제들과 해결 방법
해당 기능을 구현하면서 겪었던 가장 어려웠던 문제는 연결은 됐는데 데이터 교환이 안 되는 문제입니다. 물론 지금도 완벽하게 해결을 한 건 아니지만 그래도 해결한(?) 방법에 대해서 정리를 해보자면
먼저 MCSessionState.connected가 되었다고 해서 바로 데이터를 보내면 종종 데이터 전송이 실패를 하는 문제가 나타났습니다.
그래서 해당 문제는 연결이 되면 DispatchQueue를 사용해서 0.5초 정도 딜레이를 주고 데이터를 교환하도록 하였습니다.
그리고 제가 개발한 기능은 서로의 카드를 주고받아야 하기 때문에, 양방향 데이터 교환이 이루어져야 했습니다.
A가 B에게 카드를 보내고, B가 A에게 카드를 보내는 과정에서 누가 먼저 보내고, 둘 다 받았는지를 모두 추적해야 했기 때문에 이 부분이 생각보다 까다로웠습니다.
// 전송과 수신 상태를 별도로 추적
private var cardsSentTo: Set<String> = []
private var cardsReceivedFrom: Set<String> = []
// 카드 전송 후
func sendCard(_ card: CardModel, to peer: MCPeerID) {
// ... 전송 로직
cardsSentTo.insert(peer.displayName) // 전송 완료 기록
}
// 카드 수신 후
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
// ... 수신 처리
cardsReceivedFrom.insert(senderID) // 수신 완료 기록
// 양방향 교환 완료 체크
checkExchangeCompletion(with: senderID)
}
// 양방향 교환 완료 확인
private func checkExchangeCompletion(with peerID: String) {
if cardsSentTo.contains(peerID) && cardsReceivedFrom.contains(peerID) {
print("양방향 카드 교환 완료!")
DispatchQueue.main.async {
self.cardExchangeCompleted = true // UI 업데이트
}
}
}
개선해야 하는 부분들
현재는 "동작은 한다" 수준까지 구현했지만, 실제 사용하다 보니 몇 가지 아쉬운 부분들이 있습니다.
중복 카드 문제: 같은 친구와 카드를 다시 교환하면 또 다른 카드로 저장되는데, 친구가 프로필을 업데이트했을 때는 기존 카드를 갱신해서 정보를 새로 업데이트할 수 있도록 개선을 해야 합니다.
연결 안정성: 양방향으로 카드를 주고받을 때 종종 한쪽만 성공하고 다른 쪽은 실패하는 경우가 있습니다.
속도 문제: 전체 교환 과정이 짧으면 5초에서 길면 20초까지도 걸리는데, 이 부분도 개선을 할 예정입니다.
감사합니다.
잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!
궁금하신 부분은 댓글로 질문 부탁드립니다!
'Apple > Swift' 카테고리의 다른 글
[Swift] Swift Testing 톺아보기 (6) | 2025.07.02 |
---|---|
[Swift] suffix()로 인한 시간 초과 문제 해결하기 (8) | 2025.06.27 |
[Swift] Foundation Models Framework (6) | 2025.06.22 |
[Swift] Core ML과 MFCC를 활용한 감정 추론 (7) | 2025.05.14 |
[Swift] Tuist 살펴보기 (0) | 2025.04.27 |