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.
private → internal 변경 (캡슐화 약화)
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
문제: 피어 연결 후 정보가 누락됨
원인: MCSession의 didChange 콜백이 먼저 호출되고, 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 접근 제어자로 인해 분리가 어려울 수 있음
