본문 바로가기
C.W.K.
Lesson 04 of 05 · published

WebKit이 주는 것, 없는 것

~14 min · WebKit, 공개 API, WKWebsiteDataStore, 트레이드오프

Level 0새 탭
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

두 문장짜리 판단

메모리를 재고 나서, 평가 문서의 비교표는 외워 둘 만한 두 문장으로 끝나. '호환성과 확장만 놓고 보면 여전히 Chromium이 더 나은 엔진이다. 우리만의 브라우저를 짓기엔 WebKit이 훨씬 나은 엔진이다.' 두 말이 동시에 참이야. 이번 레슨은 그 둘을 같이 쥐는 법이야. WebKit이 공개 API로 자기 브라우저에 뭘 쥐여 주는지, 그리고 아예 없는 건 뭔지.

공개 API가 주는 것

평가 문서는 항목마다 등급을 달아서 하나씩 적어 놨어. Voyager의 모양을 정한 건 이것들이야.

  • 아이덴티티마다 격리된 저장소 하나. macOS 14, iOS 17부터 있는 WKWebsiteDataStore(forIdentifier:)는 내가 고른 id로 이름 붙은 영구 저장소를 줘. 여러 개가 동시에 살아 있을 수 있고, 팝업은 자기를 연 페이지의 저장소를 물려받아. Chromium에선 탭마다 로그인을 따로 두려면 요청 컨텍스트를 따로 줘야 하는데, 그게 프로필 하나 통째라서 확장이랑 방문 기록까지 같이 갈라져. 로그인만 떼고 나머지는 같이 쓰게 하려고 Brave는 패치 묶음을 만들었어. 여기선 이니셜라이저 하나면 되고, 다른 건 안 갈라져. 나중에 게이트도 확인해 줬어. 한 아이덴티티에서 심은 쿠키와 저장값이 다른 쪽에선 안 보였어.
  • 출처를 달고 요청마다 오는 권한 판단. 페이지가 마이크나 카메라를 달라고 하면, 프롬프트가 뜨기 전에 그 요청이 요청한 출처를 달고 호스트한테 먼저 와. 그래서 호스트가 사이트마다 판단할 수 있어.
  • 보안 컨텍스트, 그리고 피할 수 없는 제약 하나. 캡처엔 보안 컨텍스트가 필요한데, WebKit엔 평문 http 출처를 안전하다고 표시하는 공개 방법이 없어. 그래서 가족 앱은 HTTPS 이름으로 열거나, 그 앱을 띄운 Mac에선 루프백으로 열어.
  • 잠들고 되살아나기. interactionState가 페이지의 방문 기록, 스크롤 위치, 폼 데이터를 새 웹 뷰로 옮겨 줘.
  • 페이지 읽기. 격리된 콘텐츠 월드의 스크립트, 스냅숏, PDF, 웹 아카이브. 공개된 Reader나 텍스트 추출 호출은 없어서, 피파가 읽을 리더는 직접 짜야 해.
  • 콘텐츠 차단. 컴파일한 규칙 목록으로 막고, 목록 하나에 규칙이 150,000개까지 들어가. (API는 있는데, Voyager는 아직 광고나 추적기 차단을 안 만들었어.)
  • 시스템 코덱, 시스템 패치. H.264, HEVC, AAC가 OS와 함께 오고, 보안 수정은 macOS와 iOS 업데이트로 와. 앱은 엔진을 싣지 않아.
  • 기기 셋에 Swift 코드베이스 하나. iOS는 EU랑 일본 밖에선 WebKit만 허용하고, 거기서도 다른 엔진을 쓰려면 Apple한테 엔타이틀먼트를 따로 받아야 해. 그러니까 Mac도 같은 엔진이면 iPhone, iPad와 엔진 자식 코드를 같이 써.

