๐Ÿ“ถ Wi-Fi Aware

Bluetooth ์—†์ด Wi-Fi๋กœ ๊ทผ๊ฑฐ๋ฆฌ ๊ธฐ๊ธฐ ๋ฐœ๊ฒฌ ๋ฐ P2P ํ†ต์‹ 

iOS 18+์‹ ๊ทœ ๊ธฐ์ˆ 

โœจ Wi-Fi Aware๋ž€?

Wi-Fi Aware๋Š” iOS 18์—์„œ ๋„์ž…๋œ ์ƒˆ๋กœ์šด ๊ทผ๊ฑฐ๋ฆฌ ํ†ต์‹  ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค. Bluetooth๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  Wi-Fi๋งŒ์œผ๋กœ ์ฃผ๋ณ€ ๊ธฐ๊ธฐ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ตํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด๋‚˜ ๊ธฐ์กด Wi-Fi ๋„คํŠธ์›Œํฌ ์—†์ด๋„ ๋™์ž‘ํ•˜๋ฉฐ, ์ €์ „๋ ฅ์œผ๋กœ ํšจ์œจ์ ์ธ P2P(Peer-to-Peer) ํ†ต์‹ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ต์‹ฌ ๊ธฐ๋Šฅ: Wi-Fi ๊ธฐ๋ฐ˜ ๊ธฐ๊ธฐ ๋ฐœ๊ฒฌ ยท P2P ๋ฐ์ดํ„ฐ ์ „์†ก ยท Bluetooth ๋ถˆํ•„์š” ยท ์ €์ „๋ ฅ ์†Œ๋น„ ยท ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐœ๊ฒฌ ยท ์•”ํ˜ธํ™”๋œ ํ†ต์‹  ยท ๋„คํŠธ์›Œํฌ ๋…๋ฆฝ ๋™์ž‘

๐Ÿ”ง 1. ํ”„๋กœ์ ํŠธ ์„ค์ •

Wi-Fi Aware๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ”„๋กœ์ ํŠธ์— ํ•„์ˆ˜ ๊ถŒํ•œ๊ณผ Capability๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Info.plist โ€” ๊ถŒํ•œ ์„ค๋ช… ์ถ”๊ฐ€
// Info.plist์— ์ถ”๊ฐ€
NSLocalNetworkUsageDescription
"๊ทผ์ฒ˜ ๊ธฐ๊ธฐ์™€ Wi-Fi๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค"

// Bonjour ์„œ๋น„์Šค ์ถ”๊ฐ€ (์˜ต์…˜)
NSBonjourServices
_wifiaware._tcp
โš™๏ธ Capabilities: Xcode ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ "Signing & Capabilities" ํƒญ์œผ๋กœ ์ด๋™ํ•˜์—ฌ "Nearby Interaction" capability๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

๐ŸŽฏ 2. Wi-Fi Aware ์„ธ์…˜ ์‹œ์ž‘

NIWiFiAwareSession์„ ์‚ฌ์šฉํ•˜์—ฌ Wi-Fi Aware ์„ธ์…˜์„ ์‹œ์ž‘ํ•˜๊ณ  ์ฃผ๋ณ€ ๊ธฐ๊ธฐ๋ฅผ ๋ฐœ๊ฒฌํ•ฉ๋‹ˆ๋‹ค.

WiFiAwareManager.swift โ€” ์„ธ์…˜ ๊ด€๋ฆฌ
import NearbyInteraction
import Network

@Observable
class WiFiAwareManager: NSObject {
    var session: NIWiFiAwareSession?
    var discoveryToken: NIWiFiAwareDiscoveryToken?
    var discoveredPeers: [NIWiFiAwarePeerToken] = []
    var isRunning = false

    // ์„ธ์…˜ ์‹œ์ž‘
    func startSession() {
        // Wi-Fi Aware ์„ธ์…˜ ์ƒ์„ฑ
        session = NIWiFiAwareSession()
        session?.delegate = self

        // ์„ธ์…˜ ์‹œ์ž‘ ๋ฐ Discovery Token ์ƒ์„ฑ
        do {
            discoveryToken = try session?.createDiscoveryToken()
            isRunning = true
            print("Wi-Fi Aware ์„ธ์…˜ ์‹œ์ž‘๋จ")
        } catch {
            print("์„ธ์…˜ ์‹œ์ž‘ ์‹คํŒจ: \(error)")
        }
    }

