← Lab
Swift NWPathMonitor
스터디2025.07.09
- SwiftUI
- Swift
- Combine
- DispatchQueue
NWPathMonitor

NWPathMonitor는 네트워크 변경 사항을 모니터링하고 대응할 수 있도록 해주는 관찰자
import Network
let monitor = NWPathMonitor() // 네트워크 상태 모니터링을 위한 모니터를 인스턴스화
// 위처럼 단순하게 해도 되지만 특정 상황을 모니터링 하고 싶다면 생성자에 타입 집어넣기
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
let monitor = NWPathMonitor(prohibitedInterfaceTypes: [.wifi, .cellular])
monitor.start(queue: .global()) // 백그라운드 큐에서 모니터링 후 업데이트 사항 전달받기
// pathUpdateHandler는 네트워크 연결 상태에 따라 핸들링해줌
self.monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("인터넷 연결 OK")
} else {
print("인터넷 연결 끊김")
}
// 한 번 pathUpdateHandler를 통해 상태 업데이트를 받게 되면 모니터 인스턴스를 통해 현재 연결상태 확인 가능
if monitor.currentPath.isExpensive {
// 셀룰러, 핫스팟으로 연결될 경우
}
if monitor.currentPath.isConstrained {
// 데이터 전송속도 400kb이하 (저전송 데이터 모드)
}
// 간단하게 해주고 싶다면 아래처럼 활용하면 가능. onAppear 부분에서 모니터를 start 해주고
// onDisappear 부분에서 모니터 cencel을 통해 모니터 종료

