๐ Network Framework
ํ๋์ ์ธ ์ ์์ค ๋คํธ์ํฌ ํต์ ํ๋ ์์ํฌ
โจ Network Framework๋?
Network๋ TCP, UDP, TLS, WebSocket ๋ฑ์ ํ๋กํ ์ฝ์ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ํต์ ์ ๊ตฌํํ๋ ํ๋์ ์ธ ์ ์์ค ํ๋ ์์ํฌ์ ๋๋ค. URLSession๋ณด๋ค ๋ ์ธ๋ฐํ ์ ์ด๊ฐ ๊ฐ๋ฅํ๋ฉฐ, NWConnection, NWListener๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ/์๋ฒ ํต์ ์ ๊ตฌํํ ์ ์์ต๋๋ค. Bonjour ์๋น์ค ๊ฒ์, IPv6 ์ง์, ๋คํธ์ํฌ ๊ฒฝ๋ก ๋ชจ๋ํฐ๋ง ๋ฑ์ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
๐ 1. NWConnection - TCP ํด๋ผ์ด์ธํธ
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 ์๋ฒ๋ฅผ ๋ง๋ค์ด ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ์ ์์ ํฉ๋๋ค.
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 ํ๋กํ ์ฝ๋ก ๋ฐ์ดํฐ๊ทธ๋จ์ ์ฃผ๊ณ ๋ฐ์ต๋๋ค.
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 ์๋น์ค๋ฅผ ๊ฒ์ํฉ๋๋ค.
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. ๋คํธ์ํฌ ๊ฒฝ๋ก ๋ชจ๋ํฐ๋ง
๋คํธ์ํฌ ์ํ์ ๊ฒฝ๋ก ๋ณํ๋ฅผ ๊ฐ์งํฉ๋๋ค.
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๋ฅผ ๊ตฌํํฉ๋๋ค.
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 ๊ฐ์ด๋๋ผ์ธ
- ๊ถํ ์ค์ : Info.plist์ NSLocalNetworkUsageDescription ์ถ๊ฐ
- Bonjour ์๋น์ค: NSBonjourServices์ ์ฌ์ฉํ ์๋น์ค ํ์ ๋ช ์
- TLS ์ฌ์ฉ: ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ์ก ์ TLS ํ์
- ๋คํธ์ํฌ ํจ์จ: isExpensive ์ฒดํฌํ์ฌ ๋์ฉ๋ ๋ค์ด๋ก๋ ์ต์ ํ
- ์๋ฌ ์ฒ๋ฆฌ: ์ฐ๊ฒฐ ์คํจ, ํ์์์์ ๋ํ ์ ์ ํ ์ฒ๋ฆฌ
๐ฏ ์ค์ ํ์ฉ
- ์ค์๊ฐ ํต์ : ์ฑํ , ๊ฒ์ ์๋ฒ ์ฐ๋
- IoT ํต์ : ์ค๋งํธ ํ ๊ธฐ๊ธฐ ์ ์ด
- ๋ก์ปฌ ์๋ฒ: ๋๋ฒ๊น ์ฉ ๋ก์ปฌ ์๋ฒ ๊ตฌํ
- ์๋น์ค ๊ฒ์: ๋ก์ปฌ ๋คํธ์ํฌ์ ํ๋ฆฐํฐ, ๋ฏธ๋์ด ์๋ฒ ์ฐพ๊ธฐ
- P2P ํต์ : ์ง์ ์ ์ธ ๊ธฐ๊ธฐ ๊ฐ ํต์
๐ ๋ ์์๋ณด๊ธฐ
NWConnection์ ์๋์ผ๋ก ์ต์ ์ ๋คํธ์ํฌ ๊ฒฝ๋ก๋ฅผ ์ ํํฉ๋๋ค. IPv4์ IPv6๋ฅผ ๋ชจ๋ ์ง์ํ๋ฉฐ, Wi-Fi์ ์
๋ฃฐ๋ฌ ์ฌ์ด๋ฅผ ์ํํ๊ฒ ์ ํํฉ๋๋ค. UDP๋ ์ ๋ขฐ์ฑ์ด ๋ฎ์ง๋ง ๋น ๋ฅด๋ฉฐ, TCP๋ ์ ๋ขฐ์ฑ์ด ๋์ง๋ง ์ค๋ฒํค๋๊ฐ ์์ต๋๋ค.