[iOS] WKWebView 클릭 동작 최적화하기

sun02·2026년 1월 19일

iOS

목록 보기
32/32

iOS에서 WKWebView로 그린 웹 페이지 내부에서 클릭을 하는 경우
그 클릭은 디폴트로 웹뷰 내부에서만 동작합니다.

가령 유저가 웹뷰의 특정 버튼을 클릭하여 외부(사파리 또는 다른 앱)로 이동해야하는 경우에도
클릭에 대한 동작을 별도로 정의해 주지 않으면 외부로 이동하지 않고
해당 웹 페이지가 웹뷰 내에서 열리게 됩니다.

따라서, 인앱 웹뷰를 클릭하여 외부로 이동이 필요한 경우 어떻게 설정하면 되는지에 대해 작성해보려 합니다.



1. createWebViewWith

WKWebView에서 “새 창(window.open, target=_blank)”을 어떻게 처리할지 결정하는 메서드

func webView(
          _ webView: WKWebView,
          createWebViewWith configuration: WKWebViewConfiguration,
          for navigationAction: WKNavigationAction,
          windowFeatures: WKWindowFeatures) -> WKWebView?

웹에서 아래와 같은 기능을 요청했을 때 호출 되며

새 창이 필요하기 때문에
어디에 띄울 지 정해진 프레임이 없습니다.

navigationAction.targetFrame == nil 

또한 이러한 이동이 어떤행위로 발생했는지를 확인하기 위해 WKNavigationType을 확인합니다.

if navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other { 
}
  • linkActivated : 사용자가 직접 링크 클릭
  • other : location.href = , JS로 링크 트리거 등

따라서, 사용자가 특정 링크를 클릭하여 새 창(외부)으로 이동해야하는 경우 아래와 같이 작성합니다.

func webView(
          _ webView: WKWebView,
          createWebViewWith configuration: WKWebViewConfiguration,
          for navigationAction: WKNavigationAction,
          windowFeatures: WKWindowFeatures) -> WKWebView? {
              
              let isPopup = (navigationAction.targetFrame == nil)
              
              if (navigationAction.navigationType == .other || navigationAction.navigationType == .linkActivated) && isPopup {
                  if let url = navigationAction.request.url {
                      lastHandledPopupURL = url
                      UIApplication.shared.open(url)
                  }
              }
              
              return nil
      }


2. decidePolicyFor navigationAction

WKWebView에서 “이 URL 이동을 허용할지 말지”를 결정하는 메서드 입니다.

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

createWebViewWith에서 해당 URL을 외부로 이동시켜주었다면
decidePolicyFor에서는 해당 url로 더 이상 이동되지 않게끔 처리해주어야합니다.

처리해주지 않는다면, 외부로 이동한 후 다시 앱으로 돌아왔을 때 webView가 해당url을 띄우고 있게 됩니다.

func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        
        if shouldHandleExternalNavigation(for: navigationAction) {
            return decisionHandler(.cancel)
        }
    
        decisionHandler(.allow)
    }
    
    func shouldHandleExternalNavigation(for navigationAction: WKNavigationAction) -> Bool {
    
        guard let targetURL = navigationAction.request.url else {
          return false
        }
                
        // createWebViewWith에서 이미 처리한 팝업 URL이면 내부 로딩 막기
        if let handledURL = lastHandledPopupURL, handledURL.absoluteString == targetURL.absoluteString {
            lastHandledPopupURL = nil
            return true
        }
        
        return false
      }

따라서 저는 lastHandledPopupURL 변수를 생성하고
외부로 이동할 때 lastHandledPopupURL에 그 값을 넣고

decidePolicyFor에서 lastHandledPopupURL 값이 존재하고
현재 다뤄야하는 url과 동일하다면 해당 url로의 이동을 막도록 구현하였습니다.



3. 전체 코드

import UIKit
import WebKit

class ViewController: UIViewController {
    
    var webView: WKWebView!
    var lastHandledPopupURL: URL?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        
        setupWebView()
        setLayout()
        self.webView.load(URLRequest(url: webURL))
    }


    func setupWebView() {
        let controller = WKUserContentController()
        let config = WKWebViewConfiguration()
        let preferences = WKPreferences()
        
        preferences.javaScriptCanOpenWindowsAutomatically = false
        config.userContentController = controller
        config.preferences = preferences
        
        self.webView = WKWebView(frame: .zero, configuration: config)
        webView.navigationDelegate = self
        webView.uiDelegate = self
        webView.allowsBackForwardNavigationGestures = true
        
        if #available(iOS 16.4, *) {
            webView.isInspectable = true
        } else {
            // Fallback on earlier versions
        }
    }
    
    
    func setLayout() {
        [webView].forEach {
            self.view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        let safeArea = self.view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: safeArea.topAnchor),
            webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
        ])
    }
}



// MARK: - WKNavigationDelegate
extension ViewController: WKNavigationDelegate {
    
    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        
        if shouldHandleExternalNavigation(for: navigationAction) {
            return decisionHandler(.cancel)
        }
    
        decisionHandler(.allow)
    }
    
    func shouldHandleExternalNavigation(for navigationAction: WKNavigationAction) -> Bool {
    
        guard let targetURL = navigationAction.request.url else {
          return false
        }
                
        // createWebViewWith에서 이미 처리한 팝업 URL이면 내부 로딩 막기
        if let handledURL = lastHandledPopupURL, handledURL.absoluteString == targetURL.absoluteString {
            lastHandledPopupURL = nil
            return true
        }
        
        return false
      }
}


// MARK: - WKUIDelegate
extension ViewController: WKUIDelegate {
    
    func webView(
          _ webView: WKWebView,
          createWebViewWith configuration: WKWebViewConfiguration,
          for navigationAction: WKNavigationAction,
          windowFeatures: WKWindowFeatures) -> WKWebView? {
              
              let isPopup = (navigationAction.targetFrame == nil)
              
              if (navigationAction.navigationType == .other || navigationAction.navigationType == .linkActivated) && isPopup {
                  if let url = navigationAction.request.url {
                      lastHandledPopupURL = url
                      UIApplication.shared.open(url)
                  }
              }
              
              return nil
      }
}

webView 설정은 일반적으로 webView 사용 시 세팅하는 것과 동일하게 해주면 되고
ViewController extension 에서 위 내용들을 추가해주면 됩니다.



4. 기타 의견

테스트 해보았을 때, 웹 페이지에서의 대부분의 클릭과 링크에 대한 동작은
다뤄지는 걸로 확인되었습니다.

그러나 클릭 동작에 대한 최적화를 빈틈없이 다루고 싶다면 개인적으로는
웹과 bridge 통신을 통해 외부로 이동해야하는 경우 action과 url을 받아서
처리하도록 하는 방법이 더 정확하다고 생각합니다.

이번에는 이동하는 url이 계속 변경되고
저희 웹뷰에서 해당 url을 알 수 없는 구조라 통신을 통해 구현할 수 없어 위와 같이 구현하였습니다.

0개의 댓글