    // ์„ธ์…˜ ์ค‘์ง€
    func stopSession() {
        session?.invalidate()
        session = nil
        discoveryToken = nil
        discoveredPeers.removeAll()
        isRunning = false
        print("Wi-Fi Aware ์„ธ์…˜ ์ข…๋ฃŒ๋จ")
    }

    // Discovery Token์„ Data๋กœ ๋ณ€ํ™˜ (๊ณต์œ ์šฉ)
    func getDiscoveryTokenData() -> Data? {
        guard let token = discoveryToken else { return nil }
        return try? NSKeyedArchiver.archivedData(
            withRootObject: token,
            requiringSecureCoding: true
        )
    }

    // ์ƒ๋Œ€๋ฐฉ์˜ Discovery Token์œผ๋กœ ํ”ผ์–ด ์ถ”๊ฐ€
    func addPeer(tokenData: Data) {
        do {
            let peerToken = try NSKeyedUnarchiver.unarchivedObject(
                ofClass: NIWiFiAwarePeerToken.self,
                from: tokenData
            )

            if let peerToken = peerToken {
                // ํ”ผ์–ด ์ถ”๊ฐ€
                session?.add(peerToken)
                print("ํ”ผ์–ด ์ถ”๊ฐ€๋จ")
            }
        } catch {
            print("ํ”ผ์–ด ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
        }
    }

    // ํ”ผ์–ด ์ œ๊ฑฐ
    func removePeer(_ peerToken: NIWiFiAwarePeerToken) {
        session?.remove(peerToken)
        discoveredPeers.removeAll { $0 == peerToken }
        print("ํ”ผ์–ด ์ œ๊ฑฐ๋จ")
    }
}

// Delegate ๊ตฌํ˜„
extension WiFiAwareManager: NIWiFiAwareSessionDelegate {
    // ์„ธ์…˜์ด ์‹œ์ž‘๋˜์—ˆ์„ ๋•Œ
    func wifiAwareSession(_ session: NIWiFiAwareSession, didStartWith configuration: NIWiFiAwareConfiguration) {
        print("Wi-Fi Aware ์„ธ์…˜ ํ™œ์„ฑํ™”๋จ")
    }

    // ์„ธ์…˜์ด ์ค‘์ง€๋˜์—ˆ์„ ๋•Œ
    func wifiAwareSession(_ session: NIWiFiAwareSession, didInvalidateWith error: Error) {
        print("์„ธ์…˜ ๋ฌดํšจํ™”: \(error)")
        isRunning = false
    }

    // ํ”ผ์–ด๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์„ ๋•Œ
    func wifiAwareSession(_ session: NIWiFiAwareSession, didDiscover peerToken: NIWiFiAwarePeerToken) {
        if !discoveredPeers.contains(peerToken) {
            discoveredPeers.append(peerToken)
            print("์ƒˆ ํ”ผ์–ด ๋ฐœ๊ฒฌ: \(peerToken)")
        }
    }

    // ํ”ผ์–ด ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์„ ๋•Œ
    func wifiAwareSession(_ session: NIWiFiAwareSession, didLose peerToken: NIWiFiAwarePeerToken) {
        discoveredPeers.removeAll { $0 == peerToken }
        print("ํ”ผ์–ด ์—ฐ๊ฒฐ ๋Š๊น€: \(peerToken)")
    }
}

๐Ÿ“ก 3. ๋ฐ์ดํ„ฐ ์ „์†ก

Network ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Wi-Fi Aware๋กœ ๋ฐœ๊ฒฌํ•œ ํ”ผ์–ด์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค.

WiFiAwareDataChannel.swift โ€” ๋ฐ์ดํ„ฐ ์ฑ„๋„
import Network
import NearbyInteraction

@Observable
class WiFiAwareDataChannel {
    var connection: NWConnection?
    var listener: NWListener?
    var receivedMessages: [String] = []
    var isConnected = false

