WKWebView를 쓰다 보면 비슷한 순간이 반복됩니다. 로그인 쿠키를 넣었는데 첫 요청에는 빠지고, JS 브리징은 언제나 completion이나 callback으로 돌아오고, 메모리 압박이 오면 앱은 살아 있는데 웹 화면만 하얗게 비어 버립니다.
이 세 가지는 서로 다른 문제가 아닙니다. WKWebView는 앱 프로세스 안의 단순한 뷰가 아니라, 앱 바깥에서 돌아가는 웹 런타임의 프런트엔드이기 때문입니다.
이 글은 그 구조를 한 단계씩 따라가며 설명합니다. WKHTTPCookieStore를 왜 따로 써야 하는지, WKContentWorld가 정확히 무엇을 격리하는지, WKProcessPool이 지금은 왜 큰 의미가 없는지까지 한 흐름으로 정리합니다.
참고한 문서: WebKit2 아키텍처 문서, WKHTTPCookieStore, WKWebsiteDataStore, WKContentWorld, callAsyncJavaScript, WKProcessPool, App-Bound Domains
먼저 증상과 원인을 연결해 보기
WKWebView에서 자주 겪는 증상은 대체로 프로세스 경계 하나로 설명됩니다.
| 겪는 증상 | 실제 원인 |
|---|---|
| 첫 요청에 쿠키가 빠진다 | 쿠키 저장 주체가 앱이 아니라 Network Process라서 비동기 전달이 필요하다 |
| JS 결과를 동기로 받을 수 없다 | UI Process와 WebContent Process 사이를 IPC로 왕복해야 한다 |
| 웹 화면만 하얗게 남는다 | 앱이 아니라 WebContent Process만 종료됐다 |
| 웹뷰를 많이 만들수록 버벅인다 | 뷰 하나가 아니라 프로세스, 세션, 캐시, 스크립트 환경을 함께 올리는 셈이다 |
| 페이지 스크립트와 브리지 변수가 충돌한다 | 같은 JS 컨텍스트를 공유하고 있었기 때문이다 |
이 표를 먼저 머리에 넣고 아래를 읽으면 WKWebView의 API가 왜 지금처럼 설계됐는지 훨씬 빨리 연결됩니다.
WebKit2 멀티프로세스 아키텍처
WKWebView는 WebKit2 위에 만들어졌습니다. WebKit2의 핵심 목표는 두 가지입니다.
첫째, 웹 페이지가 무한 루프에 빠지거나 크래시하더라도 앱 전체가 같이 멈추지 않게 하는 것. 둘째, JIT를 쓰는 JavaScript 실행 환경을 앱 코드와 분리해 보안 경계를 더 단단하게 만드는 것입니다.
WebKit 문서도 같은 이유를 설명합니다. 웹 페이지는 별도의 WebContent Process에서 실행되고, 여러 WebContent Process는 하나의 Network Process를 공유할 수 있으며, 민감한 시스템 자원 접근은 UI Process가 대신 중개합니다.
UI Process
우리가 붙잡고 있는 WKWebView 인스턴스가 있는 곳입니다. 터치 이벤트를 받고, 델리게이트를 호출하고, 다른 프로세스가 만든 결과를 화면에 합성합니다.
중요한 점은 이 프로세스가 웹 페이지를 직접 실행하지는 않는다는 점입니다. 앱 코드가 evaluateJavaScript를 부르더라도 실제 실행은 WebContent Process에서 일어납니다. UI Process는 요청을 전달하고 결과를 되돌려 받는 중개자에 가깝습니다.
WebContent Process
HTML 파싱, DOM 구성, CSS 계산, 레이아웃, JavaScript 실행이 일어나는 곳입니다. WebKit 문서가 강조하듯, 이 프로세스는 JIT로 임의 기계어를 생성할 수 있기 때문에 강하게 샌드박싱됩니다.
그래서 파일 시스템, 클립보드, 마이크, 카메라에 직접 접근하지 못합니다. 페이지에서 그런 기능을 요구하면 UI Process가 대신 권한을 확인하고 연결해 줍니다.
Network Process
HTTP 요청, 디스크 캐시, 쿠키, Web Storage, IndexedDB 같은 저장 계층을 담당합니다. NSHTTPCookieStorage와 WKHTTPCookieStore가 자연스럽게 하나로 합쳐지지 않는 이유도 여기 있습니다. 쿠키의 진짜 저장 주체가 앱 프로세스가 아니기 때문입니다.
GPU Process
합성 작업과 일부 가속 렌더링을 분리해 처리합니다. 모든 화면 문제가 GPU Process만으로 설명되지는 않지만, 최종 화면 출력이 단일 프로세스 안에서 끝나지 않는다는 점은 기억해 둘 만합니다.
페이지를 하나 로드하면 어디서 무슨 일이 일어날까
load(_:)를 부른 뒤의 흐름을 한 줄로 줄이면 이렇습니다.
이 흐름을 이해하고 나면 WKNavigationDelegate의 콜백도 훨씬 덜 기계적으로 보입니다. 대표적인 메인 프레임 내비게이션 흐름은 아래처럼 읽으면 됩니다.
여기서 didCommitNavigation은 "HTML 전체가 끝났다"가 아니라 첫 콘텐츠가 실제로 표시되기 시작했다에 가깝습니다. didFinishNavigation은 문서 로드가 끝난 시점이고, 이후 비동기 XHR이나 fetch가 더 돌아갈 수 있습니다.
실무에서 많이 헷갈리는 것도 이 차이입니다. 스켈레톤을 언제 숨길지, 인증 실패를 어느 콜백에서 잡을지, 페이지별 JS 설정을 언제 바꿀지를 이 구간에 맞춰야 합니다.
IPC: 왜 브리징은 항상 비동기일까
프로세스가 나뉘었다는 말은 곧 모든 핵심 동작이 IPC를 통과한다는 뜻입니다. WebKit은 내부 IPC 추상화 위에서 이 통신을 처리하고, 실제 전송 계층은 Mach 포트 기반 메시징을 사용합니다.
evaluateJavaScript 하나만 놓고 봐도 흐름은 단순하지 않습니다.
즉, JS 브리징이 비동기인 이유는 API 설계 취향이 아니라 프로세스 경계를 안전하게 넘기기 위한 제약입니다. 동기 호출로 막아 버리면 교착 상태와 UI 정지 위험이 커집니다.
이 관점으로 보면 callAsyncJavaScript, WKScriptMessageHandler, WKScriptMessageHandlerWithReply도 모두 같은 계열의 도구입니다. 결국은 UI Process와 WebContent Process 사이에서 메시지를 안전하게 주고받는 방식만 다를 뿐입니다.
WebContent Process 안의 렌더링 파이프라인
페이지가 로드된 뒤 WebContent Process 안에서는 브라우저 엔진다운 일이 벌어집니다.
Render Tree
DOM 전체가 아니라 실제로 그려야 할 노드만 모은 트리입니다. display: none처럼 시각적으로 제외되는 노드는 여기서 빠집니다.
Layout
각 렌더 객체의 위치와 크기를 계산합니다. 이 단계가 비싼 이유는 부모 변화가 자식 전체에 영향을 줄 수 있기 때문입니다. 웹 화면이 자주 흔들리거나, 자바스크립트가 DOM을 반복적으로 건드릴 때 체감 성능이 급격히 떨어지는 이유가 여기서 드러납니다.
Compositing
transform, opacity, will-change 같은 속성이 붙은 요소는 별도 합성 레이어로 분리될 수 있습니다. 이 덕분에 화면 전체를 다시 그리지 않고 변경된 레이어만 합성할 수 있습니다.
다만 최종 결과를 우리가 곧바로 손에 쥐는 건 아닙니다. 합성된 결과는 다시 IPC를 통해 UI Process로 넘어가고, 그 뒤에야 앱 화면에서 보입니다. 그래서 UIKit 내부 뷰 하나를 다루는 것과 WKWebView 안의 DOM 하나를 다루는 경험은 본질적으로 다릅니다.
WKWebViewConfiguration: 생성 전에 거의 다 결정된다
WKWebViewConfiguration은 웹뷰의 런타임 성격을 미리 정하는 객체입니다. 특히 아래 세 가지는 나중에 문제가 생겼을 때 가장 먼저 되짚게 됩니다.
let userContentController = WKUserContentController()
let config = WKWebViewConfiguration()
config.userContentController = userContentController
config.websiteDataStore = .default()
let webView = WKWebView(frame: .zero, configuration: config)| 프로퍼티 | 역할 | 실무에서 중요해지는 순간 |
|---|---|---|
userContentController | 스크립트 주입, 메시지 핸들러 등록 | 브리지 충돌, 메모리 누수, 스크립트 중복 |
websiteDataStore | 쿠키, 캐시, 스토리지 세션 관리 | 로그인 공유, 프라이빗 모드, 프로필 분리 |
limitsNavigationsToAppBoundDomains | App-Bound Domains 제한 적용 | 자사 도메인 전용 웹앱과 일반 인앱 브라우저를 나눌 때 |
생성 후에도 바꿀 수 있는 값은 있지만, 세션 성격이나 스크립트 환경은 처음 어떤 configuration으로 만들었는지가 대부분을 결정합니다.
WKWebsiteDataStore: 쿠키 공유 문제는 여기서 갈린다
WKWebsiteDataStore는 쿠키만 담는 객체가 아닙니다. Apple 문서 기준으로 쿠키, 디스크 캐시, 메모리 캐시, Web Storage, IndexedDB 같은 웹사이트 데이터를 한 세션 단위로 묶습니다.
기본 persistent store
config.websiteDataStore = .default()디스크에 저장되고, 기본 데이터 스토어를 같이 쓰는 웹뷰들 사이에 데이터가 유지됩니다. 대부분의 "앱 내 로그인 상태 유지"는 여기에서 시작합니다.
non-persistent store
config.websiteDataStore = .nonPersistent()메모리에만 존재하고 디스크에 쓰지 않습니다. 인앱 시크릿 세션, 일회성 인증 플로우, 테스트용 격리 세션에 적합합니다.
identifier 기반 persistent store
Apple 문서는 init(forIdentifier:)도 제공합니다. 즉, persistent 하되 기본 스토어와는 분리된 프로필형 세션을 만들 수 있습니다. 브라우저형 앱이나 다중 계정 시나리오에서 이 선택지가 꽤 중요합니다.
핵심은 이것입니다. 데이터 공유는 WKWebView 인스턴스가 아니라 WKWebsiteDataStore 단위로 생각해야 합니다. 같은 데이터 스토어를 보느냐가 쿠키와 스토리지 공유를 결정합니다.
쿠키: 왜 NSHTTPCookieStorage가 아니라 WKHTTPCookieStore인가
이 부분은 실제 장애로 자주 이어집니다. 앱 쪽에서 HTTPCookieStorage.shared에 값을 넣었는데 WKWebView 첫 요청에는 안 붙는 경우가 대표적입니다.
이유는 단순합니다. WKWebView의 쿠키 저장소는 앱 프로세스의 NSHTTPCookieStorage가 아니라, 데이터 스토어에 연결된 WKHTTPCookieStore입니다.
let cookieStore = webView.configuration.websiteDataStore.httpCookieStore
let cookie = HTTPCookie(properties: [
.name: "access_token",
.value: token,
.domain: ".sendy.kr",
.path: "/",
.secure: "TRUE"
])!
cookieStore.setCookie(cookie) {
webView.load(URLRequest(url: url))
}Apple 문서도 setCookie와 getAllCookies가 비동기 API인 점을 분명히 드러냅니다. 이 완료 시점을 기다리지 않고 load를 시작하면 Network Process 쪽 저장이 아직 끝나지 않았을 수 있습니다. 그래서 첫 요청에만 쿠키가 빠지는 현상이 생깁니다.
정리하면 쿠키 문제를 볼 때는 아래 순서로 생각하는 편이 빠릅니다.
- 어느
WKWebsiteDataStore를 쓰고 있는가 - 쿠키를 어느 store에 넣고 있는가
setCookie완료 전에 요청을 시작하지 않았는가- 앱 쿠키 저장소와 웹 쿠키 저장소를 같은 것으로 착각하고 있지 않은가
WKUserContentController와 스크립트 주입
WKUserContentController는 WKWebView 안의 JS 환경을 다루는 관문입니다. 역할은 크게 두 가지입니다.
첫째, WKUserScript로 스크립트를 주입합니다. 둘째, JS가 네이티브로 메시지를 보낼 수 있는 핸들러를 등록합니다.
WKUserScript의 주입 시점
let script = WKUserScript(
source: "window.__appVersion = '1.0.0';",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
userContentController.addUserScript(script)Apple 문서 기준으로:
atDocumentStart: 문서의 루트 엘리먼트가 생성된 직후, 다른 콘텐츠가 로드되기 전에 주입됩니다.atDocumentEnd: 문서 로딩이 끝난 뒤, 다른 하위 리소스가 모두 로드되기 전 주입됩니다.
그래서 atDocumentStart는 전역 브리지 객체를 미리 심거나 페이지 스크립트보다 먼저 후킹해야 할 때 쓰고, atDocumentEnd는 DOM 구조가 준비된 뒤 안전하게 조작할 때 더 잘 맞습니다.
WKContentWorld: 변수는 격리하지만 DOM은 격리하지 않는다
WKContentWorld는 iOS 14부터 들어온 중요한 경계입니다.
let appWorld = WKContentWorld.world(name: "SendyBridge")
let script = WKUserScript(
source: "window.__bridgeVersion = '2.0.0';",
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: appWorld
)WKContentWorld.page를 쓰면 페이지 스크립트와 같은 JS 컨텍스트를 공유합니다. 반대로 커스텀 world를 만들면 앱 전용 변수, 함수, 확장 로직을 분리할 수 있습니다.
여기서 중요한 공식 문서의 단서가 하나 있습니다. content world는 JavaScript 변수 환경을 분리하지만 DOM 자체를 분리하지는 않습니다. 즉, 서로 다른 world에 있어도 같은 문서를 보고 같은 DOM을 조작할 수 있습니다.
이 차이를 놓치면 이런 오해가 생깁니다.
- 맞는 이해: "브리지용 전역 변수 이름 충돌은 피할 수 있다"
- 틀린 이해: "커스텀 world면 페이지 DOM을 건드려도 페이지 스크립트가 못 본다"
실제로는 후자가 아닙니다. DOM 변경은 모든 world에서 보입니다. 격리되는 것은 JS 변수 공간입니다.
JS-Native 브리징
JS -> Native: WKScriptMessageHandler
가장 기본적인 브리징은 WKScriptMessageHandler입니다.
userContentController.add(self, name: "sendyBridge")
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let body = message.body as? [String: Any] else { return }
let action = body["action"] as? String
}JavaScript에서는 아래처럼 부릅니다.
window.webkit.messageHandlers.sendyBridge.postMessage({ action: "close" })메시지가 앱으로 들어오는 경로는 아래와 같습니다.
Apple 문서상 이 핸들러와 관련 메서드들이 @MainActor에 묶여 있는 것도 중요합니다. 브리지 메시지는 결국 앱 쪽 main thread로 들어온다고 봐야 합니다. 그래서 heavy JSON 파싱이나 큰 파일 처리까지 여기서 바로 해 버리면 웹 브리지와 앱 UI가 같이 밀릴 수 있습니다.
Native -> JS: callAsyncJavaScript
evaluateJavaScript가 문자열 평가에 가깝다면, callAsyncJavaScript는 함수 본문과 인자를 분리해서 넘길 수 있는 더 현대적인 API입니다.
let result = try await webView.callAsyncJavaScript(
"""
const response = await fetch(endpoint)
return await response.json()
""",
arguments: ["endpoint": "https://api.sendy.kr/info"],
in: nil,
contentWorld: .defaultClient
)이 API가 실무에서 좋은 이유는 두 가지입니다.
첫째, 문자열 연결로 파라미터를 억지로 이스케이프하지 않아도 됩니다. 둘째, Promise를 반환하는 JS 흐름을 Swift 쪽 async/await와 더 자연스럽게 연결할 수 있습니다.
여기서 contentWorld를 명시하는 이유도 분명합니다. 페이지 world에서 돌릴지, 앱 전용 world에서 돌릴지를 호출 시점마다 정할 수 있기 때문입니다.
Reply가 필요한 브리지: WKScriptMessageHandlerWithReply
단방향 알림이 아니라 "JS가 요청하고 네이티브가 응답"하는 구조가 필요하면 WKScriptMessageHandlerWithReply를 씁니다.
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage,
replyHandler: @escaping (Any?, String?) -> Void
) {
replyHandler(["status": "ok"], nil)
}문서상 replyHandler로 되돌릴 수 있는 값은 NSNumber, NSString, NSDate, NSArray, NSDictionary, NSNull 계열로 제한됩니다. 브리지 계약을 설계할 때 이 제약을 미리 정리해 두지 않으면, 양쪽에서 직렬화 규칙이 조금씩 어긋나기 쉽습니다.
WKWebpagePreferences: 페이지 단위로 설정을 덮어쓰는 지점
초기 configuration이 세션 전체의 성격을 정한다면, WKWebpagePreferences는 개별 페이지 로드 단위의 정책을 건드리는 도구입니다.
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
preferences: WKWebpagePreferences,
decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void
) {
preferences.allowsContentJavaScript = false
preferences.preferredContentMode = .mobile
decisionHandler(.allow, preferences)
}공식 문서를 보면 여기서 건드릴 수 있는 대표 값이 allowsContentJavaScript, preferredContentMode, 그리고 최근 버전의 HTTPS 관련 정책입니다.
이 레벨이 중요한 이유는 간단합니다. 웹뷰 전체를 다시 만들지 않고도 특정 페이지 하나만 JS를 막거나 렌더링 모드를 조정할 수 있기 때문입니다. 예를 들어 외부 결제 페이지는 모바일 모드로 두고, 사내 관리 화면만 데스크톱 레이아웃으로 보는 식의 운영이 가능합니다.
WKURLSchemeHandler: 모든 요청을 가로채는 도구는 아니다
이 API도 오해가 잦습니다. WKURLSchemeHandler는 WebKit이 기본 처리하지 않는 커스텀 스킴을 앱이 대신 로드하게 해 주는 도구입니다.
final class LocalResourceHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
guard let url = task.request.url,
let filePath = Bundle.main.path(forResource: url.lastPathComponent, ofType: nil),
let data = FileManager.default.contents(atPath: filePath) else {
task.didFailWithError(URLError(.fileDoesNotExist))
return
}
let response = URLResponse(
url: url,
mimeType: "text/html",
expectedContentLength: data.count,
textEncodingName: "utf-8"
)
task.didReceive(response)
task.didReceive(data)
task.didFinish()
}
func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {}
}
config.setURLSchemeHandler(LocalResourceHandler(), forURLScheme: "app-resource")이 API가 잘 맞는 경우는 번들 내부 HTML, 오프라인 리소스, 앱 전용 데이터 소스처럼 커스텀 스킴을 앱이 소유하고 있을 때입니다.
반대로 일반 http나 https 요청을 범용 인터셉터처럼 여기면 설계가 꼬입니다. 이 API의 역할은 "모든 네트워크 요청을 후킹한다"가 아니라 "WebKit이 모르는 스킴을 네가 대신 로드해라"에 더 가깝습니다.
메모리 압박과 WebContent Process 종료
WKWebView에서 흔히 보는 흰 화면은 앱 크래시와 같은 사건이 아닐 때가 많습니다. Apple 문서도 webViewWebContentProcessDidTerminate(_:)를 따로 둡니다. 말 그대로 웹 콘텐츠 프로세스만 종료된 경우를 알려 주는 델리게이트입니다.
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
webView.reload()
}이 메서드를 무조건 reload()로 끝내라는 뜻은 아닙니다. 폼 입력 중이었는지, 결제 흐름 중이었는지, 다시 로드하면 부작용이 없는지를 봐야 합니다.
중요한 건 판단 기준입니다.
- 앱 프로세스는 살아 있다
- 세션과 화면 상태 일부는 이미 어긋났을 수 있다
- 사용자는 "앱이 멈췄다"가 아니라 "웹 화면만 고장났다"고 느낀다
그래서 이 구간은 기술적으로는 복구 전략 문제이고, 사용자 경험 측면에서는 복구 가능한 실패를 어떻게 노출할지의 문제이기도 합니다.
WKProcessPool: 예전처럼 만능 키가 아니다
WKProcessPool은 예전 글에서 자주 "여러 웹뷰의 세션을 맞추는 비법"처럼 소개됐습니다. 지금은 그렇게 보면 안 됩니다.
Apple 문서는 WKProcessPool 자체를 deprecated 처리했고, "여러 인스턴스를 만들어도 더 이상 효과가 없다"고 명시합니다. 문서 설명도 흥미로운데, 기본적으로 WebKit은 웹뷰마다 자체 프로세스 공간을 주다가 구현체가 정한 한계에 도달하면 같은 process pool을 쓰는 웹뷰끼리 프로세스를 공유할 수 있다고 적습니다.
즉, 이 API는 원래도 세션 저장소 그 자체가 아니라 프로세스 사용 힌트에 더 가까웠습니다. 그리고 지금은 그마저도 거의 기대할 수 없습니다.
쿠키와 스토리지를 맞추고 싶다면 질문을 이렇게 바꾸는 편이 정확합니다.
- 틀린 질문: "
WKProcessPool을 공유하면 로그인도 공유되나?" - 맞는 질문: "같은
WKWebsiteDataStore를 보고 있나?"
세션 공유의 중심축은 이제 processPool보다 websiteDataStore입니다.
App-Bound Domains: 웹앱과 인앱 브라우저를 분리하는 정책
App-Bound Domains는 보안 기능이라기보다 권한이 넓은 WKWebView를 어디까지 믿을지 정하는 정책에 가깝습니다.
WebKit 블로그가 설명하듯, WKWebView는 JS 주입, 메시지 핸들러, 쿠키 조작 같은 강력한 API를 제공합니다. 이 기능을 외부 웹 전체에 열어 두면 앱이나 서드파티 SDK가 사용자의 웹 탐색 행동을 과하게 관찰할 수 있습니다. App-Bound Domains는 그 범위를 줄이기 위해 나온 기능입니다.
<key>WKAppBoundDomains</key>
<array>
<string>sendy.kr</string>
<string>api.sendy.kr</string>
</array>여기서 많이 놓치는 점이 두 가지입니다.
첫째, WKAppBoundDomains만 넣는다고 끝나지 않습니다. 특정 웹뷰에서 제한 모드를 실제로 적용하려면 아래 설정이 함께 가야 합니다.
config.limitsNavigationsToAppBoundDomains = true둘째, 이 제한을 켜면 아무 도메인이나 탐색하면서도 JS 주입을 계속 쓸 수 있는 게 아닙니다. WebKit 글 기준으로, app-bound 도메인 안에서만 evaluateJavaScript, addUserScript, window.webkit.messageHandlers, 일부 WKHTTPCookieStore API를 다시 사용할 수 있습니다.
이 정책은 결국 웹뷰를 둘로 나누게 만듭니다.
| 웹뷰 타입 | 적합한 용도 | 핵심 특성 |
|---|---|---|
| app-bound 웹뷰 | 자사 도메인 기반 웹앱 | 강한 제어, 강한 제한 |
| 일반 웹뷰 | 외부 링크 보기 | 탐색은 자유롭지만 민감 API 사용은 제한적으로 봐야 함 |
WebKit 글도 같은 결론을 냅니다. 단순 인앱 브라우저라면 SFSafariViewController가 더 나은 선택일 수 있고, WKWebView를 쓰더라도 자사 웹앱과 외부 브라우징은 같은 인스턴스 하나로 억지로 섞지 않는 편이 자연스럽습니다.
샌드박스 보안 모델
여기까지의 설계를 보안 관점에서 다시 읽으면 그림이 더 선명해집니다.
WebContent Process는 인터넷에서 내려받은 JS를 JIT로 실행할 수 있는 프로세스입니다. 다시 말해, 메모리를 쓰고 그 메모리를 실행할 수 있는 권한을 가진 셈입니다. 이 권한을 앱 프로세스와 같은 권한 범위에 두면 브라우저 취약점 하나가 곧 앱 전체 취약점이 됩니다.
그래서 WebKit은 이 프로세스를 강하게 가둡니다.
- 파일 시스템 직접 접근 불가
- 클립보드 직접 접근 불가
- 마이크, 카메라, 위치 같은 민감 자원 직접 접근 불가
- 필요한 요청은 UI Process가 중개
이 구조 덕분에 웹 콘텐츠 취약점이 발생해도 피해 범위를 WebContent Process 샌드박스 안으로 좁히는 것이 WebKit2 설계의 핵심입니다.
정리: WKWebView를 뷰가 아니라 시스템 경계로 봐야 한다
WKWebView를 UIKit 뷰 하나처럼 다루면 이상한 일이 계속 생깁니다. 하지만 이것을 앱 바깥의 웹 런타임과 연결된 경계 객체로 보면 대부분 설명됩니다.
- 쿠키는 앱 메모리가 아니라
WKWebsiteDataStore와WKHTTPCookieStore문제다 - JS 브리징은 함수 호출이 아니라 IPC 왕복이다
- 흰 화면은 뷰 버그가 아니라 content process 종료일 수 있다
WKContentWorld는 DOM이 아니라 JS 변수 공간을 격리한다- 세션 공유의 중심은
WKProcessPool이 아니라WKWebsiteDataStore다 - 외부 웹과 자사 웹앱을 같은 정책으로 다루면 App-Bound Domains에서 바로 한계가 드러난다
실무에서는 이 한 줄로 요약해도 충분합니다. WKWebView 이슈는 대부분 "어느 프로세스가 이 책임을 갖고 있나"를 먼저 묻는 순간 빨리 풀립니다.