๐ŸŒ Network Framework

ํ˜„๋Œ€์ ์ธ ์ €์ˆ˜์ค€ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ํ”„๋ ˆ์ž„์›Œํฌ

iOS 12+TLS 1.3 ์ง€์›

โœจ Network Framework๋ž€?

Network๋Š” TCP, UDP, TLS, WebSocket ๋“ฑ์˜ ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์ €์ˆ˜์ค€ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. URLSession๋ณด๋‹ค ๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, NWConnection, NWListener๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Bonjour ์„œ๋น„์Šค ๊ฒ€์ƒ‰, IPv6 ์ง€์›, ๋„คํŠธ์›Œํฌ ๊ฒฝ๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง ๋“ฑ์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ต์‹ฌ ๊ธฐ๋Šฅ: TCP/UDP ํ†ต์‹  ยท TLS 1.3 ์•”ํ˜ธํ™” ยท WebSocket ์ง€์› ยท Bonjour ๊ฒ€์ƒ‰ ยท ๋„คํŠธ์›Œํฌ ๊ฒฝ๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง ยท IPv4/IPv6 ยท ์ปค์Šคํ…€ ํ”„๋กœํ† ์ฝœ

๐Ÿ”Œ 1. NWConnection - TCP ํด๋ผ์ด์–ธํŠธ

TCP ์—ฐ๊ฒฐ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค.

TCPClient.swift โ€” TCP ํด๋ผ์ด์–ธํŠธ
import Network
import SwiftUI

@Observable
class TCPClient {
    private var connection: NWConnection?
    var isConnected = false
    var receivedMessages: [String] = []

    // TCP ์—ฐ๊ฒฐ ์‹œ์ž‘
    func connect(to host: String, port: UInt16) {
        // ํ˜ธ์ŠคํŠธ์™€ ํฌํŠธ ์„ค์ •
        guard let endpoint = NWEndpoint.Host(host) else {
            print("โŒ ์ž˜๋ชป๋œ ํ˜ธ์ŠคํŠธ")
            return
        }

        let port = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port.any

        // TCP ํŒŒ๋ผ๋ฏธํ„ฐ
        let parameters = NWParameters.tcp

        // TLS ํ™œ์„ฑํ™” (HTTPS)
        parameters.includeTLS = true

        // IPv4/IPv6 ์šฐ์„ ์ˆœ์œ„
        parameters.preferNoProxies = true
        parameters.requiredInterfaceType = .wifi // .wifi, .cellular, .loopback

        // ์—ฐ๊ฒฐ ์ƒ์„ฑ
        connection = NWConnection(host: endpoint, port: port, using: parameters)

        // ์ƒํƒœ ํ•ธ๋“ค๋Ÿฌ
        connection?.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                print("โœ… ์—ฐ๊ฒฐ ์„ฑ๊ณต")
                self?.isConnected = true
                self?.receiveMessage()

            case .waiting(let error):
                print("โณ ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์ค‘: \(error)")

            case .failed(let error):
                print("โŒ ์—ฐ๊ฒฐ ์‹คํŒจ: \(error)")
                self?.isConnected = false

            case .cancelled:
                print("โŒ ์—ฐ๊ฒฐ ์ทจ์†Œ๋จ")
                self?.isConnected = false

            default:
                break
            }
        }

        // ์—ฐ๊ฒฐ ์‹œ์ž‘
        connection?.start(queue: .global(qos: .userInitiated))
        print("๐Ÿ”— ์—ฐ๊ฒฐ ์‹œ๋„: \(host):\(port)")
    }

    // ๋ฉ”์‹œ์ง€ ์ „์†ก
    func sendMessage(_ message: String) {
        guard let connection = connection, isConnected else {
            print("โš ๏ธ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ")
            return
        }

        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 receiveMessage() {
        connection?.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, context, isComplete, error in
            if let error = error {
                print("โŒ ์ˆ˜์‹  ์—๋Ÿฌ: \(error)")
                return
            }

            if let data = data, !data.isEmpty {
                if let message = String(data: data, encoding: .utf8) {
                    print("๐Ÿ“ฅ ์ˆ˜์‹ : \(message)")
                    self?.receivedMessages.append(message)
                }
            }

            // ๊ณ„์† ์ˆ˜์‹  ๋Œ€๊ธฐ
            if !isComplete {
                self?.receiveMessage()
            }
        }
    }

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

