본문으로 건너뛰기
5분 AI.
GITHUB TRENDING SWIFT WEEKLY #5 딥다이브 · 2026-10-23 분석

whatcable 딥다이브
— 케이블의 숫자를 읽어 병목의 이유를 설명하는 앱

진단 앱·CLI

빠른 판단

무엇
USB-C 케이블의 등급과 실제 전력·속도 협상을 비교하는 macOS 메뉴바 앱·CLI
이럴 때
충전·데이터 전송이 느릴 때 케이블·장치·현재 연결 상태를 대조해 병목의 근거를 찾을 때
입력
Mac이 읽은 케이블·연결 장치·충전 협상 정보
결과
케이블 능력과 실제 연결 비교, 텍스트·JSON 진단 결과
같은 USB-C 모양인데 왜 충전 속도와 데이터 속도는 다를까요? whatcable은 Mac이 가진 포트·전력·케이블 정보를 모아 사람이 이해할 수 있는 진단으로 바꾸는 macOS 메뉴 막대 앱입니다. 이 글에서는 Swift의 비트 연산부터 IOKit, 비동기 상태 관리, 공통 진단 엔진, 테스트 설계까지 연결해서 읽습니다. 저장소 darrylmorley/whatcable · 스타 8,985 · 포크 274 · 최신 릴리스 v1.5.0 · 2026-10-23 GitHub 기준. 소스 분석은 커밋 58e588a 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

Mac의 하드웨어 정보를 읽어, 연결이 느린 이유를 문장으로 설명합니다

whatcable은 USB-C 연결의 상태를 해석하는 네이티브 유틸리티입니다. 메뉴 막대에서 케이블의 속도 등급·허용 전력·연결 장치·충전 협상 정보를 보여주고, CLI에서는 같은 핵심 모델을 텍스트와 JSON으로 내보냅니다. USB-C는 단자의 모양이고, 그 단자로 가능한 통신과 전력 공급의 범위는 별개의 정보입니다.

이 앱의 중심은 숫자 수집 다음에 있습니다. 케이블이 주장하는 능력, Mac이 관측한 연결 상태, 데이터베이스가 알려주는 이름을 구분한 뒤 진단 문장으로 조립합니다. 이 구분을 놓치면 240W 케이블을 꽂았다는 이유만으로 Mac이 지금 240W로 충전 중이라고 잘못 설명하게 됩니다.

핵심 비유

케이블의 이력서와 현재 성적표를 함께 읽는 통역사

이력서에는 “최대 80 Gbps, 최대 240W”라고 적혀 있습니다. 하지만 오늘 연결한 장치와 충전기가 허용하는 범위는 더 작을 수 있습니다. whatcable은 이력서와 현재 연결 결과를 나란히 놓고 어디까지 알고, 무엇 때문에 제한되는지 설명합니다.

통역사가 원문에 없는 사실을 만들어내면 안 되듯, 진단 앱도 읽히지 않은 정보를 고장으로 단정하면 안 됩니다. 이 원칙이 nil, unknownCable, 경고 여부의 분리로 코드에 나타납니다.

1-1. 화면 뒤에서는 네 단계가 이어집니다

  1. 수집: macOS의 IOKit과 전원 관련 인터페이스에서 포트·USB-PD·USB·Thunderbolt 상태를 읽습니다.
  2. 해독: 정수와 비트 필드를 케이블 속도, 전류 등급, 장치 식별 정보로 바꿉니다.
  3. 판단: 충전기·케이블·Mac 또는 연결 장치의 한계와 현재 협상 결과를 비교합니다.
  4. 표현: SwiftUI 화면, CLI의 문장, JSON의 구조화된 필드로 결과를 전달합니다.
용어
e-marker와 USB Power Delivery
e-marker는 케이블의 특성을 알려주는 전자 식별 칩입니다. USB-PD는 연결된 기기들이 전원 공급 조건 등을 주고받는 규약입니다. 여기서 읽은 최대 허용 전력은 실제 소비 전력과 다릅니다. e-marker 정보가 없는 연결도 코드에서는 정상적으로 고려합니다.

1-2. 예시 하나로 제품의 목적 이해하기

다음은 이해를 위한 가정입니다. 충전기가 100W를 지원하고 케이블은 60W까지 전달할 수 있다면, 케이블의 정격이 제한 요인이 될 수 있습니다. 반대로 충전기와 케이블에 여유가 있어도 Mac이 적은 전력만 요청할 수 있습니다. 배터리 충전 보류나 완충 상태까지 함께 봐야 설명이 맞아집니다.

숫자뜻읽을 때의 질문
케이블 정격케이블이 표시하는 허용 범위이 케이블이 허용하는 상한은 얼마인가?
충전기 능력전원이 제공할 수 있다고 알리는 범위어떤 전압·전류 조합을 제공하는가?
협상 결과이번 연결에서 선택된 전원 조건현재 어떤 계약을 맺었는가?
실시간 계측시점별 실제 전력에 관한 별도 읽기지금 얼마를 공급받거나 쓰는가?

코드의 출발점은 Sources/WhatCableCore/Power/ChargingDiagnostic.swift의 init?입니다. 충전 상태를 설명할 근거가 부족하면 객체 생성을 포기하고, 판단할 수 있으면 Bottleneck과 설명 문장을 함께 만듭니다.

이 글에서 얻어갈 것
작은 유틸리티 안의 큰 설계 문제

비트 몇 개를 읽는 코드가 사용자에게 믿을 만한 설명이 되려면 데이터 출처, 시간 순서, 장치 연결 관계, 불확실성을 함께 다뤄야 합니다. 이 저장소는 그 과정을 공부하기 좋은 사례입니다.

2왜 주목받는가

눈에 보이지 않는 케이블 차이를, 바로 확인할 수 있는 문제로 바꿉니다

2-1. 흔한 불편과 구체적인 답이 만납니다

USB-C 케이블을 바꿨더니 SSD가 느려지거나, 충전기를 바꿨는데도 충전이 기대만큼 빨라지지 않는 상황은 원인을 짐작하기 어렵습니다. whatcable은 포트별 정보를 모으고 원인 후보를 설명하는 데 집중합니다. 문제 상황을 한 문장으로 말하기 쉽고 결과도 바로 이해하기 쉽다는 점이 관심을 끌 만한 요소입니다.

2026-10-23 GitHub 기준 스타는 8,985, 포크는 274이며, GitHub Trending Swift Weekly #5에 선정됐습니다. 다만 이 수치만으로 방문 경로나 인기의 원인을 증명할 수는 없습니다. 아래의 주목 요인은 기능과 코드 구조를 바탕으로 한 해석입니다.

2-2. 무엇과 비교하면 차이가 보일까요?

Apple의 시스템 정보 공식 안내에 따르면 시스템 정보 앱은 Mac과 연결 하드웨어 등의 보고서를 보여줍니다. whatcable의 차별점은 그중 케이블 문제에 필요한 신호를 한데 모아 설명하는 흐름입니다. 비교 범위는 도구의 역할이며, 특정 유료 제품의 우열을 검증한 결과는 아닙니다.

도구·접근법잘하는 일whatcable이 더해 주는 것
시스템 정보Mac과 연결 하드웨어의 상세 보고서 확인케이블·충전·데이터 연결에 집중한 포트별 요약과 진단
직접 IOKit 읽기필요한 속성을 개발자가 직접 수집서비스 탐색, 포트 매칭, 해독, 문장 출력을 미리 연결
케이블 사양표제조사가 표시한 모델별 능력 확인실제로 연결된 케이블의 식별 정보와 현재 연결 상태 확인
외부 측정 장비장비가 지원하는 전기적·통신 신호 측정Mac이 노출한 정보만으로 시작하는 소프트웨어 진단

