[ios]네트워크 상태 모니터링 구현 및 WebViewKit 적용
문제 정의
네트워크 연결 상태를 감지하고 사용자에게 적절한 피드백을 제공하며, 문제를 해결할 수 있는 UI와 로직을 구현 목표
목표
1. 네트워크 상태 모니터링 구현
• NetworkMonitor를 활용하여 네트워크 상태 변화를 실시간으로 감지.
• 네트워크가 끊어졌을 때 알림을 보내고 연결 상태를 UI에 반영.
2. 에러 화면 제공
• 네트워크 연결 문제가 발생했을 때 NetworkErrorView를 통해 사용자에게 상태를 알리고 해결 방안을 제공.
3. 사용자 동작 지원
• “다시 시도하기” 버튼을 통해 WebView를 다시 로드하거나 초기 화면으로 복귀.
• “웹으로 보기” 버튼을 통해 Safari 또는 Chrome에서 URL을 열어 문제를 해결.
구현 과정
1. 네트워크 상태 모니터링
설명:
Apple의 Network 프레임워크를 사용하여 네트워크 상태를 감지합니다. NWPathMonitor를 활용하여 네트워크 연결 상태 변화를 감지하고, 상태에 따라 알림(NotificationCenter)을 전송합니다.
코드 및 주석
import Network
class NetworkMonitor {
static let shared = NetworkMonitor() // 싱글턴으로 네트워크 상태를 관리
private let monitor = NWPathMonitor() // 네트워크 상태를 감지하는 객체
private let queue = DispatchQueue.global(qos: .background) // 백그라운드에서 네트워크 상태를 감지
var isConnected: Bool = true // 네트워크 연결 상태를 저장
private init() {}
// 네트워크 상태 감지를 시작하는 메서드
func startMonitoring() {
monitor.start(queue: queue) // 감지 시작
monitor.pathUpdateHandler = { [weak self] path in
let isConnected = path.status == .satisfied // 연결 상태 확인
if isConnected != self?.isConnected {
self?.isConnected = isConnected
DispatchQueue.main.async { // UI 업데이트를 위해 메인 스레드에서 실행
if isConnected {
NotificationCenter.default.post(name: .networkConnected, object: nil)
} else {
NotificationCenter.default.post(name: .networkDisconnected, object: nil)
}
}
}
}
}
// 네트워크 상태 감지를 중지하는 메서드
func stopMonitoring() {
monitor.cancel()
}
}
// 네트워크 연결 상태에 대한 알림 이름 정의
extension Notification.Name {
static let networkDisconnected = Notification.Name("networkDisconnected")
static let networkConnected = Notification.Name("networkConnected")
}
2. 네트워크 에러 화면 구현
설명:
UIView를 상속받아 NetworkErrorView를 구현하였습니다. 이 뷰는 네트워크 연결이 끊어졌을 때 사용자에게 메시지와 버튼을 제공하여 문제를 해결하도록 돕습니다.
구성 요소
1. UILabel: 에러 메시지를 표시.
2. UIButton:
• “다시 시도하기” 버튼: WebView를 재로드하거나 초기 화면으로 복귀.
• “웹으로 보기” 버튼: Safari 또는 Chrome으로 URL 열기.
코드 및 주석
import UIKit
class NetworkErrorView: UIView {
// 에러 메시지를 표시하는 레이블
private let errorLabel: UILabel = {
let label = UILabel()
label.text = "네트워크 연결이 원활하지 않습니다."
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 18)
return label
}()
// 다시 시도하기 버튼
private let retryButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("다시 시도하기", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
return button
}()
// 웹으로 보기 버튼
private let webViewButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("웹으로 보기", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()
// 초기화
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupActions()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
setupActions()
}
// UI를 구성하는 메서드
private func setupUI() {
self.addSubview(errorLabel)
self.addSubview(retryButton)
self.addSubview(webViewButton)
errorLabel.translatesAutoresizingMaskIntoConstraints = false
retryButton.translatesAutoresizingMaskIntoConstraints = false
webViewButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
errorLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
errorLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 200),
retryButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
retryButton.topAnchor.constraint(equalTo: errorLabel.bottomAnchor, constant: 20),
webViewButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
webViewButton.topAnchor.constraint(equalTo: retryButton.bottomAnchor, constant: 10)
])
}
// 버튼 액션 설정
private func setupActions() {
retryButton.addTarget(self, action: #selector(retryButtonTapped), for: .touchUpInside)
webViewButton.addTarget(self, action: #selector(webViewButtonTapped), for: .touchUpInside)
}
// 다시 시도하기 버튼이 눌렸을 때 호출
@objc private func retryButtonTapped() {
if let parentViewController = self.findParentViewController() as? HomeViewController {
parentViewController.reloadWebView()
self.removeFromSuperview()
}
}
// 웹으로 보기 버튼이 눌렸을 때 호출
@objc private func webViewButtonTapped() {
if let url = URL(string: "https://www.example.com") {
UIApplication.shared.open(url, options: [:])
}
}
// 부모 ViewController를 찾는 메서드
private func findParentViewController() -> UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder?.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
3. HomeViewController에서 WebView 처리
설명:
네트워크 상태 변화에 따라 WebView를 다시 로드하거나 초기화하며, 네트워크 상태를 감지해 NetworkErrorView를 표시하고 제거합니다.
코드 및 주석
import UIKit
@preconcurrency import WebKit
class HomeViewController: UIViewController {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
loadWebPage()
setupNetworkMonitoring()
}
// WebView 초기화
private func setupWebView() {
webView.navigationDelegate = self
}
// 초기 웹 페이지 로드
private func loadWebPage() {
let url = URL(string: "https://www.example.com")!
webView.load(URLRequest(url: url))
}
// 네트워크 상태 모니터링 설정
private func setupNetworkMonitoring() {
NotificationCenter.default.addObserver(self,
selector: #selector(showNetworkErrorView),
name: .networkDisconnected,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(removeNetworkErrorView),
name: .networkConnected,
object: nil)
NetworkMonitor.shared.startMonitoring()
}
// 네트워크 에러 화면 표시
@objc private func showNetworkErrorView() {
let errorView = NetworkErrorView(frame: self.view.bounds)
self.view.addSubview(errorView)
}
// 네트워크 에러 화면 제거
@objc private func removeNetworkErrorView() {
self.view.subviews.forEach {
if $0 is NetworkErrorView {
$0.removeFromSuperview()
}
}
}
// WebView 다시 로드
func reloadWebView() {
if let currentURL = webView.url {
webView.load(URLRequest(url: currentURL))
} else {
let url = URL(string: "https://www.example.com")!
webView.load(URLRequest(url: url))
}
}
}
문제 및 해결
1. NetworkErrorView 중복 추가 문제
• 문제: 네트워크 연결 끊김 시 NetworkErrorView가 중복으로 추가되는 현상 발생.
• 해결: view.subviews.contains를 사용하여 중복 추가를 방지.
2. WebView 로드 실패 시 초기 URL 처리
• 문제: WebView의 URL이 nil일 경우 로드 실패.
• 해결: reloadWebView에서 URL이 nil이면 초기 URL을 설정.
3. Safari와 Chrome 연동 문제
• 문제: “웹으로 보기” 버튼에서 Chrome이 설치되어 있어도 Safari에서 열리는 현상.
• 해결: Info.plist에 googlechrome URL 스키마를 추가.