๐ŸŽง 2. NWListener - TCP ์„œ๋ฒ„

TCP ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ์„ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค.

TCPServer.swift โ€” TCP ์„œ๋ฒ„
import Network

@Observable
class TCPServer {
    private var listener: NWListener?
    private var connections: [NWConnection] = []
    var isListening = false
    var port: UInt16 = 0

    // ์„œ๋ฒ„ ์‹œ์ž‘
    func startServer(on port: UInt16) {
        do {
            // TCP ํŒŒ๋ผ๋ฏธํ„ฐ
            let parameters = NWParameters.tcp
            parameters.acceptLocalOnly = true // ๋กœ์ปฌ ์—ฐ๊ฒฐ๋งŒ ํ—ˆ์šฉ

            // ๋ฆฌ์Šค๋„ˆ ์ƒ์„ฑ
            let nwPort = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port.any
            listener = try NWListener(using: parameters, on: nwPort)

            // ์ƒํƒœ ํ•ธ๋“ค๋Ÿฌ
            listener?.stateUpdateHandler = { [weak self] state in
                switch state {
                case .ready:
                    print("โœ… ์„œ๋ฒ„ ์‹œ์ž‘๋จ")
                    self?.isListening = true
                    if let port = self?.listener?.port {
                        self?.port = port.rawValue
                        print("  ํฌํŠธ: \(port)")
                    }

                case .failed(let error):
                    print("โŒ ์„œ๋ฒ„ ์‹คํŒจ: \(error)")
                    self?.isListening = false

                case .cancelled:
                    print("โŒ ์„œ๋ฒ„ ์ทจ์†Œ๋จ")
                    self?.isListening = false

                default:
                    break
                }
            }

            // ์ƒˆ ์—ฐ๊ฒฐ ํ•ธ๋“ค๋Ÿฌ
            listener?.newConnectionHandler = { [weak self] newConnection in
                print("๐Ÿ”— ์ƒˆ ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ")
                self?.handleConnection(newConnection)
            }

            // ๋ฆฌ์Šค๋„ˆ ์‹œ์ž‘
            listener?.start(queue: .global(qos: .userInitiated))

        } catch {
            print("โŒ ์„œ๋ฒ„ ์‹œ์ž‘ ์‹คํŒจ: \(error)")
        }
    }

    // ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
    private func handleConnection(_ connection: NWConnection) {
        connections.append(connection)

        // ์ƒํƒœ ํ•ธ๋“ค๋Ÿฌ
        connection.stateUpdateHandler = { state in
            switch state {
            case .ready:
                print("  โœ… ํด๋ผ์ด์–ธํŠธ ์ค€๋น„๋จ")
                self.receiveMessage(from: connection)

            case .failed(let error):
                print("  โŒ ํด๋ผ์ด์–ธํŠธ ์‹คํŒจ: \(error)")
                self.removeConnection(connection)

            case .cancelled:
                print("  โŒ ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ")
                self.removeConnection(connection)

            default:
                break
            }
        }

        connection.start(queue: .global(qos: .userInitiated))
    }

    // ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
    private func receiveMessage(from connection: NWConnection) {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
            if let error = error {
                print("  โŒ ์ˆ˜์‹  ์—๋Ÿฌ: \(error)")
                return
            }

            if let data = data, !data.isEmpty {
                if let message = String(data: data, encoding: .utf8) {
                    print("  ๐Ÿ“ฅ ์ˆ˜์‹ : \(message)")

                    // Echo ์‘๋‹ต
                    let response = "Echo: \(message)"
                    self.sendMessage(response, to: connection)
                }
            }

            if !isComplete {
                self.receiveMessage(from: connection)
            }
        }
    }

    // ๋ฉ”์‹œ์ง€ ์ „์†ก
    private func sendMessage(_ message: String, to connection: NWConnection) {
        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 removeConnection(_ connection: NWConnection) {
        connections.removeAll { $0 === connection }
    }

    // ์„œ๋ฒ„ ์ค‘์ง€
    func stopServer() {
        listener?.cancel()
        listener = nil

        connections.forEach { $0.cancel() }
        connections.removeAll()

        isListening = false
        print("โน๏ธ ์„œ๋ฒ„ ์ค‘์ง€")
    }
}

