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

계정 바로가기와 아이덴티티 메뉴

~13 min · 아이덴티티, appkit, ux, 데이터 삭제

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

아이덴티티 만들기

아이덴티티는 설정 안에 자기 구역이 따로 있어. 줄마다 색 고르개, 그 자리에서 고치는 이름, Default(기본) 표시나 Make Default(기본으로 지정) 링크, 지우기 버튼이 있어. 줄들 아래엔 New identity (Work, Family, a second account…)라고 적힌 입력란과 Add 버튼이 있어. 업무용, 가족용, 두 번째 계정처럼 새 아이덴티티 이름을 넣는 자리야. 새 아이덴티티는 일곱 색 팔레트에서 색을 받는데, 어느 색인지는 이미 있는 아이덴티티 수로 정해져. 물론 나중에 아빠가 바꿀 수 있어. 그 아래 설명은 아이덴티티가 뭘 하는지 한 줄로 말해. 아이덴티티마다 쿠키와 로그인을 따로 지닌다고.

이름이나 색을 바꾸면 책만 바뀌어. id는 그대로니까 저장소도 그대로고, 이름을 바꿔도 로그인은 다 살아 있어.

아이덴티티 메뉴

보통 창이면 어디든 툴바에 사람 모양 버튼이 있고, 맨 앞 탭 아이덴티티의 색으로 칠해져 있어. 누르면 메뉴 하나가 열려.

  • Open This Site as …(이 사이트를 …로 열기). 맨 앞 탭이 http나 https 페이지면 아이덴티티마다 하나씩 생겨.
  • New Tab as …(…로 새 탭). 아이덴티티마다 하나씩 있고, 그 색의 동그란 견본이 붙어. 지금 아이덴티티 옆엔 체크 표시가 있고.
  • Save Account Shortcut…(계정 바로가기 저장). 맨 앞 페이지를 그 아이덴티티로 저장해.
  • Manage Accounts and Identities…(계정과 아이덴티티 관리). 설정을 열어.

이 중 어느 것도 살아 있는 탭의 아이덴티티를 바꾸지 않아. 앞의 두 목록은 항목마다 새 탭을 열고, Save Account Shortcut…은 바로가기 제목을 묻는 시트를 띄우고, 마지막 항목은 설정을 열어. 불변식 7번의 '탭은 아이덴티티를 정확히 하나 갖는다'가 눈에 보이게 된 거지. 탭은 제 아이덴티티로 태어나서 그 아이덴티티로 끝나.

홈의 계정 바로가기

네이티브 홈에 Account shortcuts(계정 바로가기) 구역이 있어. 바로가기는 셋으로 돼 있어. 제목, URL, 이미 있는 아이덴티티 id. workspace.json에 저장돼. 타입 주석은 이걸 사용자가 가진 실행 설정일 뿐이라고 불러. 자격 증명도, 가족 등록부도 아니라는 거야.

WorkspaceModel.save가 거절하는 바로가기는 이래. 제목이 빈 것, 시크릿 아이덴티티인 것, 스킴이 http나 https가 아닌 것, 호스트가 없는 것, URL에 사용자 이름이나 비밀번호가 박힌 것. 같은 URL을 같은 아이덴티티로 또 저장하면 제목만 바뀌어. 여는 쪽도 똑같이 깐깐해. 시크릿 창에선 절대 안 열고, 없어진 아이덴티티로도 안 열고, 늘 바로가기 자기 아이덴티티의 새 탭으로 열어. UX 리뷰가 의도를 적어 뒀어. 바로가기는 없는 계정을 몰래 다른 걸로 바꾸지도, 살아 있는 탭의 아이덴티티를 바꾸지도, 쿠키를 복사하지도, 시크릿 아이덴티티를 쓰지도 못해. 아이덴티티가 사라진 바로가기는 Account unavailable(계정을 쓸 수 없음)이라고 뜨고 아무것도 안 해.

