WKWebView를 사용해 웹페이지 가져오기

회사에서 웹뷰를 활용한 챗봇을 만드는 프로젝트가 시작해 Swift에서 웹뷰를 사용하는 방식에 대해서 기록으로 남겨두려고 한다. 요즘 많은 프로젝트에서 웹뷰를 활용한 개발이 많이 이뤄지고 있는데 사실 제대로된 웹뷰를 사용해보지 않아 이번 기회에 공부해보려고 한다.

iOS에서 웹뷰를 사용하기 위해서는 주로 WKWebView를 사용한다. 하지만 나는 SwiftUI로 개발을 진행하고 있기 때문에 이 SwiftUI에서는 WKWebView를 사용할 수 없어 UIViewRepresentable를 활용해 UIKit에서 가져와 사용하는 방식으로 구현하려고 한다.

 

UIViewRepresentable 사용

UIViewRepresentable은 UIKit의 뷰를 SwiftUI에서 사용할 수 있도록 해주는 프로토콜이다. WKWebView를 SwiftUI에서 사용하려면 UIViewRepresentable를 구현해서 가져와야만 사용할 수 있다.

 

코드를 통해 알아보자

import SwiftUI
import WebKit

struct ChatWebView: UIViewRepresentable {
    let urlString: String
    
    // 1. UIView 생성
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator // Navigation 이벤트 처리
        return webView
    }
    
    // 2. SwiftUI 상태에 따라 UIView 업데이트
    func updateUIView(_ uiView: WKWebView, context: Context) {
        if let url = URL(string: urlString) {
            let request = URLRequest(url: url)
            uiView.load(request)
        }
    }
    
    // 3. Coordinator 생성
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // 4. Coordinator 클래스 구현
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: ChatWebView
        
        init(_ parent: ChatWebView) {
            self.parent = parent
        }
        
        // Navigation 이벤트 처리 (예: 로딩 완료 후 동작)
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            print("WebView did finish loading")
        }
    }
}

UIViewRepresentable를 채택하는 View를 만들고 makeUIView(context:) 를 활용해 WKWebView 인스턴스를 생성하고 초기화 한다.

 

그 다음 updateUIView(_:context:): 에서 urlString 값에 따라 URL 요청을 생성하고 웹뷰를 업데이트한다. makeCoordinator와 Coordinator가 보이는데 Coordinator는 SwiftUI와 UIKit 간 이벤트 처리 및 상호작용을 위해 사용된다.

 

추가적으로 웹뷰를 활용해서 사용할 수 있는 몇가지를 알아보자

 

먼저 첫번쨰로 JavaScript를 실행할 수 있다. evaluateJavaScript(_: completionHandler) 를 사용하면 JS 코드를 실행해 웹콘텐츠를 동적으로 조작할 수 있다.

let jsCode = "document.getElementById('header').style.display='none';"
webView.evaluateJavaScript(jsCode) { (result, error) in
    if let error = error {
        print("JavaScript Error: \\(error.localizedDescription)")
    } else {
        print("JavaScript executed successfully")
    }
}

특정 HTML 요소를 숨기거나 수정할 수 있고 웹페이지에서 값을 읽어올 수 있다. 웹앱과 동적 데이터 교환도 가능하다.

 

두번째로 JavaScript ← → Native 통신이 가능하다.

WKUserContentController를 사용해 JS 메세지를 Swift로 전달할 수 있다.

class Coordinator: NSObject, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "nativeHandler", let messageBody = message.body as? String {
            print("Received message: \\(messageBody)")
        }
    }
}

세번째로 파일 다운로드가 가능하다. 이 파일 다운로드는 WKWebView에서 기본적으로 지원하는 것은 아니지만, URL을 직접 처리해 다운로드할 수 있도록 구현할 수 있다.

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    if let response = navigationResponse.response as? HTTPURLResponse,
       response.mimeType == "application/pdf" {
        downloadFile(from: response.url)
        decisionHandler(.cancel)
    } else {
        decisionHandler(.allow)
    }
}

func downloadFile(from url: URL?) {
    guard let url = url else { return }
    URLSession.shared.downloadTask(with: url) { localURL, response, error in
        if let localURL = localURL {
            // 파일 저장 처리
            print("File downloaded to: \\(localURL)")
        }
    }.resume()
}

또한 로딩 상태를 표시해 웹페이지가 로딩 상태일 경우 로딩 인디케이터를 표시할 수 있다.

@Binding var isLoading: Bool

func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
    isLoading = true
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    isLoading = false
}

ZStack {
    ChatWebView(urlString: "<https://example.com>", isLoading: $isLoading)
    if isLoading {
        ProgressView("Loading...")
    }
}

아무래도 웹에서 가져와서 처리하기 때문에 자주는 아니지만 꼭 필요할 순간이 있다고 생각한다. 지금 우리 프로젝트에서도 스트림릿을 사용해 챗봇을 가져오다 보면 다소 느린 경향이 있어 상기 내용을 참고해 이용자 경험을 높힐 수 있도록 해야할 것 같다.

 

또한 현재 보고 있는 웹페이지를 PDF로 저장할 수도 있다.

func createPDF(webView: WKWebView) {
    let config = WKPDFConfiguration()
    webView.createPDF(configuration: config) { result in
        switch result {
        case .success(let data):
            let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
            try? data.write(to: url)
            print("PDF saved to \\(url)")
        case .failure(let error):
            print("Error generating PDF: \\(error)")
        }
    }
}

이 외에 여러가지 다양한 기능을 아주 많이 가지고 있는 웹뷰를 알아보았다. 지금까지는 가져와서 보여주는 방식까지만 적용하고 있기 때문에 추후에 좋은 기회가 있다면 다양한 웹뷰를 활용해 앱을 구성하는 방식도 도전해봐야겠다!!!!