엔진이 뭔지 말해주는 파일 하나
Sources/VoyagerCore/EngineContract.swift는 첫 줄부터 자기 일을 밝혀. '부모가 엔진한테 물을 수 있는 전부, 그리고 엔진이 부모한테 넘기는 결정 전부.' 탭, 세션, 신뢰, 아이덴티티, 피파 호스트는 여기 있는 타입만 알고 그 아래는 몰라. 주석은 파일 안의 모든 게 메인 액터에서 돈다고 해. 이유도 짧게 대. '페이지는 화면 위에 있는 물건이다.' 프로토콜까지는 맞는 말이야. 페이지, 그 델리게이트들, 패널, 엔진은 전부 @MainActor가 붙어 있거든. 그런데 평범한 값들은 아니야. 액터에 묶이지 않은 Sendable 구조체와 열거형이라서, 세션 기록기가 PageState를 자기 백그라운드 큐에서 직렬화할 수 있는 거야.
파일엔 두 종류가 들어 있어. 이 둘을 가르는 게 설계의 대부분이야.
부모가 따져 보는 평범한 값
PageStatus: 주소, 제목, 로딩 여부와 진행률, 뒤로·앞으로 갈 수 있는지, 보안 연결인지, 아이콘, 소리가 나는지.Equatable이라서 테스트가 상태 전체를 한 줄로 비교해.PageState: 페이지가 있던 자리(주소, 방문 기록, 스크롤 위치)를 바이트 덩어리로 담은 값이야. 그 바이트를 쓴 엔진 이름표가 붙어 있어. 부모는 저장했다가 돌려줄 뿐, 속은 안 들여다봐.PageOrigin: 스킴, 호스트, 포트. 들어올 때 소문자로 바뀌어. 주석은 이걸 신뢰를 정하는 기준이라고 불러.PermissionKind,PermissionRequest,PermissionDecision: 첫째는 페이지가 받을 수 있는 것의 닫힌 목록이야. 둘째는 요청하는 프레임과 그 프레임이 들어 있는 페이지를 함께 담은 요청이고, 셋째는 허용·거부·묻기 중 하나로 하는 답이야.
여기엔 WebKit이 한 글자도 안 나와. 브라우저가 다루는 걸 Swift 자기 말로 적었을 뿐이야. 값이니까 테스트에도 세션 파일에도 정수만큼 쉽게 들어가.
엔진이 만들고, 부모는 프로토콜로 쥐는 객체
| 프로토콜 | 정체 | 구현하는 쪽 |
|---|---|---|
BrowserEngine | 아이덴티티를 페이지로 바꾸는 물건. 페이지와 패널을 만들고, 아이덴티티 데이터를 지우고, 요청받으면 쿠키 헤더 하나를 만들어 줘 | WebKitEngine, 그리고 테스트의 FakeEngine |
EnginePage | 아이덴티티 하나에 속한 페이지 하나. 불러오기, 찾기, 확대, 음소거, 상태 담기, 피파용으로 읽기, 닫기 | WebKitPage |
EnginePageDelegate | Voyager가 내릴 결정과 페이지가 알리는 사실 | 부모의 BrowserTab |
EngineDownloadDelegate | 다운로드가 어디로 가는지, 어디까지 왔는지 | 부모의 DownloadsModel |
PanelSurface와 그 delegate | cwkPippa 자기 패널. 탭이 아니고, 아빠 아이덴티티도 없고, 자기 origin을 절대 안 벗어나 | WebKitPanel, 부모의 PippaPanelHost |
방향을 봐. 질문은 내려가. 부모가 페이지한테 불러오라고 하고, 탭이 잠들기 전에 상태를 달라고 하고, 피파가 페이지를 봐야 할 때 아빠가 뭘 보고 있는지 물어. 결정은 올라와. 페이지가 마이크든 새 창이든 대화 상자든 파일이든 원하면, 엔진은 혼자 정하지 않아. delegate한테 묻고, 그 delegate가 부모야.
다운로드에 프로토콜이 따로 있는 이유도 파일에 적혀 있어. '다운로드는 탭이 닫힌 뒤에도 계속된다.' 탭을 닫아도 다운로드는 이어져야 하거든. 페이지에 딸린 다운로드였다면 페이지랑 같이 죽었을 거야.
결정 하나를 끝까지 따라가 보기
마이크 요청 하나만 따라가 봐. WebKit이 자식한테 물어. 자식은 WebKit의 보안 origin과 프레임을 PermissionRequest로 바꾸는데, 요청한 프레임의 origin이랑 맨 위 페이지의 origin을 둘 다 담아. BrowserTab은 한 줄로 답해. window?.services.trust.decision(for: request) ?? .ask. 자식은 허용·거부·묻기를 다시 WebKit 자기 답 타입으로 옮기고. 신뢰 규칙은 WebKit 타입을 한 번도 안 만져. 그래서 그 테스트는 PermissionRequest를 손으로 만들 뿐, 페이지는 아예 안 만들어.
못 하는 엔진을 위한 기본값
파일 아래쪽 프로토콜 확장들은 능력이 모자란 엔진한테도 할 말을 쥐여 줘. '알 수 없는 엔진은 바쁘지 않은 걸로 친다.' 확장 호스트가 없는 엔진엔 확장 페이지도 없어. 일치 개수를 못 세는 패널은 nil로 답해. 이 기본값 덕분에 계약이 자라도 구현이 한꺼번에 깨지지 않고, 테스트용 가짜도 짧게 남을 수 있어.
실제로 많이 자랐어. 첫 벽돌 땐 251줄이었는데 지금은 746줄이고, 커밋 17개를 거쳤어. 뭔가 늘 때마다 그걸 필요로 한 기능이 같이 왔어. PanelSurface는 창 옆의 피파와 함께 들어왔어(a449ddd). reloadFromOrigin()은 강력 새로고침과(aaa8bcf), cookiesDidChange는 가족 카탈로그 로그인과(ac9bfc3) 함께 왔고. isBusy()는 놀고 있는 탭을 재우는 기능과(3638f97), FindMatches는 찾기 막대의 "3 of 17" 표시와(24cd64b), isMuted는 탭의 스피커 아이콘과(9e3f985) 함께 왔어.
설계도가 된 다형성
아빠는 세상을 객체로 읽어. 여기선 그 읽기가 말 그대로야. 계약이 부모고, 엔진은 그걸 채우는 자식이야. BrowserServices는 엔진을 any BrowserEngine으로 쥐고 있어서, 그 위에 있는 건 어느 자식을 받았는지 몰라. 첫 설계(224e256)는 루트 하나 아래 자식을 둘 그렸어. Mac엔 Chromium, 아이폰과 아이패드엔 WebKit. 같은 날 밤 아빠가 WebKit으로 정하면서 자식 하나가 빠졌고, 구조 문서는 문장 하나로 루트를 남겼어. '엔진은 갈아 끼울 수 있는 자식이라는 아빠 결정은 그대로고, 두 번째 자식이 온다면 여기로 들어온다. 계획된 건 없다.'