shortcutsKeepIndependentStoresAndDoNotGuessMissingAccounts 테스트는 같은 https://same.example/를 두 아이덴티티로 바로가기 둘에 저장해. 그리고 파일을 다시 읽은 뒤 둘 다 열어서, 아이덴티티가 다른 탭 둘이 나오는지 봐. 거절도 확인해. 모르는 아이덴티티, 시크릿 아이덴티티, 비밀번호가 든 URL, javascript: 주소. 홈의 탭 검색은 아이덴티티 이름도 맞춰 봐서, "second"를 치면 Second라는 아이덴티티의 탭이 나와.

리뷰에서 한 문장 더. 사용자가 친 이름으로 요금제가 정해지지는 않는다. 유료 요금제 이름을 붙인 바로가기는 아빠가 친 이름표일 뿐이야. Voyager 어디에서도 이름표에서 요금제를 읽지 않아.

탭의 표시

불변식 7번은 아이덴티티가 탭에 늘 보인다는 문장으로 끝나. 코드는 더 신중해. 탭 줄은 아빠의 아이덴티티가 둘 이상이고 시크릿 창이 아닐 때만 탭 앞쪽 가장자리에 아이덴티티 색의 3포인트 막대를 그려. 그 필드 주석이 이유를 말해. 색은 표시가 아빠한테 뭔가를 알려 줄 때만 그린대. 아이덴티티가 하나면 모든 탭이 똑같은 회색 막대를 달고 아무 말도 안 하는 셈이니까. 대신 툴바의 사람 버튼은 아이덴티티가 하나여도 보통 창이면 늘 있어. 문서는 '늘'이라고 하고, 코드는 '알려 줄 때'라고 해. 여기선 코드가 맞아.

아이덴티티 지우기

지우기 앞엔 말을 돌리지 않는 확인 창이 있어. 그 아이덴티티의 쿠키와 로그인은 전부 지워지고, 그 아이덴티티 탭은 먼저 다 닫아야 한다고 대놓고 말해. removeIdentity는 뭘 건드리기 전에 세 경우를 거절해. 이미 지우는 중인 것, 마지막 하나(지우는 중인 건 없는 셈 치고 세어), 어느 창에서든 탭이 열려 있는 것.

이 레슨의 요점은 그다음 순서야. 저장소가 먼저 가. WebKit이 못 지우면 항목은 남기고 이유를 보여 줘. WebKit이 일하는 동안 그 아이덴티티 탭이 열렸으면 그때도 항목을 남겨. 그다음에야 항목이 책에서 빠져. 지운 게 기본 아이덴티티였으면 기본은 남은 첫 번째로 넘어가고, 아니면 기본은 그대로야. 왜 이 순서일까? 저장소 없는 항목은 해가 없어. 그 id는 그냥 빈 저장소를 새로 만들 테니까. 항목 없는 저장소는 유일한 이름을 잃은 거고(레슨 1), Voyager 어디에서도 다시 못 찾아.

그 뒤엔 아무것도 짐작하지 않아. 아이덴티티가 사라진 저장된 세션 탭은 페이지 상태 없이 기본 아이덴티티로 다시 열리고, 그걸 가리키는 바로가기는 Account unavailable을 보여.

사이트 하나만 지우는 더 작은 도구도 옆에 있어. 사이트 정보 팝오버는 WebKit이 그 사이트 이름으로 담아 둔 데이터를 탭의 아이덴티티에서만 지워. 다른 아이덴티티의 같은 사이트는 로그인이 그대로야.

원칙: 이름과 몸이 따로 있는 걸 지울 땐 몸을 먼저, 이름을 마지막에 지워. 몸 없는 이름은 되살릴 수 있지만, 이름 없는 몸은 잃어버린 거야.

Code