이 앱의 장점은 원시 정보를 많이 보여주는 데서 끝나지 않고, 그 정보를 근거가 드러나는 설명으로 바꾸는 데 있습니다. 외부 측정 장비와 달리 Mac이 공개하지 않는 신호까지 직접 측정할 수는 없으므로, 소프트웨어의 관측 범위도 함께 이해해야 합니다.

2-3. 사용자용 제품과 공개 소스의 범위를 나누어 읽기

README는 무료 앱과 별도로 Pro의 전력 모니터, 저장한 케이블 이력, 진단 화면, 터미널 대시보드를 소개합니다. 하지만 커밋 58e588a의 Sources/WhatCablePlugins/Bootstrap.swift는 등록 함수 본문이 비어 있는 4줄짜리 파일입니다.

범위 구분
README의 Pro 소개를 공개 구현 전체로 읽으면 안 됩니다

공개 소스에는 플러그인 확장 지점과 여러 공통 모델이 있지만, README가 설명하는 Pro 구현 전체는 들어 있지 않습니다. 공개 코드만으로 Pro 대시보드나 라이선스 처리를 재현할 수 있다고 단정할 수 없습니다.

오히려 학습 주제는 더 선명해집니다. 무료 핵심 기능이 추가 기능에 의존하지 않도록 경계를 어떻게 만들었는가를 살펴보면 됩니다. 4절에서 빈 등록 함수가 왜 의미 있는 설계 흔적인지 설명합니다.

3기술 스택 전체 지도

네이티브 앱의 핵심 엔진과 별도로 운영되는 정적 웹사이트를 구분합니다

3-1. 백엔드: 서버 대신 Mac 내부의 데이터 수집 계층

이 프로젝트에서 백엔드라고 부를 만한 부분은 웹 API 서버가 아니라 WhatCableDarwinBackend입니다. macOS에 묶인 읽기 코드를 모아 WhatCableCore의 모델로 전달합니다. 핵심 하드웨어 진단 경로에 별도 서버가 필요한 구조는 아닙니다.

계층실제 기술·파일역할과 버전 근거
패키지 기준Package.swiftswift-tools-version: 5.9, .macOS(.v14). 매니페스트의 도구 기준이며 실제 사용 컴파일러 버전 기록은 아닙니다.
핵심 모델Swift · FoundationWhatCableCore의 값 타입, 해독 함수, 진단, 출력 조립
하드웨어 읽기IOKit · CoreGraphics · 전원 인터페이스WhatCableDarwinBackend에서 서비스·디스플레이·전원 상태 수집. 개별 버전은 macOS SDK에 종속됩니다.
참조 데이터SQLite3CableDB.swift에서 시스템 C API로 번들 DB 읽기. 별도 SQLite 패키지 버전은 선언돼 있지 않습니다.
알림 판단WhatCableNotifications변경 순서와 알림 내용을 다루는 별도 모듈. 앱의 UI·전달 계층과 분리됩니다.

근거는 Package.swift입니다. 제품은 앱·CLI 실행 파일 2개와 라이브러리 3개로 선언됩니다. 알림·플러그인 모듈은 내부 타깃이며, 모든 타깃이 독립 배포 제품인 것은 아닙니다.

3-2. 프론트엔드: SwiftUI와 AppKit의 협업

Sources/WhatCable/App/App.swift의 WhatCableApp은 SwiftUI의 진입점입니다. @NSApplicationDelegateAdaptor로 AppDelegate를 연결하고, 실제 메뉴 막대 항목·팝오버·일반 창의 수명은 AppKit 쪽에서 관리합니다. 카드 내용은 Sources/WhatCable/Views/ContentView.swift 등 SwiftUI 코드가 그립니다.

화면·출력기술소스에서 확인되는 연결
메뉴 막대 앱SwiftUI + AppKitApp.swift의 앱 수명 관리와 ContentView의 선언형 화면
상태 구독Combine@ObservedObject, watcher의 게시 값, WatcherHub.didRefresh
명령행Foundation + AppKitWhatCableCLI.swift가 인수를 직접 검사. ArgumentParser 의존성은 없습니다.
데스크톱 위젯WidgetKit + AppIntentsWhatCableWidget.swift와 CableTimelineProvider가 별도 확장으로 동작
다국어번들 리소스Sources/WhatCable/Resources/ko.lproj/Localizable.strings 등 언어별 자료. 핵심·앱·알림 모듈마다 리소스가 있습니다.

이 코드는 상태 구독에 ObservableObject 계열을 사용합니다. 최근 Swift 예제에서 보이는 @Observable로 이미 바뀌었다고 읽으면 안 됩니다. 학습할 때도 저장소의 선언과 다른 버전의 문법을 구분하세요.

3-3. 의존성 선언과 잠금 파일을 함께 읽는 이유

Package.swift에는 외부 .package(...) 의존성 선언이 없습니다. 그런데 Package.resolved에는 TUIkit의 main 브랜치와 리비전 18246360bbdefd5ad7e5eab47955d47eac9d824f가 남아 있습니다. 이것만으로 공개 앱이 현재 TUIkit을 사용한다고 결론 내릴 수 없습니다.

용어
선언 파일과 잠금 파일
선언 파일은 “무엇에 의존하는가”를, 잠금 파일은 “해결된 의존성의 어떤 판본을 기록했는가”를 나타냅니다. 둘이 어긋나면 실제 타깃의 선언과 import까지 확인해야 합니다. 잠금 파일에 이름이 있다고 현재 기능이 구현돼 있다는 뜻은 아닙니다.

3-4. 웹사이트와 인프라·빌드

루트의 package.json은 Mac 앱이 아니라 whatcable.uk의 정적 사이트용입니다. eleventy.config.js가 Nunjucks 자료를 처리하고 src/에서 docs/를 생성합니다. 이 src/는 저장소 내부 웹사이트 폴더이며 Swift 앱 코드는 Sources/에 있습니다.

항목선언고정·설정값
사이트 자체 버전package.jsonwhatcable-site · 1.0.0. 앱 릴리스 번호와 별개입니다.
Eleventy^3.0.0bun.lock에 고정된 버전 3.1.5
RSS 플러그인^2.0.2bun.lock에서 2.0.4
구문 강조 플러그인^5.0.0bun.lock에서 5.0.2
사이트 배포.github/workflows/pages.ymlubuntu-latest, Bun 설치, 잠금 파일 고정 설치 후 GitHub Pages 배포
위젯 빌드project.ymlmacOS 14.0, xcodeVersion: "16.3". XcodeGen이 확장 프로젝트를 구성합니다.
앱 패키징scripts/smoke-test.shVERSION="1.5.0", Swift 테스트·빌드·서명·선택적 공증·검증 순서

버전의 근거는 package.json과 bun.lock입니다. 워크플로는 actions/checkout@v4, oven-sh/setup-bun@v2, actions/configure-pages@v5, actions/upload-pages-artifact@v3, actions/deploy-pages@v4를 사용합니다. Bun 자체 버전은 이 설정에서 고정하지 않습니다.

Mac 앱의 빌드 환경과 사이트의 빌드 환경은 서로 다르므로, 루트에 JavaScript 의존성이 있다는 이유로 앱을 웹 기술 기반이라고 분류하면 안 됩니다. 공개 워크플로에서 확인되는 자동 배포는 Pages용이며, 앱 릴리스 과정은 별도 셸 스크립트에 담겨 있습니다.

4아키텍처 심화 분석

관측한 사실을 모으는 일과 사용자에게 설명하는 일을 나눕니다

4-1. 전체 시스템 구조

