반응형

WKWebView 실전 팁 모음 (iOS 15+)

iOS 앱에서 WKWebView를 쓸 때 자주 필요한 패턴과 주의사항을 한 번에 정리했습니다. 실제 프로젝트에서 바로 붙여 넣어 사용할 수 있는 코드 위주로 구성했습니다.

 

1. URL 변경 감지

 

1) KVO로 url 감지

class MyVC: UIViewController {
    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
        webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(WKWebView.url) {
            guard let url = webView.url?.absoluteString else { return }
            print("URL changed:", url)
        }
    }

    deinit {
        webView?.removeObserver(self, forKeyPath: "URL")
    }
}

 

2) NavigationDelegate로 감지

URL이 바뀌는 “의도”를 더 빨리 잡고 싶을 때는 다음을 권장합니다.

extension MyVC: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url {
            print("Request:", url.absoluteString)
        }
        decisionHandler(.allow)
    }
}

 

3) SPA(hash, pushState) 내부 이동까지 100% 잡기

SPA는 같은 문서 내에서 주소가 바뀌기도 합니다. 자바스크립트로 history/hashchange 를 후킹하여 메시지를 앱으로 보냅니다.

// Swift (App)
configuration.userContentController.add(self, name: "routeChange")

// Swift (handler)
extension MyVC: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "routeChange", let path = message.body as? String {
            print("SPA route:", path)
        }
    }
}

// JS to inject
const post = (p)=>window.webkit?.messageHandlers?.routeChange?.postMessage(p);
(function(){
  const _push = history.pushState;
  history.pushState = function(state, title, url){
    _push.apply(this, arguments);
    post(location.href);
  };
  window.addEventListener('popstate', ()=>post(location.href));
  window.addEventListener('hashchange', ()=>post(location.href));
  post(location.href);
})();

 

2. 링크 처리 (WKNavigationDelegate)

새 탭 링크, 외부 스킴, 앱 내부에서 그대로 열기 등 정책을 분기합니다.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

    guard let url = navigationAction.request.url else { decisionHandler(.cancel); return }

    // a) 외부 스킴은 시스템으로 넘기기
    let allowedSchemes = ["http", "https"]
    if let scheme = url.scheme, !allowedSchemes.contains(scheme) {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
        decisionHandler(.cancel)
        return
    }

    // b) 링크 클릭만 잡아서 수동 로드
    if navigationAction.navigationType == .linkActivated {
        webView.load(URLRequest(url: url))
        decisionHandler(.cancel) // 직접 로드했으므로 취소
        return
    }

    decisionHandler(.allow)
}

target="_blank" 처리까지 포함하려면 WKUIDelegate 를 함께 사용합니다.

extension MyVC: WKUIDelegate {
    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
                 for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
            webView.load(URLRequest(url: url))
        }
        return nil
    }
}

 

3. User-Agent 추가/교체

원본 UA 뒤에 태그를 붙이는 패턴.

webView.evaluateJavaScript("navigator.userAgent") { [weak self] (result, _) in
    guard let self, let ua = result as? String else { return }
    self.webView.customUserAgent = ua + " [MyApp_iOS]"
}

앱 전역으로 통일하려면 WKWebViewConfiguration.defaultWebpagePreferences 대신 customUserAgent 를 인스턴스별로 설정하거나, 네트워크 레벨에서 UA 헤더를 주입합니다.

 

4. 터치/인터랙션 막기

웹뷰의 하위 뷰를 순회하며 터치 차단.

webView.scrollView.subviews.forEach { $0.isUserInteractionEnabled = false }

특정 영역만 막고 싶다면 CSS로 pointer-events: none 을 주입하는 방법도 있습니다.

 

5. 바운스(bounce) 제어와 iOS 16 이슈 대처

일반 설정:

webView.scrollView.bounces = false
webView.scrollView.alwaysBounceVertical = false
webView.scrollView.alwaysBounceHorizontal = false

iOS 16.0~16.1 일부 환경에서 바운스가 비활성화되지 않는 리포트가 있었습니다. 다음을 함께 적용해 보세요.

webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.scrollView.clipsToBounds = true
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.showsHorizontalScrollIndicator = false
webView.isOpaque = false

웹 콘텐츠 자체 스크롤이 강제되는 경우에는 CSS에 html, body { overscroll-behavior: none; } 또는 touch-action: pan-y; 를 추가해 상호작용을 줄입니다.

직접 서브뷰를 재귀해서 막는 방법도 있습니다. (비추)

webView.allsubviews.forEach { ($0 as? UIScrollView)?.bounces = false }

extension UIView {
    var allsubviews: [UIView] {
        var all = [UIView]()
        func getSubview(view: UIView) {
            all.append(view)
            guard !view.subviews.isEmpty else { return }
            view.subviews.forEach { getSubview(view: $0) }
        }
        getSubview(view: self)
        return all
    }
}

 

6. 기본 탐색 콜백 정리 (WKNavigationDelegate)

func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { }
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { }
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { }
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
    // 메모리 압력 등으로 프로세스 종료됨. 필요 시 자동 재로딩
    webView.reload()
}

응답 헤더/상태코드에 따라 허용/차단하려면 decidePolicyFor navigationResponse 를 사용합니다.

 

7. 뒤로가기/앞으로가기 제스처

webView.allowsBackForwardNavigationGestures = true // 제스처 허용
if webView.canGoBack { webView.goBack() }
if webView.canGoForward { webView.goForward() }

 

8. 캐시/스토리지 관리

 

1) 디스크/메모리 캐시만 삭제(로그인 유지)

extension WKWebView {
    func clearCache() {
        let types: Set<String> = [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]
        WKWebsiteDataStore.default().removeData(ofTypes: types,
            modifiedSince: Date(timeIntervalSince1970: 0), completionHandler: {})
    }
}

 

2) 모든 웹사이트 데이터 삭제(히스토리/쿠키 포함)

WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
    WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
                                            for: records, completionHandler: {})
}

 

9. 초기화와 오토레이아웃

let config = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: config)
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
    webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
let req = URLRequest(url: URL(string: "https://www.google.com")!)
webView.load(req)

 

10. 메시지 브리지로 앱↔웹 통신

// Swift
let ucc = webView.configuration.userContentController
ucc.add(self, name: "bridge")
webView.evaluateJavaScript("window.BRIDGE_READY = true;")

// JS (페이지에 주입)
window.MyApp = {
  post: (data) => window.webkit.messageHandlers.bridge.postMessage(data)
}

 

11. 파일 업로드/다운로드 주의점

  • 카메라/포토 접근은 NSCameraUsageDescription, NSPhotoLibraryUsageDescription 키 필요
  • 다운로드는 WKDownloadDelegate (iOS 14+) 활용
  • iOS 15+에서 defaultWebpagePreferences.allowsContentJavaScript = true기본값 확인

 

12. 쿠키/세션

SameSite, HttpOnly, Secure 등 설정에 따라 앱 내 요청과 브라우저 동작이 달라질 수 있습니다. 필요 시 HTTPCookieStorage.shared  WKHTTPCookieStore 동기화를 고려합니다.

let store = webView.configuration.websiteDataStore.httpCookieStore
store.getAllCookies { cookies in
    for c in cookies { print(c.name, c.value) }
}

 

13. 디버깅 팁

  • Xcode의 “Web Inspector” 활성화: iOS 설정 > Safari > 고급 > Web Inspector
  • 콘솔 로그 수집: window.console.log 를 후킹해 messageHandlers로 전달
  • 크래시/메모리: webViewWebContentProcessDidTerminate 모니터링

 

 

 

반응형
Posted by 까칠코더
,