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 모니터링
'Dev Study > iOS' 카테고리의 다른 글
| UIKit ↔ SwiftUI 혼용 시 Life-Cycle 이해하기 (0) | 2025.11.14 |
|---|---|
| ViewController 비만화 방지 — 비즈니스 로직 분리 (0) | 2025.11.14 |
| WKWebView 쿠키(Cookie) 가이드 (0) | 2025.11.10 |
| 화면을 이미지로 전환 (UIView -> UIImage, View -> Image) (0) | 2025.11.06 |
| Swift 날짜 포맷(DateFormatter) 치트시트 & 사용법 정리 (iOS) (0) | 2025.11.06 |
| iOS 위젯에서 앱 특정 화면으로 이동하는 방법 (0) | 2025.11.06 |
| iOS 앱 크래시 발생 시 확인하는 방법 (0) | 2025.11.06 |
| iOS DocC 완전 정복 가이드: Swift 문서화를 위한 최고의 도구 (0) | 2025.11.06 |