import SwiftUI
import Network
import Combine
// MARK: - NetworkMonitor
/// 네트워크 연결 상태를 실시간으로 모니터링하는 ObservableObject
/// NWPathMonitor를 사용하여 네트워크 상태 변화를 감지하고 UI에 반영
final class NetworkMonitor: ObservableObject {
/// 현재 네트워크 연결 상태 (true: 연결됨, false: 연결 안됨)
/// @Published로 선언하여 값이 변경되면 자동으로 View 업데이트
@Published var isConnected: Bool = true
/// Apple의 Network 프레임워크에서 제공하는 네트워크 경로 모니터
/// 네트워크 인터페이스(WiFi, Cellular 등)의 상태 변화를 감지
private let monitor = NWPathMonitor()
/// 네트워크 모니터링을 실행할 별도의 백그라운드 큐
/// 메인 스레드를 블로킹하지 않기 위해 별도 큐 사용
private let queue = DispatchQueue(label: "NetworkMonitor")
init() {
startMonitoring()
}
/// 네트워크 모니터링 시작
private func startMonitoring() {
// 네트워크 경로 상태가 변경될 때마다 호출되는 핸들러 설정
monitor.pathUpdateHandler = { [weak self] path in
// UI 업데이트는 반드시 메인 스레드에서 실행
DispatchQueue.main.async {
// path.status가 .satisfied면 네트워크 연결됨
// .unsatisfied면 연결 안됨, .requiresConnection은 연결 시도 중
self?.isConnected = path.status == .satisfied
}
}
// 지정한 큐에서 모니터링 시작
monitor.start(queue: queue)
}
/// 네트워크 모니터링 중지
/// 메모리 누수 방지를 위해 객체 해제 시 반드시 호출 필요
func stopMonitoring() {
monitor.cancel()
}
deinit {
stopMonitoring()
}
}
// MARK: - NetworkStatusBorderModifier
/// 네트워크 연결 상태에 따라 화면 테두리 색상을 변경하는 ViewModifier
/// 연결됨: 초록색, 연결 안됨: 빨간색
struct NetworkStatusBorderModifier: ViewModifier {
/// App에서 주입한 NetworkMonitor를 EnvironmentObject로 받음
/// 모든 하위 View에서 동일한 NetworkMonitor 인스턴스 공유
@EnvironmentObject var networkMonitor: NetworkMonitor
func body(content: Content) -> some View {
content
// overlay를 사용하여 기존 View 위에 테두리 추가
.overlay(
// RoundedRectangle로 테두리 모양 정의
RoundedRectangle(cornerRadius: 0)
// stroke로 테두리만 그리기 (내부는 투명)
// lineWidth: 테두리 두께
.stroke(
// 네트워크 상태에 따라 색상 변경
networkMonitor.isConnected ? Color.green : Color.red,
lineWidth: 4
)
)
// 애니메이션 효과 추가: 색상 변경 시 부드럽게 전환
.animation(.easeInOut(duration: 0.3), value: networkMonitor.isConnected)
}
}
// MARK: - View Extension
/// View에 쉽게 적용할 수 있도록 편의 메서드 제공
extension View {
/// 네트워크 상태 테두리를 View에 적용
/// 사용법: SomeView().networkStatusBorder()
func networkStatusBorder() -> some View {
self.modifier(NetworkStatusBorderModifier())
}
}
// MARK: - 적용된 IssueTrackerApp
@main
struct IssueTrackerApp: App {
private let container = DIContainer.shared
/// 인증 상태를 관리하는 Provider
@StateObject private var authProvider: AuthProvider
/// 화면 전환을 관리하는 Coordinator
@StateObject private var loginCoordinator = LoginCoordinator()
/// 네트워크 상태를 모니터링하는 Monitor
/// App 생명주기와 함께 생성되고 소멸됨
@StateObject private var networkMonitor = NetworkMonitor()
init() {
// 임시: 직접 생성하여 주입 (추후 Assembler 패턴으로 개선 예정)
let keychainManager = KeychainManager()
let authProvider = AuthProvider(keychainManager: keychainManager)
self._authProvider = StateObject(wrappedValue: authProvider)
// 의존성 등록
registerAuthDependencies(authProvider: authProvider)
registerLoginDependencies()
registerIssueListDependencies()
}
var body: some Scene {
WindowGroup {
// Group으로 감싸서 if-else 전체에 modifier 적용 가능하게 함
Group {
if authProvider.isLoggedIn {
// 로그인된 상태: IssueList 화면 표시
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.issueListView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
} else {
// 로그인 안 된 상태: Login 화면 표시
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.loginView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
}
}
// AuthProvider를 환경에 주입 (하위 모든 View에서 접근 가능)
.environmentObject(authProvider)
// NetworkMonitor를 환경에 주입 (Modifier에서 사용)
.environmentObject(networkMonitor)
// 네트워크 상태 테두리 적용 (전체 화면에 적용됨)
.networkStatusBorder()
}
}
// MARK: - 인증 의존성
private func registerAuthDependencies(authProvider: AuthProvider) {
container.register(AuthProviderProtocol.self, lifetime: .singleton) { authProvider }
}
// MARK: - LoginView 의존성
private func registerLoginDependencies() {
container.register(LoginRepositoryProtocol.self, lifetime: .singleton) {
LoginRepository(
requester: container.resolve(RequesterProtocol.self),
keychainManager: container.resolve(KeychainManagerProtocol.self)
)
}
container.register(LoginUseCaseProtocol.self, lifetime: .singleton) {
LoginUseCase(repository: container.resolve(LoginRepositoryProtocol.self))
}
container.register(LoginViewModel.self, lifetime: .transient) {
LoginViewModel(
loginUseCase: container.resolve(LoginUseCaseProtocol.self),
authProvider: container.resolve(AuthProviderProtocol.self)
)
}
}
// MARK: - IssueListView 의존성
private func registerIssueListDependencies() {
container.register(IssueListViewModel.self, lifetime: .transient) {
IssueListViewModel(authProvider: container.resolve(AuthProviderProtocol.self))
}
}
}
// MARK: - 테스트용 Preview
#Preview {
ContentView()
.environmentObject(NetworkMonitor())
.networkStatusBorder()
}
struct ContentView: View {
var body: some View {
VStack {
Text("네트워크 상태 테스트")
.font(.largeTitle)
Text("WiFi나 비행기 모드를 켜고 끄면")
Text("화면 테두리 색상이 변경됩니다")
}
.padding()
}
}
import SwiftUI
import Network
import Combine
// MARK: - Notification Names
extension Notification.Name {
/// 네트워크 연결 상태가 변경되었을 때 발송되는 Notification
static let networkStatusDidChange = Notification.Name("networkStatusDidChange")
}
// MARK: - NetworkMonitor
/// 네트워크 연결 상태를 실시간으로 모니터링하고 NotificationCenter를 통해 알림
/// NWPathMonitor를 사용하여 네트워크 상태 변화를 감지하고 Notification 발송
final class NetworkMonitor {
/// 싱글톤 인스턴스
static let shared = NetworkMonitor()
/// 현재 네트워크 연결 상태 (true: 연결됨, false: 연결 안됨)
private(set) var isConnected: Bool = true
/// Apple의 Network 프레임워크에서 제공하는 네트워크 경로 모니터
private let monitor = NWPathMonitor()
/// 네트워크 모니터링을 실행할 별도의 백그라운드 큐
private let queue = DispatchQueue(label: "NetworkMonitor")
private init() {
startMonitoring()
}
/// 네트워크 모니터링 시작
private func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let newStatus = path.status == .satisfied
// 상태가 변경된 경우에만 Notification 발송
if self.isConnected != newStatus {
self.isConnected = newStatus
// NotificationCenter를 통해 상태 변경 알림
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .networkStatusDidChange,
object: nil,
userInfo: ["isConnected": newStatus]
)
}
}
}
monitor.start(queue: queue)
}
/// 네트워크 모니터링 중지
func stopMonitoring() {
monitor.cancel()
}
deinit {
stopMonitoring()
}
}
// MARK: - NetworkStatusBorderModifier
/// 네트워크 연결 상태에 따라 화면 테두리 색상을 변경하는 ViewModifier
/// NotificationCenter를 통해 상태 변화를 감지
struct NetworkStatusBorderModifier: ViewModifier {
/// 현재 네트워크 연결 상태
@State private var isConnected: Bool = NetworkMonitor.shared.isConnected
func body(content: Content) -> some View {
content
.overlay(
RoundedRectangle(cornerRadius: 0)
.stroke(
isConnected ? Color.green : Color.red,
lineWidth: 4
)
)
.animation(.easeInOut(duration: 0.3), value: isConnected)
// View가 나타날 때 Notification 구독
.onAppear {
subscribeToNetworkChanges()
}
}
/// NotificationCenter를 통해 네트워크 상태 변화 구독
private func subscribeToNetworkChanges() {
NotificationCenter.default.addObserver(
forName: .networkStatusDidChange,
object: nil,
queue: .main
) { notification in
if let isConnected = notification.userInfo?["isConnected"] as? Bool {
self.isConnected = isConnected
}
}
}
}
// MARK: - View Extension
extension View {
/// 네트워크 상태 테두리를 View에 적용
func networkStatusBorder() -> some View {
self.modifier(NetworkStatusBorderModifier())
}
}
// MARK: - 적용된 IssueTrackerApp
@main
struct IssueTrackerApp: App {
private let container = DIContainer.shared
@StateObject private var authProvider: AuthProvider
@StateObject private var loginCoordinator = LoginCoordinator()
init() {
// NetworkMonitor 초기화 (싱글톤이지만 명시적으로 시작)
_ = NetworkMonitor.shared
// 임시: 직접 생성하여 주입
let keychainManager = KeychainManager()
let authProvider = AuthProvider(keychainManager: keychainManager)
self._authProvider = StateObject(wrappedValue: authProvider)
// 의존성 등록
registerAuthDependencies(authProvider: authProvider)
registerLoginDependencies()
registerIssueListDependencies()
}
var body: some Scene {
WindowGroup {
Group {
if authProvider.isLoggedIn {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.issueListView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
} else {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.loginView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
}
}
.environmentObject(authProvider)
// 네트워크 상태 테두리 적용
.networkStatusBorder()
}
}
// MARK: - 의존성 등록
private func registerAuthDependencies(authProvider: AuthProvider) {
container.register(AuthProviderProtocol.self, lifetime: .singleton) { authProvider }
}
private func registerLoginDependencies() {
container.register(LoginRepositoryProtocol.self, lifetime: .singleton) {
LoginRepository(
requester: container.resolve(RequesterProtocol.self),
keychainManager: container.resolve(KeychainManagerProtocol.self)
)
}
container.register(LoginUseCaseProtocol.self, lifetime: .singleton) {
LoginUseCase(repository: container.resolve(LoginRepositoryProtocol.self))
}
container.register(LoginViewModel.self, lifetime: .transient) {
LoginViewModel(
loginUseCase: container.resolve(LoginUseCaseProtocol.self),
authProvider: container.resolve(AuthProviderProtocol.self)
)
}
}
private func registerIssueListDependencies() {
container.register(IssueListViewModel.self, lifetime: .transient) {
IssueListViewModel(authProvider: container.resolve(AuthProviderProtocol.self))
}
}
}
// MARK: - 테스트용 Preview
#Preview {
ContentView()
.networkStatusBorder()
}
struct ContentView: View {
var body: some View {
VStack {
Text("네트워크 상태 테스트")
.font(.largeTitle)
Text("WiFi나 비행기 모드를 켜고 끄면")
Text("화면 테두리 색상이 변경됩니다")
}
.padding()
}
}
// MARK: - 다른 곳에서 네트워크 상태를 구독하는 예제
class SomeViewModel: ObservableObject {
@Published var isNetworkConnected: Bool = NetworkMonitor.shared.isConnected
private var cancellables = Set<AnyCancellable>()
init() {
// NotificationCenter를 Combine Publisher로 변환하여 구독
NotificationCenter.default.publisher(for: .networkStatusDidChange)
.compactMap { $0.userInfo?["isConnected"] as? Bool }
.assign(to: \.isNetworkConnected, on: self)
.store(in: &cancellables)
}
}