확장은 제일 새로 생긴 조각이야. 웹 뷰 앱을 위한 Apple의 확장 호스팅 API(WKWebExtension, WKWebExtensionContext, WKWebExtensionController)는 macOS 15.4와 iOS 18.4부터 공개됐어. Voyager 패키지가 플랫폼 하한을 딱 그 릴리스로 잡은 이유가 이거야. platforms: [.macOS("15.4"), .iOS("18.4")]. 압축을 풀어 둔 Manifest V3 폴더를 WebKit이 구현한 API 범위 안에서 그대로 불러오고, 탭 모델, 액션 팝업, 권한 프롬프트, 네이티브 메시징은 호스트한테 맡겨. 이게 될 거라고 믿게 해 준 선례가 Orion이야. Chrome과 Firefox 확장을 돌리는 WebKit 브라우저인데, WebExtensions API의 70퍼센트쯤을 구현했고 6년 동안 자기 구현을 다듬은 끝에 2025년 11월에 1.0을 냈어(Secondary 등급).

WebKit에 없는 것, 이름 붙여서

  • Touch ID 패스키. 2026-10-04에 Apple의 웹 브라우저 자격 없이 빌드해서 쟀어. PublicKeyCredential은 있는데 isUserVerifyingPlatformAuthenticatorAvailable()가 false라서, 페이지는 플랫폼 인증기를 못 받아.
  • 기기 API. WebKit이 거절한 WebUSB, Web Serial, Web Bluetooth, WebHID, 그리고 아예 없는 Web MIDI.
  • Chrome에만 딸린 것들. Google Docs 오프라인(Chrome과 Edge 전용), 설치 경로로서의 Chrome Web Store, Chrome DevTools(대신 Web Inspector를 써). 그리고 AI 어시스턴트 확장처럼 Chrome 전용 API로 Chrome 탭을 조종하는 확장도 안 돼.
  • 탭 음소거. 공개 헤더엔 미디어를 일시정지하고 멈추는 호출은 있는데, 음소거는 없어. Safari의 음소거는 비공개야. Voyager의 탭 스피커는 자기 콘텐츠 월드에서 프레임마다 페이지의 미디어 요소를 꺼. 그래서 요소 없이 나는 소리(페이지에 붙이지 않은 Audio 객체, WebAudio)는 듣지도 끄지도 못해.
  • 찾기 개수. find(_:configuration:)는 맞는 게 있었는지만 알려줘. "17개 중 3번째"는 페이지에 보이는 텍스트로 직접 세.
  • iPhone, iPad에선: 일반 탭의 서비스 워커, 홈 화면에 추가, 기본 브라우저 지위, 아무 사이트에서나 쓰는 패스키. 전부 같은 관리형 자격이 있어야 해.

그 자격은 진짜 결정이었어. 2026-10-26에, Apple이 공개 TestFlight 링크와 외부 베타 앱 심사를 거쳐야만 준다는 얘기를 듣고 아빠가 정했어.

아니, 근데 이거 다 디폴트 브라우져 자격 얻는데 필요한 깐깐한 심사 아니야? 디폴트로 안 쓰면 그만이잖아? 이걸 외부 베타 테스트 심사까지 받는다고? 그건 하면 안 돼.

그래서 Voyager는 어떤 기기에서도 웹 브라우저 자격을 달고 있지 않아. 대가는 현재 사실로 적혀 있어. 페이지에 Touch ID 패스키가 없으니까 cwkPippa의 패스키 로그인엔 버튼이 안 뜨고, 대신 가족 PIN으로 들어가.

가족 웹 앱도 뒤져 봤어

결정 전에 확인이 하나 더 들어갔어. 가족은 자기 웹 앱을 꽤 많이 돌려. 그걸 깨뜨리는 WebKit 브라우저라면 첫 임무부터 실패하는 셈이지. 그래서 가족 프론트엔드 전부에서 Chromium 전용 웹 API를 찾았어. 나온 건 두 군데야. 음악 앱 Bonfire는 Web MIDI를 요청하는데, 없으면 이미 "MIDI 없음"으로 처리하고 있어. 그러니 WebKit에선 MIDI 입력만 그냥 안 돼. Loomis는 폴더 선택기를 먼저 쓰려 하는데 Safari용 대체 경로를 이미 갖고 있어. 검색에 걸린 Chromium 전용 사용처는 그게 다였어. 게다가 가족 웹 앱은 원래 WebKit인 iPhone, iPad에서 이미 돌고 있었고.