+----------------------------------------------------------+ | Mac: USB-C / MagSafe / USB / Thunderbolt / DisplayPort | +-----------------------------+----------------------------+ | IOKit / 전원 / 화면 정보 | +-----------------------------v----------------------------+ | WhatCableDarwinBackend | | | | HubRead.readAll(_:smcReader:) | | 포트 -> PD 식별 -> 전원 -> TB -> USB3 -> 화면 등 | | | | | | WatcherHub DarwinSnapshotProvider | | 앱용 상태 갱신 CLI용 snapshot / watch | +----------+------------------------------+----------------+ | | | CableSnapshot | | | | +----------v------------------------------v----------------+ | WhatCableCore | | CableSnapshotContext: 포트별 연결 관계 조립 | | PDVDO / PortSummary / ChargingDiagnostic | | DataLinkDiagnostic / CableDB / 출력용 모델 | +----------+------------------------------+----------------+ | | +----------v-----------+ +-----------v----------------+ | WhatCable | | WhatCableCLI | | SwiftUI + AppKit | | TextFormatter / JSONFormatter| | 메뉴 막대 / 창 | | 텍스트 / JSON / 변경 스트림 | +----------------------+ +----------------------------+ 별도 확장: WhatCableWidget OS의 타임라인 요청 -> 포트 직접 읽기 -> 포트 없으면 App Group 자료 별도 사이트: Nunjucks + Eleventy -> docs/ -> GitHub Pages

구조도의 핵심은 앱과 CLI가 같은 창을 공유한다는 뜻이 아니라, 같은 도메인 모델과 해석 코드를 재사용한다는 점입니다. UI의 상태를 CLI가 몰래 읽는 대신 각 실행 경로가 하드웨어 데이터를 얻고 핵심 모델에 전달합니다. 위젯도 갱신 시점과 수명이 별도입니다.

4-2. 패턴 ① 수집 경계와 스냅샷: 읽는 방식이 달라도 판단은 재사용

WhatCableCore의 CableSnapshotProvider는 데이터를 얻는 두 가지 동작만 약속합니다. 현재 상태 하나를 얻는 snapshot()과 상태 변화를 받는 watch()입니다. 아래는 Sources/WhatCableCore/Snapshot/CableSnapshot.swift의 선언입니다.

public protocol CableSnapshotProvider: Sendable {
    func snapshot() async throws -> CableSnapshot
    func watch() -> AsyncThrowingStream<CableSnapshot, Error>
}

DarwinSnapshotProvider가 이 약속을 macOS용으로 구현합니다. makeDefaultSnapshotProvider()는 구체 구현을 any CableSnapshotProvider로 돌려주고, CLI는 그 공급자에게 요청합니다. 읽기 장치를 테스트용 가짜 구현으로 바꿔볼 수 있는 경계가 생깁니다.

용어
프로토콜과 스냅샷
프로토콜은 부품이 제공해야 할 동작의 약속입니다. 스냅샷은 어떤 읽기에서 얻은 상태를 한 묶음으로 정리한 자료입니다. 이 프로젝트의 스냅샷은 여러 서비스를 순차적으로 읽어 조립하므로, 하드웨어 전체가 정확히 같은 순간에 멈춰 찍힌 사진은 아닙니다.

DarwinSnapshotProvider.swift의 watch()는 매번 읽기 뒤 1초를 기다리며, 이전 CableSnapshot과 다를 때만 스트림에 내보냅니다. onTermination에서는 내부 Task를 취소합니다. CLI 변경 감시의 수명을 명시적으로 닫는 부분입니다.

이 경계가 모든 동시성 문제를 자동으로 해결하지는 않습니다. 해당 클래스는 @unchecked Sendable을 선언하고 내부 State는 @MainActor로 격리합니다. 또 CableSnapshotContext의 주석은 CableSnapshot이 아직 Sendable이 아니라고 밝힙니다. “프로토콜이 Sendable이니 모든 반환값도 안전하다”고 일반화하면 안 됩니다.

4-3. 패턴 ② 읽기와 적용 분리: 늦게 끝난 결과가 최신 상태를 지우지 않게

앱의 WatcherHub.swift는 watcher들을 한곳에서 소유합니다. requestRefresh(steady:)가 읽기를 요청하고, HubRead.readAll(_:smcReader:)이 결과 묶음을 만든 다음, apply(_:readStartedAt:)가 상태에 반영합니다.

상황실제 설정·함수설계 의도
UI가 보임activeInterval = .seconds(1)현재 정보를 자주 갱신
UI가 숨겨짐idleInterval = .seconds(30)상시 폴링의 에너지 비용 줄이기
빠른 연결 변화scheduleBurst()150·500·1500·3000·6000ms를 순서대로 기다린 뒤 추가 읽기
읽기 진행 중readInFlight, followUpQueued정기 요청은 건너뛰고 중요한 추가 요청은 하나로 합치기
창 여러 개visibleSurfaces: Set<String>하나를 닫아도 다른 창이 보이면 활성 주기 유지

버스트의 대기값은 순차적으로 더해집니다. 읽기 시간과 스케줄링 지연을 제외하면 시작 후 약 0.15·0.65·2.15·5.15·11.15초에 요청하는 셈이며, 연결 사건에서 각 숫자만큼 떨어진 절대 시각을 뜻하지 않습니다.

포트를 읽는 동안 케이블이 빠지면 어떤 일이 생길까요? 알림으로 watcher가 먼저 최신 상태를 게시하고, 뒤늦게 끝난 읽기가 이전 연결을 다시 덮어쓸 수 있습니다. 이 앱은 비동기 읽기에 참여하는 watcher의 refreshGeneration을 읽기 시작 때 기록해 두고, 적용할 때 여전히 같은지 검사합니다.

// WatcherHub.apply(_:readStartedAt:)에서 발췌
if portWatcher.refreshGeneration == start.port {
    portWatcher.apply(reading.ports)
} else {
    skipped = true
}
// 생략된 다른 watcher 검사 뒤에 이어지는 처리
if skipped { followUpQueued = true }
비유

재고표를 복사하기 시작한 시점이 10번 개정판인데, 복사가 끝나기 전에 원본이 11번으로 바뀌었다면 그 복사본을 최신 재고표로 붙이면 안 됩니다. refreshGeneration은 이 개정 번호이고, 재읽기 예약은 새 복사본을 요청하는 일입니다.

메인 액터에서 상태를 바꾸더라도, 비동기 읽기가 시작된 뒤 더 최신 정보가 도착할 수 있으므로 결과의 신선도를 별도로 검사해야 합니다. 상태 접근의 안전성과 데이터의 최신성은 서로 다른 문제입니다.

정기·버스트 갱신의 공유 읽기는 Task.detached(priority: .userInitiated)에서 수행하고 적용은 메인 액터로 돌아옵니다. 다만 start()의 초기 포트·PD·전원 갱신은 메인 액터에서 동기적으로 처리합니다. CLI 공급자도 이 앱용 스케줄러와 다른 읽기 경로를 사용합니다.

읽기 순서에도 이유가 있습니다. HubRead.readAll(_:smcReader:)은 포트 → PD 식별 → 전원 순서로 읽습니다. 전원 노드가 없는 일부 환경에서 만드는 보완 정보가 이번 읽기의 포트와 PD 식별에 의존하기 때문입니다. 독립적이지 않은 읽기를 무작정 병렬화할 수 없는 사례입니다.

4-4. 패턴 ③ 공통 키로 결합하고, 정보의 출처는 보존

하나의 물리 포트도 USB 장치 목록, 충전 정보, PD 응답, Thunderbolt 정보에서는 다른 자료로 등장합니다. PortIdentity는 typeCode와 number를 묶고 "2/1" 같은 키를 만듭니다. 코드의 USB-C 타입 값은 0x2, MagSafe 3는 0x11입니다.