    // ๋ฐ์ดํ„ฐ ๋ฆฌ์Šค๋„ˆ ์‹œ์ž‘ (์„œ๋ฒ„ ์—ญํ• )
    func startListener(peerToken: NIWiFiAwarePeerToken) {
        let parameters = NWParameters.tcp
        parameters.requiredInterfaceType = .wifi

        do {
            listener = try NWListener(using: parameters)

            listener?.newConnectionHandler = { [weak self] newConnection in
                self?.handleNewConnection(newConnection)
            }

            listener?.stateUpdateHandler = { state in
                switch state {
                case .ready:
                    print("๋ฆฌ์Šค๋„ˆ ์ค€๋น„๋จ")
                case .failed(let error):
                    print("๋ฆฌ์Šค๋„ˆ ์‹คํŒจ: \(error)")
                default:
                    break
                }
            }

            listener?.start(queue: .main)
        } catch {
            print("๋ฆฌ์Šค๋„ˆ ์‹œ์ž‘ ์‹คํŒจ: \(error)")
        }
    }

    // ํ”ผ์–ด์— ์—ฐ๊ฒฐ (ํด๋ผ์ด์–ธํŠธ ์—ญํ• )
    func connectToPeer(endpoint: NWEndpoint) {
        let parameters = NWParameters.tcp
        parameters.requiredInterfaceType = .wifi

        connection = NWConnection(to: endpoint, using: parameters)

        connection?.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                self?.isConnected = true
                print("ํ”ผ์–ด ์—ฐ๊ฒฐ ์™„๋ฃŒ")
                self?.receiveData()
            case .failed(let error):
                self?.isConnected = false
                print("์—ฐ๊ฒฐ ์‹คํŒจ: \(error)")
            case .cancelled:
                self?.isConnected = false
                print("์—ฐ๊ฒฐ ์ทจ์†Œ๋จ")
            default:
                break
            }
        }

        connection?.start(queue: .main)
    }

    // ์ƒˆ ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
    private func handleNewConnection(_ newConnection: NWConnection) {
        connection = newConnection
        isConnected = true

        connection?.stateUpdateHandler = { [weak self] state in
            if state == .ready {
                self?.receiveData()
            }
        }

        connection?.start(queue: .main)
    }

    // ๋ฐ์ดํ„ฐ ์ „์†ก
    func sendData(_ message: String) {
        guard let data = message.data(using: .utf8) else { return }

        connection?.send(
            content: data,
            completion: .contentProcessed { error in
                if let error = error {
                    print("์ „์†ก ์‹คํŒจ: \(error)")
                } else {
                    print("๋ฉ”์‹œ์ง€ ์ „์†ก ์™„๋ฃŒ: \(message)")
                }
            }
        )
    }

    // ๋ฐ์ดํ„ฐ ์ˆ˜์‹ 
    private func receiveData() {
        connection?.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, context, isComplete, error in
            if let data = data, !data.isEmpty {
                if let message = String(data: data, encoding: .utf8) {
                    self?.receivedMessages.append(message)
                    print("๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : \(message)")
                }
            }

            if error == nil {
                self?.receiveData() // ๊ณ„์† ์ˆ˜์‹  ๋Œ€๊ธฐ
            }
        }
    }

    // ์—ฐ๊ฒฐ ์ข…๋ฃŒ
    func disconnect() {
        connection?.cancel()
        listener?.cancel()
        connection = nil
        listener = nil
        isConnected = false
    }
}

๐Ÿ”„ 4. Discovery Token ๊ตํ™˜

QR ์ฝ”๋“œ๋‚˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ Discovery Token์„ ๊ตํ™˜ํ•˜์—ฌ ํ”ผ์–ด๋ฅผ ๋ฐœ๊ฒฌํ•ฉ๋‹ˆ๋‹ค.

TokenExchange.swift โ€” Token ๊ตํ™˜
import SwiftUI
import CoreImage.filters

class TokenExchangeHelper {
    // Token์„ QR ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜
    func generateQRCode(from tokenData: Data) -> UIImage? {
        let filter = CIFilter.qrCodeGenerator()
        filter.message = tokenData

        if let outputImage = filter.outputImage {
            let transform = CGAffineTransform(scaleX: 10, y: 10)
            let scaledImage = outputImage.transformed(by: transform)

            let context = CIContext()
            if let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) {
                return UIImage(cgImage: cgImage)
            }
        }

        return nil
    }

    // Base64 ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (๊ฐ„๋‹จํ•œ ๊ณต์œ )
    func encodeTokenToString(_ tokenData: Data) -> String {
        return tokenData.base64EncodedString()
    }

    // Base64 ๋ฌธ์ž์—ด์„ Token Data๋กœ ๋ณ€ํ™˜
    func decodeTokenFromString(_ string: String) -> Data? {
        return Data(base64Encoded: string)
    }
}