Bonfire의 MIDI가 가족 쪽에 남은 유일한 빈틈이고, 아직 열린 결정이야. 그 페이지에 네이티브 CoreMIDI 다리를 놓거나, Bonfire의 MIDI는 다른 브라우저에 두거나. 엔진 때문에 생기는 빈틈 전체 표와, 이름을 붙여야 "상위집합"이 정직해지는 이유는 첫 트랙의 이름 붙은 빈틈 레슨에 있어.

원칙: 엔진 선택이 정직하려면, 잃는 것들을 누가 발견하기 전에 먼저 이름 붙여 적고, 하나하나에 고른 답을 달아 둬야 해.

Code

cwkVoyager, docs/ENGINE-EVALUATION.md: 우리만의 WebKit 브라우저가 공개 API로 얻는 것 (원문 그대로, 92-115행)·text
### What a WebKit browser of our own gets from public API

- One persistent, isolated store per identity
  (`WKWebsiteDataStore(forIdentifier:)`, iOS 17 and macOS 14), several live
  at once, and a pop-up inherits its opener's store. Research.
- A per-request decision on microphone and camera with the requesting
  origin, so family origins are granted without a prompt and everything else
  is asked (iOS 15 and macOS 12). Research; the family already uses the
  method in cwkAriandel (coop learning #733).
- Capture needs a secure context, and WebKit has no public way to mark a
  plain-http origin secure. Family apps are therefore opened at their
  Tailscale HTTPS names, or at loopback on the Mac that serves them.
  Research.
- `WKWebView` traffic needs no Local Network permission. Research (Apple
  TN3179).
- Content blocking by compiled rule lists, 150,000 rules per list, several
  lists at once; no programmatic interception of http or https. Research.
- Sleep and restore: `interactionState` carries history, scroll position and
  form data into a new web view (iOS 15, macOS 12). Research.
- Page reading for Pippa: script in an isolated content world, snapshot, PDF
  and web archive; no public Reader or text-extraction call. Research.
- The system's codecs. Expected; exercised in the gate.
- Security fixes arrive with macOS and iOS updates; the app ships no engine.
  Secondary (DuckDuckGo's browser states the same of itself).
cwkVoyager, Sources/VoyagerWebKit/WebKitEngine.swift: 공개 이니셜라이저로 만드는 아이덴티티별 데이터 저장소 (원문 그대로, 78-93행)·swift
    /// One data store per identity, named by the identity's id: its cookies
    /// and site storage are in no other identity's reach.
    func store(for identity: IdentityID) -> WKWebsiteDataStore {
        if let store = stores[identity] { return store }
        // The private identity's store lives in memory for this run only.
        let store = identity == .privateSession ? WKWebsiteDataStore.nonPersistent() : WKWebsiteDataStore(forIdentifier: identity.raw)
        stores[identity] = store
        if identity != .privateSession {
            // The cookie store holds its observers weakly; the engine keeps
            // the watcher, and the watcher keeps no store.
            let watcher = CookieWatcher(identity: identity, engine: self)
            store.httpCookieStore.add(watcher)
            cookieWatchers[identity] = watcher
        }
        return store
    }

External links

Exercise

지금 앞에 놓인 플랫폼 결정 하나를 골라 봐. 엔진이든 런타임이든 클라우드 서비스든. 목록을 두 개 써. 첫째, 후보가 공개되고 문서화된 API로 주는 것. 항목마다 등급을 달아(문서를 직접 읽었는지, 리서치 요약인지, 제3자 보고인지, 확인 안 됐는지). 둘째, 후보에 없는 것. 빈틈마다 테스트할 수 있을 만큼 정확히 이름을 붙이고, 고른 답을 달아(내 코드로 메우기, 네이티브로 다리 놓기, 받아들이기). 끝으로 두 번째 목록의 API를 네 코드베이스에서 전부 찾아서, 지금 어디서 쓰고 있는지 적어 봐.
Hint
빈틈엔 형용사("호환성이 떨어짐") 말고 사용자가 알아챌 API나 기능 수준("찾기 개수 없음", "Web MIDI 없음")으로 이름을 붙여. 답이 없는 빈틈은 끝난 게 아니야. 받아들일 권한이 있는 사람이 받아들였다면 "받아들임"도 답이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.