CableSnapshotContext.init(snapshot:)은 포트마다 충전원·PD 식별·USB 전송·CIO·화면 정보를 모읍니다. 장치 목록에서는 직접 매칭된 장치와 구조상 이 포트에 속하는 터널 장치를 합치되, Set<UInt64>로 중복 식별자를 제거합니다.

PD 응답 ------ 포트 키 2/1 --+ 전원 정보 ---- 포트 키 2/1 --+--> PortContext --> 진단과 출력 USB 장치 ----- 포트 매칭 ----+ TB 터널 장치 - 구조적 소속 --+ 케이블 자기 보고 / Mac의 관측 / 번들 DB의 이름 출처를 구분해서 사용자에게 설명

이때 e-marker가 보고하는 제조사 식별자는 칩 공급자와 관련될 수 있습니다. CableDB.curatedCables(vid:pid:cableVDO:)는 VID·PID에 더해 Cable VDO 변형을 비교하며, 한 지문에 여러 판매 브랜드가 연결될 수 있음을 고려합니다. 제조사 ID 하나로 소매 제품명을 단정하지 않는 것입니다.

출력의 bulletGroups도 학습할 부분입니다. 골든 JSON은 source를 emarker, measured, database, charger 등으로 구분합니다. 화면에 비슷하게 보이는 한 줄들이 어떤 근거에서 왔는지 모델 수준에서 나눕니다.

코드에 남아 있는 경계
공통 조립기가 모든 선택 정책을 통합한 것은 아닙니다

CableSnapshotContext는 VDO가 있는 SOP'·SOP''를 우선 선택하지만, ChargingDiagnostic과 DataLinkDiagnostic은 전달받은 식별 목록에서 자체 선택을 합니다. 소스 주석도 이 차이를 명시합니다. 중앙화 의도와 현재 적용 범위를 함께 읽어야 합니다.

4-5. 패턴 ④ 등록 객체를 통한 확장: 추가 기능이 없어도 핵심은 유지

Sources/WhatCableAppKit/PluginRegistry.swift는 메뉴 항목, 카드의 추가 뷰, 위젯 데이터 제공자, CLI 명령 등을 배열에 등록합니다. 앱과 CLI는 등록된 항목을 소비합니다. 추가 기능의 구체 타입을 핵심 화면이 직접 알아야 하는 구조를 피합니다.

// Sources/WhatCablePlugins/Bootstrap.swift 전체
import WhatCableAppKit

public func bootstrapPlugins(registry: PluginRegistry) {
}

빈 함수도 호출 경계를 유지합니다. 공개판에서는 등록된 Pro 명령이 없고, 추가 구현을 연결하는 판본에서는 같은 지점에 기능을 등록할 수 있습니다. 다만 이것은 앱 내부의 등록 방식입니다. 임의의 외부 바이너리 플러그인을 실행 중에 찾아 불러오는 시스템이라고 설명할 근거는 없습니다.

CLI의 main()은 등록된 명령의 플래그를 기존 플래그 집합에 합친 뒤 알 수 없는 인수를 검사합니다. 등록된 플러그인 명령 두 개가 동시에 매칭되면 오류로 처리합니다. 확장 가능한 구조에서도 사용자 입력의 모호함을 방치하지 않는 사례입니다.

5디렉토리 구조 해부