๐Ÿ“ฑ 5. SwiftUI ํ†ตํ•ฉ

Wi-Fi Aware ๊ธฐ๋Šฅ์„ SwiftUI ์•ฑ์— ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

WiFiAwareView.swift โ€” SwiftUI ํ†ตํ•ฉ
import SwiftUI
import NearbyInteraction

struct WiFiAwareView: View {
    @State private var manager = WiFiAwareManager()
    @State private var dataChannel = WiFiAwareDataChannel()
    @State private var tokenHelper = TokenExchangeHelper()

    @State private var messageToSend = ""
    @State private var showQRCode = false
    @State private var qrCodeImage: UIImage?

    var body: some View {
        NavigationStack {
            List {
                // ์„ธ์…˜ ์ƒํƒœ
                Section("์„ธ์…˜") {
                    HStack {
                        Text("์ƒํƒœ")
                        Spacer()
                        Text(manager.isRunning ? "ํ™œ์„ฑ" : "๋น„ํ™œ์„ฑ")
                            .foregroundStyle(manager.isRunning ? .green : .secondary)
                    }

                    if manager.isRunning {
                        Button("๋‚ด QR ์ฝ”๋“œ ํ‘œ์‹œ") {
                            if let tokenData = manager.getDiscoveryTokenData() {
                                qrCodeImage = tokenHelper.generateQRCode(from: tokenData)
                                showQRCode = true
                            }
                        }
                    }

                    Button(manager.isRunning ? "์„ธ์…˜ ์ค‘์ง€" : "์„ธ์…˜ ์‹œ์ž‘") {
                        if manager.isRunning {
                            manager.stopSession()
                            dataChannel.disconnect()
                        } else {
                            manager.startSession()
                        }
                    }
                    .foregroundStyle(manager.isRunning ? .red : .blue)
                }

                // ๋ฐœ๊ฒฌ๋œ ํ”ผ์–ด
                Section("๋ฐœ๊ฒฌ๋œ ๊ธฐ๊ธฐ (\(manager.discoveredPeers.count))") {
                    if manager.discoveredPeers.isEmpty {
                        Text("์ฃผ๋ณ€์— ๊ธฐ๊ธฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
                            .foregroundStyle(.secondary)
                    } else {
                        ForEach(manager.discoveredPeers.indices, id: \.self) { index in
                            HStack {
                                Image(systemName: "wifi")
                                    .foregroundStyle(.blue)
                                Text("๊ธฐ๊ธฐ \(index + 1)")

                                Spacer()

                                if dataChannel.isConnected {
                                    Image(systemName: "checkmark.circle.fill")
                                        .foregroundStyle(.green)
                                }
                            }
                        }
                    }
                }

                // ๋ฉ”์‹œ์ง€ ์ „์†ก
                if dataChannel.isConnected {
                    Section("๋ฉ”์‹œ์ง€ ์ „์†ก") {
                        TextField("๋ฉ”์‹œ์ง€ ์ž…๋ ฅ", text: $messageToSend)

                        Button("์ „์†ก") {
                            dataChannel.sendData(messageToSend)
                            messageToSend = ""
                        }
                        .disabled(messageToSend.isEmpty)
                    }
                }

                // ์ˆ˜์‹  ๋ฉ”์‹œ์ง€
                Section("์ˆ˜์‹  ๋ฉ”์‹œ์ง€") {
                    if dataChannel.receivedMessages.isEmpty {
                        Text("์ˆ˜์‹ ๋œ ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
                            .foregroundStyle(.secondary)
                    } else {
                        ForEach(dataChannel.receivedMessages, id: \.self) { message in
                            Text(message)
                        }
                    }
                }
            }
            .navigationTitle("Wi-Fi Aware")
            .sheet(isPresented: $showQRCode) {
                QRCodeView(qrCodeImage: qrCodeImage)
            }
        }
    }
}

struct QRCodeView: View {
    let qrCodeImage: UIImage?
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„œ ์ด QR ์ฝ”๋“œ๋ฅผ ์Šค์บ”ํ•˜์„ธ์š”")
                    .font(.headline)

                if let image = qrCodeImage {
                    Image(uiImage: image)
                        .interpolation(.none)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 300, height: 300)
                        .padding()
                        .background(Color.white)
                        .cornerRadius(12)
                        .shadow(radius: 4)
                } else {
                    Text("QR ์ฝ”๋“œ ์ƒ์„ฑ ์‹คํŒจ")
                        .foregroundStyle(.secondary)
                }
            }
            .padding()
            .navigationTitle("๋‚ด QR ์ฝ”๋“œ")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("์™„๋ฃŒ") {
                        dismiss()
                    }
                }
            }
        }
    }
}

