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

팝업과 리디렉션은 제 아이덴티티에 남는다

~14 min · webkit, 아이덴티티, 팝업, wkuidelegate

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

두 번째 계정이 새는 곳

탭 둘을 떼어 놓는 건 일의 절반이야. 페이지는 또 페이지를 열거든. 로그인 창, target="_blank" 링크, 새 탭에서 열리는 결과. 그중 하나라도 다른 저장소에 떨어지면 두 번째 계정이 슬그머니 첫 번째 계정이 되거나, 팝업에서 끝낸 로그인이 그걸 요청한 탭에 끝내 안 닿아. 아키텍처 불변식 7번은 이걸 두 문장으로 다 막아. 탭에서 연 링크, 팝업, 리디렉션은 그 탭의 아이덴티티에 남는다. 아이덴티티 사이로 쿠키를 옮기는 건 아무것도 없다.

Voyager는 새 페이지가 생기는 길마다 다른 방법으로 이 규칙을 지켜.

리디렉션: 정할 게 없어

리디렉션은 서버가 보내든 스크립트가 location을 바꾸든 같은 웹뷰 안의 이동이야. WKWebView는 만들어질 때 데이터 저장소까지 포함한 설정을 받고, 살아 있는 뷰의 저장소는 못 바꿔. 그러니 리디렉션은 아이덴티티를 떠날 수가 없고, Voyager에도 그걸 위한 코드가 없어. 이동에 다른 설정이 필요한 곳은 딱 하나, 확장 프로그램 자체 페이지와 보통 페이지 사이야. 거기선 페이지를 새 뷰로 갈아 끼워(트랙 8).

팝업: WebKit이 설정을 건네줘

페이지가 window.open을 부르거나 target="_blank" 링크를 따라가면, WebKit은 UI 델리게이트한테 새 웹뷰를 달라고 하면서 설정 하나를 같이 넘겨. Apple 문서가 조건을 박아 뒀어. 돌려주는 웹뷰는 넘겨받은 그 설정으로 만들어야 한다고. 그 설정에 여는 쪽의 데이터 저장소가 들어 있고, 로그인 팝업이 자기를 연 페이지에 결과를 알리는 데 필요한 연결도 같이 들어 있어.

Voyager의 자식은 딱 그 설정으로 새 페이지를 만들고, 여는 쪽의 아이덴티티를 이름표로 붙여. 따로 찾아볼 필요가 없는 이유는 주석이 말해. 자식은 만들어지는 방식 자체로 여는 쪽의 아이덴티티에 있다는 거야. 그러고 나서 부모한테 이 페이지를 둘지 물어. 거절하면 자식을 닫고 nil을 돌려줘. 페이지 쪽에선 window.open이 null을 돌려받는 걸로 보여.

그래도 부모가 한 번 더 봐

BrowserTab은 자식을 창에 넘기고, 창의 adopt는 여는 쪽과 아이덴티티가 다른 자식을 거절해. WebKit 자식에선 이 검사가 걸릴 일이 없어. 몇 줄 위에서 여는 쪽 아이덴티티를 그대로 복사했으니까. 그래도 있는 건 부모가 WebKit을 못 보기 때문이야. 규칙이 부모 자기 말로도 있어야 가짜 엔진이 브라우저 없이 시험할 수 있어.

aPageOpenedByAPageJoinsOnlyInTheSameIdentity가 바로 그걸 해. 여는 탭한테 새 아이덴티티를 가진 낯선 페이지를 건네면 창이 거절하고 탭은 여전히 하나야. 여는 쪽 아이덴티티의 자식은 바로 다음 탭이 되고 앞으로 나와. 코드에서 하나 더. 페이지가 크기를 정한 창을 달라고 해도 자식은 늘 여는 탭 바로 뒤의 탭이 돼. 요청엔 창 속성에서 읽은 wantsOwnWindow가 적히지만, 지금 창 모델은 그걸 쓰지 않아.

새 탭에서 여는 링크

Command 클릭, 가운데 버튼 클릭, 컨텍스트 메뉴의 Open Link in New Tab(새 탭에서 링크 열기)은 WebKit 팝업 경로를 아예 안 타. 두 클릭은 자식이 이동을 취소하고 부모한테 wantsNewTabAt을 알리면, 탭이 자기 뒤에 자기 아이덴티티로 탭을 열어 답해. 메뉴 명령은 엔진까지 가지도 않아. 셸이 직접 model.openTab을 부르면서 그 탭의 아이덴티티를 넘기고, 새 탭을 그 탭 뒤에 놓고, 앞으로 꺼내지 않아. Command만 누르면 뒤에 열리고, Shift를 더하면 앞으로 나오고, 가운데 버튼은 뒤에 열어. Chrome이랑 같아. aLinkedTabKeepsItsOpenersIdentity는 "Work" 아이덴티티 탭에서 링크를 열고 Work 탭 둘을 기대해. 탭 복제도 아이덴티티를 그대로 지켜.

창은 클릭이 있어야 열려

엔진이 만드는 설정은 전부 javaScriptCanOpenWindowsAutomatically를 false로 둬. Apple 정의로는 JavaScript가 사용자 동작 없이 창을 열어도 되는지를 정하는 설정이야. 옆 주석은 페이지가 창을 여는 건 아빠가 직접 클릭해서 원했을 때뿐이라고 적어 뒀어.