처음부터 모든 파일을 읽기보다 핵심 흐름의 경로를 따라갑니다
whatcable/ |-- Package.swift Swift 타깃과 의존 관계 |-- Package.resolved 기록된 SPM 의존성 리비전 |-- Sources/ | |-- WhatCable/ 앱 진입점, SwiftUI 뷰, 서비스 | | |-- App/ | | |-- Views/ | | |-- Services/ | | `-- Resources/ 언어별 문자열 | |-- WhatCableCore/ 모델, 해독, 진단, 출력 | | |-- PD/ USB-PD 식별·VDO 해독 | | |-- Port/ 포트 모델과 식별·표시 정책 | | |-- Power/ 전원 모델과 충전 진단 | | |-- USB/ 장치·데이터 연결 진단 | | |-- Thunderbolt/ 연결과 터널 모델 | | |-- Display/ 화면·EDID 해독 | | |-- Devices/ 연결 장치 트리와 소속 | | |-- Snapshot/ 읽기 결과와 포트별 조립 | | |-- Database/ 공급자·케이블 조회 | | |-- Output/ 텍스트·JSON 표현 | | `-- Resources/ 번역, whatcable.db 등 | |-- WhatCableDarwinBackend/ | | |-- Watchers/ IOKit 감시와 읽기 조율 | | |-- Reading/ 시스템·화면·전력 읽기 | | `-- Support/ IOKit 변환·수명 관리 | |-- WhatCableAppKit/ 추가 UI·명령의 등록 계약 | |-- WhatCableNotifications/ 알림 내용과 변경 순서 | |-- WhatCableCLI/ CLI 진입점 하나 | |-- WhatCableWidget/ WidgetKit 확장 | `-- WhatCablePlugins/ 공개판의 빈 등록 함수 |-- Tests/ 핵심·Darwin·알림·앱 테스트 |-- probes/test-kit/ C 기반 하드웨어 조사 도구 |-- data/ 케이블 관련 원본 자료 |-- scripts/ 빌드·릴리스·DB·사이트 도구 |-- project.yml 위젯용 XcodeGen 설정 |-- package.json / bun.lock 웹사이트 의존성 |-- eleventy.config.js 정적 사이트 설정 |-- src/ 웹사이트 원본 |-- docs/ 사이트 결과물·자료 `-- .github/workflows/pages.yml 사이트 배포 워크플로

5-1. Swift 코드의 무게중심

커밋 58e588a의 텍스트 파일을 줄 단위로 세면 다음과 같습니다. 공백·주석을 포함한 물리적 줄 수이며 복잡도나 실행 성능의 척도는 아닙니다. Swift 전체는 422개 파일, 약 147,961줄입니다.

영역Swift 파일 수줄 수
WhatCableCore8725,204
DarwinBackend3110,932
메인 앱279,831
알림 모듈74,288
위젯71,368
AppKit 확장 계약8488
CLI1479
플러그인 공개 부분14
Sources 합계16952,594
Tests 합계24791,863
scripts 내 Swift53,435

핵심 모듈에는 생성된 EDIDTimingTables.generated.swift 329줄도 포함됩니다. 테스트가 구현보다 길다는 사실은 다양한 하드웨어 사례와 회귀 조건을 많이 기술한다는 단서입니다. 테스트가 모두 실행되거나 통과한다는 증거로 사용할 수는 없습니다.

5-2. 다른 언어도 역할별로 세어야 합니다

언어·형식범위대략적인 규모
C / 헤더조사·검증 도구 등 8개 파일4,208줄
Python타이밍 표 추출·검증 등 5개 파일2,821줄
셸빌드·릴리스 등 11개 파일2,177줄
JavaScriptdocs·vendor를 뺀 관리 코드 5개 파일737줄
Nunjucks사이트 원본 24개 파일7,915줄
동봉 JavaScript웹 탐색기 vendor 폴더 3개 파일64,465줄, 직접 작성 규모와 구분

JavaScript를 전부 합하면 130,310줄이지만, 여기에는 배포 결과 쪽의 사본과 동봉 라이브러리가 들어갑니다. 앱의 규모를 그 숫자로 설명하면 구조를 잘못 전달하게 됩니다. 줄 수를 볼 때는 언어뿐 아니라 원본·생성물·외부 코드의 경계도 확인하세요.

5-3. 초보자를 위한 읽기 순서

  1. Package.swift로 모듈 이름과 화살표를 적습니다.
  2. Sources/WhatCableCore/PD/USBPDVDO.swift와 Tests/WhatCableCoreTests/PDVDOTests.swift를 나란히 읽습니다.
  3. Sources/WhatCableCore/Power/ChargingDiagnostic.swift에서 입력과 결과 enum을 연결합니다.
  4. Sources/WhatCableCore/Snapshot/CableSnapshotContext.swift로 입력들이 포트별로 모이는 과정을 봅니다.
  5. Sources/WhatCableDarwinBackend/Watchers/HubRead.swift와 WatcherHub.swift로 실제 수집·갱신을 이해합니다.
  6. 마지막으로 WhatCableCLI.swift와 ContentView.swift에서 사용자까지의 경로를 확인합니다.

6학습 포인트 (기술별)

문법 하나를 배울 때마다, 이 앱에서 그 문법이 필요한 이유를 붙입니다

6-1. Swift 비트 연산: 정수 하나 안의 여러 필드 꺼내기

PDVDO.decodeIDHeader(_:)는 32비트 정수에서 서로 다른 필드를 분리합니다. 아래 두 줄은 실제 코드의 식을 학습용 변수에 대입한 예입니다. 하위 16비트는 공급자 ID, 29~27번 비트는 UFP 제품 유형입니다.

let vdo: UInt32 = 0x1800_1234
let vendorID = Int(vdo & 0xFFFF)               // 0x1234
let productType = Int((vdo >> 27) & 0b111)    // 3

// 별도의 Cable VDO에서 속도·전류 필드만 연습한 값
let cableVDO: UInt32 = (2 << 5) | 4
let speedBits = Int(cableVDO & 0b111)           // 4
let currentBits = Int((cableVDO >> 5) & 0b11)   // 2
용어
시프트와 마스크
>>는 원하는 비트를 오른쪽 끝으로 옮깁니다. 비트 AND인 &는 필요한 자리만 남깁니다. 0b111은 끝의 3비트, 0xFFFF는 끝의 16비트를 남기는 체입니다.

실습 아이디어: 종이에 32칸을 그리고 0x1800_1234의 필드를 분리해 보세요. 그다음 PDVDOTests.decodePassiveCableIDHeader()와 답을 비교합니다. 같은 숫자 3도 UFP에서는 수동 케이블, DFP에서는 전원 공급 장치이므로 필드의 맥락을 빼면 잘못 해독하게 됩니다.

위 예시의 cableVDO는 속도·전류 필드만 구성한 연습값이며 완전한 케이블 응답은 아닙니다. 실제 함수는 이 밖의 필드도 검사합니다. 파일 맨 위 주석은 PD 3.0/3.1 해독이라고 소개하지만, 세부 주석은 R3.2 표를 인용합니다. CableSpeed.usb4Gen3 주석은 일부 규격 판본 사이의 속도 인코딩 해석이 모호하다는 점도 남깁니다. 상단 요약만으로 모든 필드의 규격 판본과 해석이 확정됐다고 읽지 마세요.

6-2. enum과 실패 가능한 초기화: 모르는 상태를 자료형으로 표현

DataLinkDiagnostic.Bottleneck은 fine, cableLimit, hostLimit, deviceLimit, degraded, unknownCable, cableContradictsActive, blockedBySecurity를 구분합니다. 각 상태는 필요한 숫자를 연관값으로 가집니다.

// DataLinkDiagnostic.Bottleneck의 일부
case cableLimit(cableGbps: Double, capableGbps: Double)
case degraded(activeGbps: Double, expectedGbps: Double)
case unknownCable(activeGbps: Double)
case cableContradictsActive(cableGbps: Double, activeGbps: Double)

케이블 능력을 모르는 것과 연결이 기대보다 느린 것은 다릅니다. 읽힌 속도와 e-marker의 주장이 충돌하는 경우도 별도 상태입니다. 알 수 없음을 정상적인 결과로 모델링하면, 근거가 부족한데도 사용자에게 잘못된 원인을 지목하는 일을 줄일 수 있습니다.

실습 아이디어: DataLinkDiagnosticTests.swift의 degradedLink()와 unknownCableWhenNoSignal()을 비교하세요. “현재 속도” 외에 어떤 능력 정보가 있어야 더 강한 판단을 할 수 있는지 입력 표로 정리합니다.

6-3. 진단 결과와 경고 강도는 별개입니다

DataLinkDiagnostic.isWarning은 fine, deviceLimit, unknownCable을 경고로 표시하지 않습니다. 느린 USB 장치가 자신의 최고 속도로 동작하는 것은 케이블 고장이 아니기 때문입니다. 충전 쪽 ChargingDiagnostic.isWarning의 현재 구현은 cableLimit만 true입니다.

여기에는 작은 주석 불일치도 있습니다. 데이터 진단 파일의 설명에는 충전 진단은 fine만 비경고라는 비교가 남아 있지만, 실제 충전 진단의 switch는 그보다 넓게 비경고를 처리합니다. 동작을 설명할 때는 실행 분기를 우선 근거로 삼아야 합니다.

실습 아이디어: 상태 이름만 보고 빨간색을 고르지 말고 “사용자가 케이블을 바꾸면 해결되는 문제인가?”라는 질문으로 경고 여부를 따로 정리하세요. UI의 색과 문구도 도메인 정책의 일부임을 배울 수 있습니다.

6-4. SwiftUI 상태: 화면에 필요한 정보와 그 정보의 수명

ContentView는 @ObservedObject로 공유 watcher를 관찰하고, 내부에서 소유하는 ConnectionFaultTracker는 @StateObject로 보관합니다. 일반 값인 PortVisibilityTracker는 @State로 유지합니다. 구독과 소유, 값의 지속성을 서로 다른 선언으로 나타냅니다.

앱 진입점의 WhatCableApp.startUp(defaults:applyLanguage:bootstrap:)은 언어 적용을 플러그인 등록보다 먼저 합니다. 번역 문자열로 메뉴 제목을 만드는 시점 전에 언어를 확정해야 첫 화면부터 맞게 표시되기 때문입니다.

실습 아이디어: ContentView.swift에서 속성 래퍼 세 종류를 찾아 “누가 객체를 만들고, 뷰가 다시 만들어져도 무엇이 남아야 하는가”를 설명해 보세요. SwiftUI 공식 문서는 선언형 화면과 상태 관리 개념을 더 찾아볼 출발점입니다.

6-5. IOKit과 자원 수명: 반복 중에도 하드웨어는 바뀝니다

IOKitHelpers.swift의 wcDrainAllRetrying<T>는 IOKit iterator를 끝까지 읽습니다. 항목을 하나 이상 읽은 뒤 iterator가 무효화됐고 시도 횟수가 남아 있으면, 그 회차의 결과를 버리고 처음부터 다시 읽습니다. 기본 시도 횟수는 maxAttempts = 3입니다. 마지막 시도에서도 무효하면 그 회차에서 모은 결과를 그대로 반환하므로 일부 항목이 빠질 수 있습니다. 처음부터 빈 목록은 재시도하지 않습니다.

이 함수는 각 항목을 변환한 뒤 IOObjectRelease(next)로 반환합니다. Swift의 일반 객체처럼 보이는 코드에서도 C 계열 시스템 자원의 수명을 따로 관리해야 하는 이유가 드러납니다. 자세한 경계는 해당 소스 파일과 Apple의 IOKit 문서를 함께 보세요.

실습 아이디어: 첫 시도에서 장치 A를 읽은 직후 무효화되고, 다음 시도에서 A와 B를 읽는 경우를 적어보세요. 첫 회차 결과를 버리지 않으면 A가 중복되는 이유를 설명할 수 있어야 합니다.

6-6. SQLite와 데이터 식별: 이름 하나 붙이기도 조심스럽게

CableDB는 Bundle.module의 whatcable.db를 SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX로 열고, 자료를 메모리 딕셔너리로 옮긴 뒤 핸들을 닫습니다. 정적인 번들 자료를 반복 조회하는 용도이며 변경 이력을 저장하는 사용자 DB와는 다릅니다.

curatedCables(vid:pid:cableVDO:)는 VID나 PID가 0이면 빈 배열을 돌려줍니다. VID·PID가 맞는 후보 안에서 VDO가 정확히 같은 행을 우선하고, 없으면 VDO가 0인 미지정 행으로만 돌아갑니다. 사양이 다르다는 이유로 엉뚱한 브랜드를 붙이지 않도록 검색 범위를 제한합니다.

실습 아이디어: 같은 VID·PID에 3A·5A 두 행을 그려 놓고 VDO 일치, 미일치, 미지정 경우의 결과를 예상해 보세요. Tests/WhatCableCoreTests/CableDBVariantLookupTests.swift와 연결됩니다.

6-7. 테스트: 정상 예제보다 틀리기 쉬운 경계를 먼저 읽기

테스트는 import Testing, @Suite, @Test, #expect를 사용하는 Swift Testing 코드가 다수입니다. 비트 해독 테스트는 장치 없이 입력 정수만으로 기대 결과를 기술할 수 있습니다. 반면 하드웨어 코퍼스 재생 테스트는 별도 자료의 유무까지 확인해야 합니다.

// PDVDOTests의 입력과 핵심 검사를 줄인 예
let vdo: UInt32 = 0x1800_0000 | 0x1234
let header = PDVDO.decodeIDHeader(vdo)
#expect(header.ufpProductType == .passiveCable)
#expect(header.vendorID == 0x1234)

Tests/WhatCableDarwinTests/WatcherHubTickTests.swift는 가짜 읽기를 주입해 메인 스레드 밖의 읽기, 요청 합치기, 상태 반영을 검증하도록 작성돼 있습니다. 실제 케이블을 뽑는 타이밍에 의존하지 않고 경합을 재현하는 설계입니다.

실습 아이디어: 테스트 이름을 입력 조건과 기대 결과로 바꿔 읽어보세요. 마지막으로 Tests/WhatCableCoreTests/Fixtures/golden/의 JSON·텍스트 쌍을 비교하면, 같은 판단을 여러 출력으로 일관되게 전달하는 계약도 배울 수 있습니다.

7하드웨어 / 시스템 요구사항

프로그램이 실행되는 조건과 진단에 필요한 정보가 나오는 조건은 다릅니다

7-1. 앱 사용과 소스 학습의 조건

항목요구·제약근거와 읽는 방법
운영체제macOS 14 이상Package.swift의 .macOS(.v14), 패키징의 MIN_OS="14.0"
주요 지원 하드웨어Apple Silicon MacREADME와 warnIfUnsupportedArchitecture()가 포트 컨트롤러 정보의 Intel 제약을 설명합니다.
케이블 정보연결과 장치에 따라 가용 정보가 다름e-marker·전원 노드·화면 모드가 없을 수 있어 모델에 선택값과 보완 경로가 존재합니다.
RAM·저장 공간정량적인 최소 요구 미명시측정된 메모리 사용량이나 설치 크기 없이 임의의 숫자를 권장하지 않습니다.
앱 코드 읽기텍스트 편집기만으로 시작 가능해독·판단·테스트 입력은 장치를 연결하지 않고도 추적할 수 있습니다.
테스트 도구Swift Testing을 지원하는 도구 체인 확인현재 테스트는 import Testing을 사용합니다. 매니페스트의 5.9 숫자만으로 충분성을 판단하지 않습니다.
배포용 앱 구성Xcode 도구·XcodeGen 등SPM 외에 위젯 확장 빌드와 번들 조립 단계가 필요합니다.

README의 빌드 안내는 Swift 5.9+ (Xcode 15+)라고 적지만, 현재 테스트는 Swift Testing을 사용합니다. Apple은 Xcode 16 이상에 Swift Testing이 포함됨을 안내합니다. 따라서 README의 옛 최소 안내만 보고 이 커밋의 전체 테스트 환경이 충족된다고 판단하면 안 됩니다.

7-2. Universal 바이너리와 Intel 지원을 혼동하지 않기

scripts/smoke-test.sh는 앱과 CLI를 --arch arm64 --arch x86_64로 빌드합니다. 위젯 빌드에도 아키텍처 재정의를 전달합니다. 반면 project.yml의 기본값은 ARCHS: arm64입니다. 설정 파일의 기본값과 배포 스크립트의 최종 인수가 다릅니다.

Intel용 실행 코드가 들어 있다는 사실과, Intel Mac에서 케이블 진단에 필요한 하드웨어 정보가 나온다는 사실은 별개입니다. CLI는 실제 하드웨어를 확인해 Intel에서는 포트 컨트롤러 데이터가 없다는 경고를 보냅니다. Rosetta의 실행 아키텍처만 보고 판단하지 않도록 런타임 확인을 합니다.

예외도 있습니다. --tb-debug 경로는 Thunderbolt 패브릭 정보가 Intel에서도 유용할 수 있어 같은 경고를 적용하지 않습니다. 지원 범위를 “Intel에서는 어떤 정보도 없다”로 넓혀 쓰면 소스의 의도를 놓칩니다.

7-3. 권한·배포·위젯의 현실적인 경계

README는 Developer ID 서명과 Apple 공증을 거친 배포를 설명합니다. 소스의 배포 경로는 서명·공증·Gatekeeper 검증을 다루지만, 인증서와 공증 설정 없이 만든 개발판이 같은 배포 상태가 되는 것은 아닙니다. 앱을 실행할 수 있는 최소 OS와 개발 도구를 실행할 수 있는 OS도 서로 다른 조건입니다.

README의 “private API를 쓰지 않는다”는 설명도 범위를 나눠 읽어야 합니다. 소스는 IOKit 접근과 Apple의 서비스 클래스·속성 이름 해석에 의존합니다. 공개 IOKit 접근 함수를 사용한다는 사실이 모든 서비스 속성의 영구적인 호환성을 보장하지는 않습니다.

장치 호환성
깊은 USB 탐색에 예민한 KVM·허브를 위한 우회 경로가 있습니다

--no-usb-probe는 USBWatcher.probeBillboardDescriptors를 끄는 경로입니다. 해당 코드 주석은 일부 장치가 기능 정보 탐색에 반응해 재설정될 수 있다고 설명합니다. 값을 읽는 프로그램도 하드웨어와의 상호작용 범위를 확인해야 합니다.

CableTimelineProvider.swift는 포트 상태를 직접 읽고, 포트 목록을 얻지 못하면 앱이 저장한 App Group 자료를 사용합니다. 충전 상태는 기록 후 120초 이내인 앱 캐시를 우선하며, 그런 캐시가 없으면 SMC에서 읽습니다. 위젯이 메뉴 막대 화면처럼 매초 계속 실행되는 것은 아닙니다. Apple의 위젯 갱신 문서도 타임라인과 시스템의 갱신 예산을 설명합니다.

8직접 해볼 수 있는 실습 과제

설치와 장치 연결에 앞서, 파일과 입력값만으로도 설계를 연습할 수 있습니다

다섯 과제는 순서대로 난도가 올라갑니다. 먼저 종이와 편집기로 답을 적고, 해당 테스트와 구현을 대조하세요. 실습의 완성 기준은 결과를 맞히는 데서 끝나지 않고, 어떤 근거가 부족하면 판단을 보류해야 하는지 설명하는 것입니다.

과제 1

숫자를 케이블 정보로 해독하기 입문

읽을 파일: Sources/WhatCableCore/PD/USBPDVDO.swift, Tests/WhatCableCoreTests/PDVDOTests.swift.

할 일: 0x1800_1234와 0x0180_1234에서 vendorID, UFP 제품 유형, DFP 제품 유형을 각각 구합니다. 오른쪽 시프트와 마스크를 적용한 중간값도 적습니다.

기대 결과: 두 값의 vendorID는 모두 0x1234입니다. 첫 값은 UFP가 수동 케이블이고, 둘째 값은 DFP가 전원 공급 장치입니다. 둘째 값의 숫자 3을 수동 케이블로 읽으면 필드 맥락을 놓친 것입니다.

제출물: 비트 위치 표 하나와 “같은 정수값도 필드에 따라 의미가 달라지는 이유”를 설명하는 세 문장.

과제 2

충전 진단의 경계값 표 만들기 입문 → 중급

읽을 파일: Sources/WhatCableCore/Power/ChargingDiagnostic.swift의 init?과 isWarning.

할 일: 포트가 활성이고 외부 전원이 존재하며 정상적인 전원 자료가 있다는 전제에서 아래 세 경우를 판단합니다. 케이블 정격을 먼저 비교하는 분기 순서를 그대로 따라갑니다.

확장: B의 협상 결과를 90W, 89W로 바꾸고 max(5, chargerMaxW / 10)을 계산하세요. 비교가 <인지 <=인지에 따라 경계에서 결과가 달라집니다.

제출물: 결과 enum, 경고 여부, 사용자에게 보여줄 한국어 설명을 적은 표. 단순히 “느림”이라고 쓰지 말고 원인과 정상 상태를 구분합니다.

경우충전기 / 케이블 / 협상기본 기대 결과
A100W / 60W / 60WcableLimit, 경고
B100W / 100W / 30WmacLimit, 비경고
C100W / 100W / 100W, 완충fine, 비경고. 문구는 충전 중이 아닌 완충 상태를 설명
과제 3

골든 JSON에서 정보의 출처 추적하기 중급

읽을 파일: Tests/WhatCableCoreTests/Fixtures/golden/single-port-charging.json과 같은 이름의 .txt, Sources/WhatCableCore/Output/JSONFormatter.swift.

할 일: JSON의 ports[0].cable.maxWatts, ports[0].powerSources[0].negotiated.powerW, ports[0].charging.bottleneck을 찾습니다. ports[0].bulletGroups의 source마다 어떤 설명이 들어가는지도 정리합니다.

기대 결과: 이 고정 예제에서 케이블 최대 전력은 240W지만 협상 전력은 100W입니다. 충전 결과는 fine입니다. 최대 사양과 현재 계약이 달라도 정상일 수 있음을 설명할 수 있어야 합니다.

제출물: 케이블의 보고·Mac의 관측·DB의 정보·충전기의 보고를 나눈 네 칸 표. 고정 테스트 자료를 실제 연결 측정치로 부르지 않습니다.

{
  "ports": [{
    "cable": { "maxWatts": 240 },
    "charging": { "bottleneck": "fine" },
    "powerSources": [{
      "negotiated": { "powerW": 100 }
    }]
  }]
}

위 상자는 키의 위치만 보여주는 축약입니다. 전체 자료 구조를 확인할 때는 저장소의 실제 .json 파일을 기준으로 삼으세요.

과제 4

늦게 끝나는 읽기 결과를 시간선으로 재현하기 고급

읽을 파일: Sources/WhatCableDarwinBackend/Watchers/WatcherHub.swift, Tests/WhatCableDarwinTests/WatcherHubTickTests.swift.

할 일: 세대 7에서 읽기 A 시작 → 케이블 분리 알림으로 세대 8 → A 완료 순서를 그립니다. A의 포트 결과를 적용하면 안 되는 이유와 후속 읽기가 필요한 이유를 적습니다.

추가 조건: A 진행 중 정기 요청 두 번과 수동 요청 두 번이 왔다고 가정합니다. 정기 요청은 버리고 수동 요청들은 followUpQueued 하나로 합치는 흐름을 그립니다.

제출물: 시간선과 불변 조건 세 가지. “허브의 비동기 읽기는 한 번에 하나”, “중요한 변화는 재읽기”, “오래된 결과로 세대를 되돌리지 않음”을 각 코드 변수와 연결합니다.

시각 t0 시각 t1 시각 t2 읽기 A 시작 분리 알림 A 완료 기록한 세대 = 7 현재 세대 = 8 7 != 8 | 이전 결과 적용 보류 후속 읽기 한 번 예약
과제 5

케이블 이름을 잘못 붙이지 않는 조회 설계 고급

읽을 파일: Sources/WhatCableCore/Database/CableDB.swift, Tests/WhatCableCoreTests/CableDBVariantLookupTests.swift.

할 일: 동일 VID·PID 아래 VDO A인 모델 두 행, VDO B인 모델 한 행, VDO 0인 미지정 행 한 개를 가정합니다. 입력 A·B·C·0에 대해 어떤 후보 배열을 반환할지 적습니다.

기대 결과: A와 B는 정확히 일치하는 행을 우선합니다. 일치하지 않는 C는 미지정 행으로만 돌아갑니다. 동일 지문에 두 판매 브랜드가 있으면 결과도 복수일 수 있습니다. VID나 PID가 0인 경우는 별도 규칙으로 빈 배열입니다.

제출물: 분기 의사코드와 네 입력의 예상 결과. 지문을 근거로 이름을 추정하는 것과 제품 하나를 확정하는 것이 왜 다른지 덧붙입니다.

9관련 기술 심화 학습 로드맵

6주 동안 작은 입력에서 시작해 상태가 바뀌는 시스템까지 넓혀갑니다
주차학습 목표이 저장소에서 읽을 것완료 기준
1주차Swift 값 타입·enum·선택값·비트 연산PortIdentity.swift, USBPDVDO.swift, PDVDOTests.swift하나의 VDO를 손으로 해독하고 필드 맥락 설명
2주차입력 전제와 진단 분기 설계ChargingDiagnostic.swift, DataLinkDiagnostic.swift와 대응 테스트진단 결과·설명·경고를 분리한 경계값 표 작성
3주차시스템 API와 자원 수명IOKitHelpers.swift, AppleHPMInterfaceWatcher.swift, HubRead.swift데이터 출처, 읽기 순서, 해제 지점을 표시한 흐름도 작성
4주차동시성·취소·결과의 신선도WatcherHub.swift, DarwinSnapshotProvider.swift, WatcherHubTickTests.swift늦은 결과와 중복 요청을 시간선으로 설명
5주차공통 모델과 여러 출력의 일관성CableSnapshotContext.swift, JSONFormatter.swift, TextFormatter.swift, 골든 자료같은 포트의 사실이 JSON과 문장에 연결되는 경로 추적
6주차앱 수명·확장·배포App.swift, PluginRegistry.swift, 위젯 공급자, smoke-test.sh, pages.yml앱·CLI·위젯·사이트의 실행·빌드 경계 설명

9-1. 선수 지식이 부족할 때의 우회로

Swift가 처음이라면 1주차를 두 번 반복해도 괜찮습니다. 먼저 let, struct, enum, guard, if let을 익히고 2주차로 넘어가세요. IOKit 함수 이름을 전부 외우는 것보다, 함수가 받아들이는 입력과 만들 수 있는 결과의 범위를 설명하는 능력이 먼저입니다.

작은 해독 함수의 입력과 출력을 이해한 뒤 그 입력이 어디서 오고 언제 바뀌는지 추적하면, 시스템 코드도 단계적으로 읽을 수 있습니다. 처음부터 14만 줄 전체를 이해하려고 할 필요는 없습니다.

9-2. 다음 학습으로 연결할 질문

학습 기록 방법
각 주차마다 한 장의 근거표 남기기

“설명하려는 사실 → 입력 자료 → 판단 함수 → 테스트 사례 → 남은 한계” 다섯 칸을 채우세요. 기능 소개를 자신의 말로 바꾸는 것보다 코드와 설명의 연결을 검증하는 공부가 됩니다.

10핵심 키워드 사전

이름이 낯설어도, 이 프로젝트에서 맡은 역할부터 이해하면 됩니다
용어쉬운 설명
USB-C연결 단자의 형태. 모양만으로 데이터 속도나 허용 전력을 알 수는 없습니다.
USB-PD전원 공급 조건과 식별 정보 등을 주고받는 규약. 앱은 Mac이 노출하는 관련 자료를 해석합니다.
e-marker케이블 특성을 알리는 칩. 그 보고값과 실제 연결 관측값은 구분해야 합니다.
VDO정해진 비트 위치마다 의미를 담은 데이터 묶음. PDVDO가 필요한 필드를 해독합니다.
PDO전원이 제시하는 전력 공급 조건을 나타내는 객체. 여러 선택지와 선택된 조건을 구별합니다.
SOP / SOP' / SOP''PD 통신 상대를 구분하는 표기. 이 코드에서는 연결 상대와 케이블 양끝 식별 정보를 구별하는 데 사용합니다.
VID / PID공급자·제품 식별자. 케이블의 소매 브랜드 하나를 항상 확정하는 값은 아닙니다.
XIDUSB-IF 인증 식별자. 앱은 번들 목록과 대조하며, 목록에 없다는 사실만으로 위조를 단정하지 않습니다.
IOKitmacOS의 장치·드라이버 관련 객체에 접근하는 프레임워크. Darwin 수집 계층의 주요 통로입니다.
IORegistry운영체제가 관리하는 장치와 서비스의 정보 구조. 연결이 변하면 읽는 중에도 내용이 달라질 수 있습니다.
SMC시스템 관리 컨트롤러. 코드에는 전력 정보 등을 읽는 SMCPowerReader가 있습니다.
Thunderbolt / USB4연결과 전송을 다루는 기술. 앱은 관련 링크와 터널 정보를 케이블 보고값과 함께 검토합니다.
CIO 정보이 코드가 케이블 능력 판단에 활용하는 컨트롤러 쪽 신호. e-marker와 다른 근거이며 값이 다를 수도 있습니다.
터널한 연결을 통해 다른 프로토콜의 트래픽을 전달하는 통로. 장치가 어느 포트에 속하는지 추적할 때 중요합니다.
EDID화면이 자신의 지원 모드 등을 알리는 자료. Sources/WhatCableCore/Display/에 여러 형식의 해독 코드가 있습니다.
스냅샷한 번의 수집 결과를 담은 상태 묶음. 여러 서비스 읽기의 원자성을 보장한다는 뜻은 아닙니다.
폴링일정 간격으로 다시 읽는 방식. 앱은 UI 가시성에 따라 기본 간격을 조절합니다.
버스트 갱신연결 변화 뒤 여러 차례 추가로 읽는 방식. 한 번에 모든 서비스가 안정되지 않는 상황에 대응합니다.
MainActorUI와 관련된 상태 접근을 직렬로 다루는 Swift 격리 영역. 오래된 결과인지까지 자동 판단하지는 않습니다.
Sendable동시성 경계를 넘어 전달하는 안전성에 관한 타입의 약속. @unchecked는 작성자가 책임지는 부분이 있음을 뜻합니다.
세대 번호상태가 갱신됐는지 구별하는 번호. 오래 걸린 읽기가 새 상태를 덮는 것을 막는 데 사용합니다.
골든 테스트고정된 기대 출력과 결과를 비교하는 방식. JSON 키와 설명 문구가 의도치 않게 바뀌는 것을 감시합니다.
코퍼스여러 실제 사례를 모은 자료 집합. 이 코드의 일부 테스트는 공개 트리에 없는 하드웨어 자료 경로를 참조합니다.
SPM 타깃Swift Package Manager가 빌드하는 단위. 폴더가 존재한다고 자동으로 그 타깃에 편입되는 것은 아닙니다.
공증배포 앱을 Apple의 검사 절차에 제출하는 과정. 로컬 빌드나 코드 서명과 같은 단계는 아닙니다.

11참고 링크

기능 설명, 구현 근거, 공식 기술 문서를 목적에 맞게 찾아갑니다

11-1. 저장소와 제품

11-2. 구현을 다시 확인할 핵심 경로

11-3. 관련 기술 공식 문서

규격 안내 페이지의 최신 판본과 코드가 인용한 판본은 다를 수 있습니다. 코드의 주석에 적힌 표 번호, 실제 비트 위치, 회귀 테스트를 함께 대조하세요. 이 글의 판단 로직 설명은 커밋 58e588a의 구현을 기준으로 합니다.

현재 상태 (2026-10-23 확인)

항목확인 내용
선정GitHub Trending Swift Weekly #5
저장소darrylmorley/whatcable
스타 / 포크8,985 / 274 — 2026-10-23 GitHub 기준
주 언어Swift
생성일2026-05-01
최신 릴리스v1.5.0 — 2026-10-13
최근 push2026-10-13
분석 기준 커밋58e588a — 2026-10-13 · Mirror from private (a89a218b)
코드 규모Swift 422파일·147,961줄. 그중 Sources 52,594줄, Tests 91,863줄. 주석·공백 포함
다른 주요 코드C·헤더 4,208줄, Python 2,821줄, 셸 2,177줄. 직접 관리 JavaScript 737줄, Nunjucks 7,915줄
지원 기준macOS 14 이상·Apple Silicon 중심. Universal 빌드 선언과 Intel 진단 지원은 구분
라이선스GitHub 판정 NOASSERTION. 루트 LICENSE는 MIT를 기본으로 하되 별도 하위 LICENSE를 우선한다고 설명
Pro 공개 범위공개 WhatCablePlugins는 빈 등록 함수. 제품 기능 전체 구현 공개로 볼 수 없음
해석 범위커밋 58e588a의 구현·설정 기준. 가용 정보는 Mac·장치·연결 상태에 따라 달라짐
쓰기 전에 알아둘 한계
공개 소스의 범위와 관측의 범위를 먼저 확인하세요

첫째, 라이선스 배지를 한 줄로 일반화하지 마세요. README에는 MIT 배지가 있지만 루트 LICENSE는 혼합 구성과 경로별 예외를 설명하고, GitHub의 자동 판정은 NOASSERTION입니다. 재사용할 파일에 적용되는 실제 조건을 확인해야 합니다.

둘째, 배포 제품과 공개판이 같다는 전제를 두지 마세요. 공개 Pro 등록 함수는 비어 있고, TUIkit 잠금 기록은 현재 매니페스트 의존성과 일치하지 않습니다. 위젯 설정의 오래된 MARKETING_VERSION: "0.8.6"도 최종 앱 버전을 뜻하지 않습니다. 배포 스크립트가 1.5.0으로 재정의합니다.

셋째, 테스트 파일 수와 실행 보장은 다릅니다. Tests/WhatCableAppTests/는 존재하지만 이 커밋의 Package.swift에는 해당 testTarget이 없습니다. 일부 코퍼스 테스트는 공개 트리에 없는 research/customer-probes/를 참조하고, 자료가 없으면 Issue.record로 실패를 기록하는 경우도 있습니다.

넷째, 협상 전력·정격·실시간 계측을 구별하세요. 읽히지 않는 e-marker나 DB에 없는 인증 ID만으로 불량·위조를 판정할 수 없습니다. Mac과 장치가 노출하는 신호의 범위를 넘어선 원인 확정에는 다른 증거가 필요합니다.