Search

iOS P2P 연결 서비스 구현 (MultipeerConnectivity)

What I Learned

1. MultipeerConnectivity 프레임워크 이해

iOS에서 Wi-Fi, Bluetooth를 통한 근거리 P2P 통신을 구현할 때 MultipeerConnectivity 프레임워크를 사용한다.

핵심 컴포넌트

클래스
역할
MCPeerID
네트워크 상의 기기 식별자
MCSession
연결된 피어들과의 통신 세션
MCNearbyServiceAdvertiser
자신을 다른 기기에 광고
MCNearbyServiceBrowser
주변 기기 탐색

연결 흐름

sequenceDiagram
    participant A as 기기 A (Advertiser)
    participant B as 기기 B (Browser)
    
    B->>A: 탐색 (discovery)
    B->>A: 초대 (invitation)
    A->>B: 수락 (accept)
    A-->>B: 연결됨
    B-->>A: 연결됨
Mermaid
복사

2. iOS 17+ @Observable 매크로

기존 ObservableObject + @Published 대신 @Observable 매크로를 사용하면 보일러플레이트가 크게 줄어든다.
// Before (iOS 13+) class MyViewModel: ObservableObject { @Published var count: Int = 0 } // After (iOS 17+) @Observable class MyViewModel { var count: Int = 0 // 자동으로 observable }
Swift
복사
장점:
@Published 키워드 불필요
View에서 @ObservedObject 대신 일반 변수로 사용 가능
성능 향상 (필요한 프로퍼티만 추적)

3. Extension 파일 분리의 한계

처음에는 Extension으로 파일을 분리하려 했으나, private 접근 제어자 문제로 실패했다.
// P2PService.swift @Observable final class P2PService { private var session: MCSession? // private 프로퍼티 } // P2PService+Delegates.swift extension P2PService: MCSessionDelegate { func session(...) { // ❌ 에러: 'session' is inaccessible due to 'private' protection level self.session?.send(data, toPeers: peers, with: .reliable) } }
Swift
복사
해결 방법:
1.
privateinternal 변경 (캡슐화 약화)
2.
한 파일로 합치기
결론: 서비스 클래스는 하나의 응집된 단위이므로 한 파일로 유지하는 것이 적합하다. 400~500줄이라도 단일 책임이면 괜찮다.

4. Thread Safety in MultipeerConnectivity

MC 프레임워크의 delegate 콜백은 백그라운드 스레드에서 호출된다. UI 업데이트는 반드시 메인 스레드에서 수행해야 한다.
func session( _ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState ) { DispatchQueue.main.async { [weak self] in self?.handleStateChange(peerID, state: state) } }
Swift
복사

5. Discovery Info를 활용한 피어 정보 전달

MCNearbyServiceAdvertiser 생성 시 discoveryInfo를 통해 연결 전에 피어 정보를 전달할 수 있다.
let discoveryInfo: [String: String] = [ "number": "\(userNumber)", "stage": userStage.rawValue ] advertiser = MCNearbyServiceAdvertiser( peer: peerID, discoveryInfo: discoveryInfo, // 최대 400바이트 serviceType: serviceType )
Swift
복사
제한사항:
Key-Value 모두 String 타입만 가능
총 크기 400바이트 이하

Trouble Shooting

문제: 피어 연결 후 정보가 누락됨

원인: MCSessiondidChange 콜백이 먼저 호출되고, discoveryInfo 처리가 늦게 완료됨
해결: pendingPeerInfo 딕셔너리를 사용하여 연결 완료 전에 피어 정보를 저장
// Browser에서 피어 발견 시 savePendingInfo((number, peerStage), for: peerID) // Session 연결 완료 시 if let pending = getPendingInfo(for: peerID) { let peer = P2PPeer(id: peerID, number: pending.number, stage: pending.stage) addPeer(peer, for: peerID) }
Swift
복사

Project Structure (MV Pattern)

Nesty/ ├── Models/ │ ├── NestStage.swift # 성장 단계 enum │ ├── P2PConnectionState.swift # 연결 상태 │ ├── P2PMessageType.swift # 메시지 타입 │ ├── P2PPayload.swift # 전송 데이터 구조체 │ └── P2PPeer.swift # 피어 정보 └── Services/ └── P2PService.swift # P2P 연결 서비스 (Singleton)
Plain Text
복사

Key Takeaways

1.
응집도 우선: 서비스 클래스는 한 파일로 유지, 400~500줄도 단일 책임이면 OK
2.
Thread Safety: MC 콜백은 항상 메인 스레드로 디스패치
3.
@Observable: iOS 17+ 타겟이라면 적극 활용
4.
Extension 분리 주의: private 접근 제어자로 인해 분리가 어려울 수 있음

References