실제 WebKit 테스트 aPageOpenedByAPageIsInItsIdentityAndCanBeRefused는 먼저 이 설정이 꺼져 있는지 보고, 자기 설정에서만 켜서 클릭 대신으로 써. 400×300 창을 열고 셋을 확인해. 자식 아이덴티티가 여는 쪽 것인지, 자식의 데이터 저장소가 여는 쪽과 바로 같은 객체인지(===), wantsOwnWindow가 참인지. 이어서 자식이 window.close()로 스스로 닫고, 부모가 그걸 들어. 마지막으로 부모가 자식을 거절하게 해 두면 window.open이 null을 돌려줘.

이름 붙인 틈 하나

탭에서 나가는 길이 다 아이덴티티를 들고 가진 않아. 컨텍스트 메뉴의 Open Link in New Window(새 창에서 링크 열기)는 새 창을 여는데, 새 창의 첫 탭은 기본 아이덴티티를 받아. Work 탭의 링크를 이렇게 열면 기본 아이덴티티로 로그인된 채 도착해. 구현 계획서엔 링크를 다른 아이덴티티로 열거나 탭을 다른 아이덴티티로 다시 여는 기능, 그리고 사이트별 기본 아이덴티티가 아직 열린 항목으로 남아 있어. 다른 창으로 끌어다 놓은 탭은 아이덴티티를 지켜. 옮기는 탭이 저장된 아이덴티티를 같이 들고 가거든. 끌어 온 탭이 시크릿 창과 보통 창 사이를 건너는 일은 없어. 그래도 모델이 그 선을 어떻게 지키는지는 레슨 3에서 봐.

원칙: 규칙을 믿기 전에 새 페이지가 생기는 길을 전부 적어 봐. Voyager에선 세 길이 만들어지는 방식이나 검사로 아이덴티티를 지키고, 네 번째 길은 메뉴 코드를 읽다가 나왔어.

Code

cwkVoyager, Sources/VoyagerWebKit/WebKitPage.swift: 팝업은 WebKit이 건넨 설정으로 만든다 (원문 그대로, 437-453행)·swift
    func webView(
        _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
        for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures
    ) -> WKWebView? {
        guard let delegate else { return nil }
        // WebKit's configuration carries the opener and its data store, so the
        // child is in the opener's identity by construction.
        let child = WebKitPage(engine: engine, identity: identity, configuration: configuration, restoring: nil, extensionID: extensionID)
        let request = NewPageRequest(
            url: navigationAction.request.url,
            wantsOwnWindow: windowFeatures.width != nil || windowFeatures.height != nil)
        guard delegate.page(self, didOpen: child, for: request) else {
            child.close()
            return nil
        }
        return child.webView
    }
cwkVoyager, Sources/VoyagerCore/BrowserWindowModel.swift: 창은 여는 쪽 아이덴티티의 자식만 받아들인다 (원문 그대로, 315-325행)·swift
    /// A page the engine made for one of this window's pages. It joins the
    /// window only in its opener's identity.
    func adopt(_ child: any EnginePage, openedBy opener: BrowserTab) -> Bool {
        guard child.identity == opener.identity else { return false }
        child.zoom = services.settings.value.pageZoom
        let tab = BrowserTab(adopting: child)
        insert(tab, after: opener)
        selectedID = tab.id
        sessionChanged()
        return true
    }
cwkVoyager, Sources/VoyagerCore/BrowserTab.swift: 페이지가 다른 페이지를 여는 두 길, 둘 다 탭의 아이덴티티로 답한다 (원문 그대로, 393-399행)·swift
    public func page(_ page: any EnginePage, didOpen child: any EnginePage, for request: NewPageRequest) -> Bool {
        window?.adopt(child, openedBy: self) ?? false
    }

    public func page(_ page: any EnginePage, wantsNewTabAt url: URL, inBackground: Bool) {
        window?.openTab(url: url, identity: identity, after: self, select: !inBackground)
    }

External links

Exercise

데이터 저장소를 둘 이상 쓰는 WKWebView 앱에서 webView(_:createWebViewWith:for:windowFeatures:)를 구현해. 새 뷰는 WebKit이 넘긴 설정으로 만들어. 기본이 아닌 저장소에 페이지를 열고, 스크립트로 창을 하나 연 다음, 자식의 websiteDataStore가 여는 쪽과 같은 객체인지 확인하는 테스트를 써. 그리고 앱 UI에서 페이지가 다른 페이지를 열 수 있는 길을 메뉴 항목, 단축키, 끌어다 놓기, 공유 동작까지 전부 적어. 길마다 어느 저장소를 쓰는지도 적어 봐.
Hint
Apple 문서상 javaScriptCanOpenWindowsAutomatically 기본값은 macOS에서 true, iOS에서 false야. Voyager는 어디서나 false로 둬. 그래서 테스트는 클릭 없이 스크립트로 창을 열려고 설정 하나에서만 켜. 네 앱이 어떤 값으로 도는지 알아 둬. 버그는 목록에서 찾아져. 새 창을 여는 메뉴 명령은 보통 첫 탭을 출발한 페이지 기준이 아니라 기본값으로 만들거든.

Progress

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

댓글 0

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

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