🌐 KO

🌐 Network Framework

⭐ Difficulty: ⭐⭐⭐ ⏱️ Est. Time: 2-3h πŸ“‚ System & Network

ν˜„λŒ€μ μΈ μ €μˆ˜μ€€ λ„€νŠΈμ›Œν¬ 톡신 ν”„λ ˆμž„μ›Œν¬

iOS 12+TLS 1.3 Supported

✨ Network Framework?

Network is a modern low-level framework for implementing network communication using TCP, UDP, TLS, WebSocket and more. It provides finer control than URLSession, using NWConnection and NWListener for client/server communication. It offers advanced features like Bonjour service discovery, IPv6 support, and network path monitoring.

πŸ’‘ Key Features: TCP/UDP Communication Β· TLS 1.3 Encryption Β· WebSocket Support Β· Bonjour Discovery Β· Network Path Monitoring Β· IPv4/IPv6 Β· Custom Protocols

πŸ”Œ 1. NWConnection β€” TCP Client

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 Discovery
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 β€” Network Monitoring
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 Integration

λ„€νŠΈμ›Œν¬ λͺ¨λ‹ˆν„°λ§ UI implementation.

NetworkMonitorView.swift β€” Network Status 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 Guidelines

🎯 Practical Usage

πŸ“š Learn More

⚑️ Performance Tips: NWConnection automatically selects the optimal network path. It supports both IPv4 and IPv6, seamlessly switching between Wi-Fi and cellular. UDP is fast but unreliable, while TCP is reliable but has overhead.

πŸ“Ž Apple Official Resources

πŸ“˜ Documentation 🎬 WWDC Sessions