๐Ÿ“ก 3. UDP ํ†ต์‹ 

UDP ํ”„๋กœํ† ์ฝœ๋กœ ๋ฐ์ดํ„ฐ๊ทธ๋žจ์„ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค.

UDPClient.swift โ€” UDP ํด๋ผ์ด์–ธํŠธ
import Network

@Observable
class UDPClient {
    private var connection: NWConnection?
    var isConnected = false

    // UDP ์—ฐ๊ฒฐ
    func connect(to host: String, port: UInt16) {
        guard let endpoint = NWEndpoint.Host(host) else { return }
        let port = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port.any

        // UDP ํŒŒ๋ผ๋ฏธํ„ฐ
        let parameters = NWParameters.udp

        connection = NWConnection(host: endpoint, port: port, using: parameters)

        connection?.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                print("โœ… UDP ์ค€๋น„๋จ")
                self?.isConnected = true

            case .failed(let error):
                print("โŒ UDP ์‹คํŒจ: \(error)")
                self?.isConnected = false

            default:
                break
            }
        }

        connection?.start(queue: .global(qos: .userInitiated))
        print("๐Ÿ“ก UDP ์—ฐ๊ฒฐ: \(host):\(port)")
    }

    // ๋ฐ์ดํ„ฐ๊ทธ๋žจ ์ „์†ก
    func sendDatagram(_ message: String) {
        guard let connection = connection, isConnected else { return }
        guard let data = message.data(using: .utf8) else { return }

        connection.send(content: data, completion: .contentProcessed { error in
            if let error = error {
                print("โŒ UDP ์ „์†ก ์‹คํŒจ: \(error)")
            } else {
                print("๐Ÿ“ค UDP ์ „์†ก: \(message)")
            }
        })
    }

    // ๋ฐ์ดํ„ฐ๊ทธ๋žจ ์ˆ˜์‹ 
    func receiveDatagram() {
        connection?.receiveMessage { data, context, isComplete, error in
            if let error = error {
                print("โŒ UDP ์ˆ˜์‹  ์—๋Ÿฌ: \(error)")
                return
            }

            if let data = data, let message = String(data: data, encoding: .utf8) {
                print("๐Ÿ“ฅ UDP ์ˆ˜์‹ : \(message)")
            }

            // ๊ณ„์† ์ˆ˜์‹ 
            self.receiveDatagram()
        }
    }

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

๐Ÿ” 4. Bonjour ์„œ๋น„์Šค ๊ฒ€์ƒ‰

๋กœ์ปฌ ๋„คํŠธ์›Œํฌ์—์„œ Bonjour ์„œ๋น„์Šค๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

BonjourBrowser.swift โ€” Bonjour ๊ฒ€์ƒ‰
import Network

@Observable
class BonjourBrowser {
    private var browser: NWBrowser?
    var discoveredServices: [NWBrowser.Result] = []
    var isBrowsing = false

    // Bonjour ๊ฒ€์ƒ‰ ์‹œ์ž‘
    func startBrowsing(serviceType: String = "_http._tcp", domain: String = "local.") {
        // Browser ํŒŒ๋ผ๋ฏธํ„ฐ
        let parameters = NWParameters()
        parameters.includePeerToPeer = true

        // Bonjour ์„œ๋น„์Šค ํƒ€์ž…
        let bonjourType = NWBrowser.Descriptor.bonjour(type: serviceType, domain: domain)

        // Browser ์ƒ์„ฑ
        browser = NWBrowser(for: bonjourType, using: parameters)

        // ์ƒํƒœ ํ•ธ๋“ค๋Ÿฌ
        browser?.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                print("โœ… Bonjour ๊ฒ€์ƒ‰ ์‹œ์ž‘")
                self?.isBrowsing = true

            case .failed(let error):
                print("โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
                self?.isBrowsing = false

            case .cancelled:
                print("โŒ ๊ฒ€์ƒ‰ ์ทจ์†Œ๋จ")
                self?.isBrowsing = false

            default:
                break
            }
        }

        // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•ธ๋“ค๋Ÿฌ
        browser?.browseResultsChangedHandler = { [weak self] results, changes in
            print("๐Ÿ” ์„œ๋น„์Šค ๋ณ€๊ฒฝ: \(changes.count)๊ฐœ")

            for change in changes {
                switch change {
                case .added(let result):
                    print("  โž• ์„œ๋น„์Šค ๋ฐœ๊ฒฌ: \(result.endpoint)")
                    self?.discoveredServices.append(result)

                case .removed(let result):
                    print("  โž– ์„œ๋น„์Šค ์‚ฌ๋ผ์ง: \(result.endpoint)")
                    self?.discoveredServices.removeAll { $0.endpoint == result.endpoint }

                case .changed(old: let old, new: let new, flags: _):
                    print("  ๐Ÿ”„ ์„œ๋น„์Šค ๋ณ€๊ฒฝ: \(old.endpoint) -> \(new.endpoint)")

                case .identical:
                    break

                @unknown default:
                    break
                }
            }
        }

        // ๊ฒ€์ƒ‰ ์‹œ์ž‘
        browser?.start(queue: .global(qos: .userInitiated))
    }

    // ๊ฒ€์ƒ‰ ์ค‘์ง€
    func stopBrowsing() {
        browser?.cancel()
        browser = nil
        isBrowsing = false
        discoveredServices.removeAll()
        print("โน๏ธ ๊ฒ€์ƒ‰ ์ค‘์ง€")
    }
}

๐Ÿ“Š 5. ๋„คํŠธ์›Œํฌ ๊ฒฝ๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง

๋„คํŠธ์›Œํฌ ์ƒํƒœ์™€ ๊ฒฝ๋กœ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

NetworkMonitor.swift โ€” ๋„คํŠธ์›Œํฌ ๋ชจ๋‹ˆํ„ฐ๋ง
import Network

@Observable
class NetworkMonitor {
    private let monitor = NWPathMonitor()
    var isConnected = false
    var connectionType: NWInterface.InterfaceType?
    var isExpensive = false
    var isConstrained = false

    init() {
        startMonitoring()
    }

    // ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
    func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
            self?.isConnected = path.status == .satisfied
            self?.isExpensive = path.isExpensive
            self?.isConstrained = path.isConstrained

            // ์—ฐ๊ฒฐ ํƒ€์ž… ํ™•์ธ
            if path.usesInterfaceType(.wifi) {
                self?.connectionType = .wifi
                print("๐Ÿ“ก Wi-Fi ์—ฐ๊ฒฐ")
            } else if path.usesInterfaceType(.cellular) {
                self?.connectionType = .cellular
                print("๐Ÿ“ฑ ์…€๋ฃฐ๋Ÿฌ ์—ฐ๊ฒฐ")
            } else if path.usesInterfaceType(.wiredEthernet) {
                self?.connectionType = .wiredEthernet
                print("๐Ÿ”Œ ์œ ์„  ์ด๋”๋„ท ์—ฐ๊ฒฐ")
            } else {
                self?.connectionType = nil
                print("โŒ ์—ฐ๊ฒฐ ์—†์Œ")
            }

            // ์—ฐ๊ฒฐ ์ƒํƒœ
            switch path.status {
            case .satisfied:
                print("โœ… ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ๋จ")
            case .unsatisfied:
                print("โŒ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์•ˆ ๋จ")
            case .requiresConnection:
                print("โณ ์—ฐ๊ฒฐ ํ•„์š”")
            @unknown default:
                break
            }

            // ๋น„์šฉ ์ •๋ณด
            if path.isExpensive {
                print("๐Ÿ’ฐ ๋น„์šฉ์ด ๋ฐœ์ƒํ•˜๋Š” ๋„คํŠธ์›Œํฌ (์…€๋ฃฐ๋Ÿฌ ๋ฐ์ดํ„ฐ)")
            }