๐ŸŽฎ 6. ์‹ค์ „ ์˜ˆ์ œ - ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ฒŒ์ž„

Wi-Fi Aware๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ„๋‹จํ•œ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ฒŒ์ž„ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

WiFiAwareGameView.swift โ€” ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ฒŒ์ž„
import SwiftUI

struct GameMessage: Codable {
    enum Action: String, Codable {
        case move, attack, heal
    }

    let playerName: String
    let action: Action
    let position: CGPoint?
    let timestamp: Date
}

@Observable
class GameController {
    let wifiManager = WiFiAwareManager()
    let dataChannel = WiFiAwareDataChannel()

    var playerName = "Player"
    var playerPosition: CGPoint = .zero
    var opponentPosition: CGPoint = .zero
    var gameLog: [String] = []

    func startGame() {
        wifiManager.startSession()
    }

    func sendAction(action: GameMessage.Action, position: CGPoint? = nil) {
        let message = GameMessage(
            playerName: playerName,
            action: action,
            position: position,
            timestamp: Date()
        )

        if let data = try? JSONEncoder().encode(message),
           let string = String(data: data, encoding: .utf8) {
            dataChannel.sendData(string)
            gameLog.append("\(playerName): \(action.rawValue)")
        }
    }

    func processReceivedMessage(_ messageString: String) {
        guard let data = messageString.data(using: .utf8),
              let message = try? JSONDecoder().decode(GameMessage.self, from: data) else {
            return
        }

        if let position = message.position {
            opponentPosition = position
        }

        gameLog.append("\(message.playerName): \(message.action.rawValue)")
    }
}

struct WiFiAwareGameView: View {
    @State private var controller = GameController()

    var body: some View {
        VStack {
            // ๊ฒŒ์ž„ ํ™”๋ฉด
            ZStack {
                Color.gray.opacity(0.2)

                // ๋‚ด ์บ๋ฆญํ„ฐ
                Circle()
                    .fill(Color.blue)
                    .frame(width: 40, height: 40)
                    .position(controller.playerPosition)

                // ์ƒ๋Œ€ ์บ๋ฆญํ„ฐ
                Circle()
                    .fill(Color.red)
                    .frame(width: 40, height: 40)
                    .position(controller.opponentPosition)
            }
            .frame(height: 300)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        controller.playerPosition = value.location
                        controller.sendAction(action: .move, position: value.location)
                    }
            )

            // ์•ก์…˜ ๋ฒ„ํŠผ
            HStack(spacing: 20) {
                Button("๊ณต๊ฒฉ") {
                    controller.sendAction(action: .attack)
                }
                .buttonStyle(.borderedProminent)
                .tint(.red)

                Button("ํž") {
                    controller.sendAction(action: .heal)
                }
                .buttonStyle(.borderedProminent)
                .tint(.green)
            }
            .padding()

            // ๊ฒŒ์ž„ ๋กœ๊ทธ
            List(controller.gameLog, id: \.self) { log in
                Text(log)
                    .font(.caption)
            }
            .frame(height: 150)
        }
        .onAppear {
            controller.startGame()
        }
    }
}

๐Ÿ’ก HIG ๊ฐ€์ด๋“œ๋ผ์ธ

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ

๐Ÿ“š ๋” ์•Œ์•„๋ณด๊ธฐ

โšก๏ธ ์„ฑ๋Šฅ ํŒ: Wi-Fi Aware๋Š” Bluetooth๋ณด๋‹ค ๋น ๋ฅธ ๋ฐ์ดํ„ฐ ์ „์†ก ์†๋„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ „์†ก์ด๋‚˜ ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ์— ์ ํ•ฉํ•˜๋ฉฐ, ์ „๋ ฅ ์†Œ๋น„๋„ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๋‹จ, iOS 18 ์ด์ƒ์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์ด์ „ ๋ฒ„์ „ ์ง€์›์ด ํ•„์š”ํ•˜๋ฉด MultipeerConnectivity๋ฅผ ํ•จ๊ป˜ ๊ณ ๋ คํ•˜์„ธ์š”.