cwkVoyager, Sources/VoyagerCore/BrowserWindowModel.swift: 아이덴티티 지우기, 저장소 먼저 항목은 마지막 (원문 그대로, 154-177행)·swift
    /// Removes an identity and its whole store (every cookie and sign-in).
    /// Refused for the last identity and for one with a tab open; the
    /// default moves to the first identity left. Returns why, when refused.
    public func removeIdentity(_ id: IdentityID) async -> String? {
        guard let identity = identities.identity(id) else { return nil }
        guard !removingIdentities.contains(id) else { return "This account is already being removed." }
        guard identities.identities.filter({ !removingIdentities.contains($0.id) }).count > 1 else { return "The last identity cannot be removed." }
        guard !identityInUse(id) else { return "Close every tab of “\(identity.name)” first." }
        removingIdentities.insert(id)
        defer { removingIdentities.remove(id) }
        do {
            try await engine.removeData(of: id)
        } catch {
            return "“\(identity.name)” was kept because its site data could not be removed: \(error.localizedDescription)"
        }
        // Keep the entry if a new tab opened while WebKit was removing data.
        // Its storage operation has completed, but a live tab must retain its identity.
        guard !identityInUse(id) else { return "Site data was cleared, but a tab reopened “\(identity.name)”. Close it before removing the account." }
        identities.identities.removeAll { $0.id == id }
        if identities.defaultIdentity == id, let first = identities.identities.first { identities.defaultIdentity = first.id }
        saveIdentities()
        if let identityProblem { return "Site data was cleared, but the account list could not be saved: \(identityProblem)" }
        return nil
    }
cwkVoyager, Sources/VoyagerCore/Workspace.swift: 계정 바로가기가 담기를 거절하는 것 (원문 그대로, 65-77행)·swift
    @discardableResult public func save(title: String, url: URL, identity: IdentityID) -> Bool {
        let name = title.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !name.isEmpty, identity != .privateSession,
              ["https", "http"].contains(url.scheme?.lowercased() ?? ""), url.host != nil,
              url.user == nil, url.password == nil else { return false }
        if let index = book.shortcuts.firstIndex(where: { $0.url == url && $0.identity == identity }) {
            book.shortcuts[index].title = name
        } else {
            book.shortcuts.append(AccountShortcut(title: name, url: url, identity: identity))
        }
        persist()
        return true
    }
cwkVoyager, Sources/VoyagerApp/BrowserWindowController.swift: 뭔가를 알려 줄 때만 그리는 탭의 아이덴티티 표시 (원문 그대로, 323-331행)·swift
        let marksIdentity = services.identities.identities.count > 1 && !model.isPrivate
        tabStrip.update(
            items: model.tabs.map { tab in
                TabStripItem(
                    id: tab.id.raw, title: tab.displayTitle, isLoading: tab.status.isLoading, isAsleep: tab.isAsleep,
                    isPinned: tab.isPinned, favicon: icon(of: tab), faviconTone: favicons.tone(for: tab.iconAddress),
                    identityColor: marksIdentity ? services.identities.identity(tab.identity).flatMap { NSColor(hex: $0.color) } : nil,
                    audio: tab.isMuted ? .muted : (tab.status.isAudible ? .playing : .silent))
            },

External links

Exercise

네 소프트웨어에서 이름은 한 곳에, 몸은 다른 곳에 있는 걸 하나 골라. 저장 폴더가 딸린 사용자 프로필, 데이터베이스 파일이 딸린 작업 공간, 키체인 항목이 딸린 계정 같은 거. 그걸 지우는 함수를 하나로 써 봐. 먼저 진행하면 안 되는 경우를 거절해. 마지막 하나, 쓰는 중인 것, 이미 지우는 중인 것. 그리고 몸을 지우고, 그동안 누가 다시 쓰기 시작하지 않았는지 한 번 더 본 다음에야 이름을 지워. 테스트는 거절마다 하나씩 쓰고, 몸 지우기에 일부러 실패를 끼워 넣어서 이름이 살아남는지 보는 테스트도 하나 써.
Hint
지우기 코드가 제일 잘 빼먹는 줄이 await 뒤의 재확인이야. 느린 삭제가 도는 동안 사용자가 그걸 다시 열었을 수 있거든. Voyager가 창들한테 두 번 묻는 이유가 딱 그거야. 거절할 땐 이유를 글로 돌려줘. 그래야 화면이 그걸 말할 수 있어.

Progress

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

댓글 0

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

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