            if path.isConstrained {
                print("โš ๏ธ ์ œํ•œ๋œ ๋„คํŠธ์›Œํฌ (์ €์ „๋ ฅ ๋ชจ๋“œ)")
            }

            // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค
            print("  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค:")
            for interface in path.availableInterfaces {
                print("    - \(interface.type): \(interface.name)")
            }
        }

        monitor.start(queue: .global(qos: .userInitiated))
    }

    // ๋ชจ๋‹ˆํ„ฐ๋ง ์ค‘์ง€
    func stopMonitoring() {
        monitor.cancel()
    }

    deinit {
        stopMonitoring()
    }
}

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

๋„คํŠธ์›Œํฌ ๋ชจ๋‹ˆํ„ฐ๋ง UI๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

NetworkMonitorView.swift โ€” ๋„คํŠธ์›Œํฌ ์ƒํƒœ UI
import SwiftUI

struct NetworkMonitorView: View {
    @State private var networkMonitor = NetworkMonitor()

    var body: some View {
        NavigationStack {
            Form {
                Section("์—ฐ๊ฒฐ ์ƒํƒœ") {
                    HStack {
                        Circle()
                            .fill(networkMonitor.isConnected ? Color.green : Color.red)
                            .frame(width: 16, height: 16)

                        Text(networkMonitor.isConnected ? "์—ฐ๊ฒฐ๋จ" : "์—ฐ๊ฒฐ ์•ˆ ๋จ")
                            .font(.headline)
                    }

                    if let type = networkMonitor.connectionType {
                        HStack {
                            Text("์—ฐ๊ฒฐ ํƒ€์ž…")
                            Spacer()
                            Text(connectionTypeString(type))
                                .foregroundStyle(.secondary)
                        }
                    }
                }

                Section("๋„คํŠธ์›Œํฌ ํŠน์„ฑ") {
                    HStack {
                        Image(systemName: networkMonitor.isExpensive ? "checkmark.circle.fill" : "xmark.circle")
                            .foregroundStyle(networkMonitor.isExpensive ? .orange : .secondary)
                        Text("๋น„์šฉ ๋ฐœ์ƒ")
                        Spacer()
                        Text(networkMonitor.isExpensive ? "์˜ˆ" : "์•„๋‹ˆ์˜ค")
                            .foregroundStyle(.secondary)
                    }

                    HStack {
                        Image(systemName: networkMonitor.isConstrained ? "checkmark.circle.fill" : "xmark.circle")
                            .foregroundStyle(networkMonitor.isConstrained ? .orange : .secondary)
                        Text("์ œํ•œ๋จ")
                        Spacer()
                        Text(networkMonitor.isConstrained ? "์˜ˆ" : "์•„๋‹ˆ์˜ค")
                            .foregroundStyle(.secondary)
                    }
                }

                Section {
                    Text("๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            .navigationTitle("๋„คํŠธ์›Œํฌ ๋ชจ๋‹ˆํ„ฐ")
        }
    }

    private func connectionTypeString(_ type: NWInterface.InterfaceType) -> String {
        switch type {
        case .wifi: return "Wi-Fi"
        case .cellular: return "์…€๋ฃฐ๋Ÿฌ"
        case .wiredEthernet: return "์œ ์„  ์ด๋”๋„ท"
        case .loopback: return "๋กœ์ปฌ"
        case .other: return "๊ธฐํƒ€"
        @unknown default: return "์•Œ ์ˆ˜ ์—†์Œ"
        }
    }
}

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

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

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

โšก๏ธ ์„ฑ๋Šฅ ํŒ: NWConnection์€ ์ž๋™์œผ๋กœ ์ตœ์ ์˜ ๋„คํŠธ์›Œํฌ ๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. IPv4์™€ IPv6๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, Wi-Fi์™€ ์…€๋ฃฐ๋Ÿฌ ์‚ฌ์ด๋ฅผ ์›ํ™œํ•˜๊ฒŒ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. UDP๋Š” ์‹ ๋ขฐ์„ฑ์ด ๋‚ฎ์ง€๋งŒ ๋น ๋ฅด๋ฉฐ, TCP๋Š” ์‹ ๋ขฐ์„ฑ์ด ๋†’์ง€๋งŒ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.