
지난 글에서 홈랩 RTX 5090의 상태를 HTTP API로 뽑아냈다. gpu.xssh.org/status를 치면 사용률, VRAM, 온도, 돌고 있는 프로세스까지 JSON으로 나온다. 문제는 이걸 매번 터미널에서 확인하기 귀찮다는 거다.
macOS 바탕화면 귀퉁이에 위젯 하나 띄워놓고 GPU 상태가 계속 보이면 좋겠다 싶었다. WidgetKit을 쓰면 된다. 60초마다 API를 폴링해서 VRAM 사용량과 idle 여부를 갱신하는 Medium 사이즈 위젯을 만들었다.
이 프로젝트의 특이한 점은 개발 환경이다. 나는 WSL 리눅스에서 Claude Code로 코딩하는데, Swift 자체는 텍스트 파일이니까 어디서든 쓸 수 있다. 문제는 .xcodeproj 파일. 이건 macOS의 Xcode에서만 만들 수 있는 바이너리에 가까운 XML이다.
해결책은 xcodegen이다. project.yml이라는 YAML 파일 하나로 프로젝트 구조를 정의하고, xcodegen generate 한 방이면 .xcodeproj가 생성된다. 덕분에 WSL에서 코드를 쓰고 macOS에서 git pull 받아서 빌드만 하면 끝이다.
macOS 쪽에 GPUMonitor 앱과 GPUWidgetExtension 두 타겟이 있다. 위젯이 60초마다 WSL의 FastAPI 서버를 HTTPS로 호출하고, 앱은 같은 데이터를 더 넓은 화면에 보여준다. API 클라이언트와 데이터 모델은 Shared/ 디렉토리에서 양쪽이 공유한다.
macOS/iOS 위젯은 앱과 별개 프로세스로 돌아간다. 직접 타이머를 돌리는 게 아니라 시스템이 "다음에 언제 데이터를 가져올지" 스케줄링하는 Timeline 기반 아키텍처다.
func getTimeline(in context: Context,
completion: @escaping (Timeline<GPUEntry>) -> Void) {
Task {
let entry = await fetchEntry()
let nextUpdate = Calendar.current.date(
byAdding: .second, value: 60, to: entry.date)!
completion(Timeline(entries: [entry],
policy: .after(nextUpdate)))
}
}getTimeline()에서 API를 호출하고 entry를 만든 뒤, 60초 후에 다시 호출해달라고 시스템에 요청한다. .after(nextUpdate) 정책이 핵심이다. 실시간 데이터니까 주기를 짧게 잡았고, 날씨 같은 정적 데이터라면 15분이나 1시간으로 늘리면 된다.
숫자만 보여주면 "32GB 중 20GB 쓰는 게 많은 건가?" 하고 생각해야 한다. 색상을 붙이면 생각할 필요가 없다.
ProgressView(value: gpu.memoryPercent)
.tint(gpu.memoryPercent > 0.9 ? .red
: gpu.memoryPercent > 0.7 ? .orange
: .green)70% 이하면 초록, 70~90%면 주황, 90% 이상이면 빨강. GPU idle 여부도 초록 점/주황 점으로 표시해서 바탕화면을 슬쩍 보기만 해도 지금 GPU를 쓸 수 있는지 바로 알 수 있다.
WidgetKit Extension은 메인 앱과 별도 프로세스다. 처음에는 앱 코드를 import로 가져오려고 했는데 빌드 에러가 났다. Extension에서 앱 타겟의 소스를 직접 참조할 수 없기 때문이다.
결국 Shared/ 디렉토리를 만들어 GPUStatus(데이터 모델)와 GPUAPIClient(HTTP 클라이언트)를 넣고, xcodegen에서 양쪽 타겟 모두에 포함시켰다.
targets:
GPUMonitor: # 메인 앱
type: application
platform: macOS
sources: [GPUMonitor] # Shared/ 포함
GPUWidgetExtension: # 위젯
type: appExtension
platform: macOS
sources: [GPUWidget, GPUMonitor/Shared] # Shared/ 공유주의할 점은 Shared 코드에 UI 의존성을 넣으면 안 된다는 거다. SwiftUI import가 들어가면 Extension 빌드가 꼬일 수 있다. 순수 데이터 모델과 네트워크 레이어만 넣어야 한다.
처음에는 수동으로 .xcodeproj를 만들려고 했다. 당연히 안 됐다. WSL에 Xcode가 없으니까. xcodegen을 도입하니 project.yml 하나로 앱 타겟, Extension 타겟, App Sandbox 권한, Outgoing Network 설정까지 전부 텍스트로 관리할 수 있게 됐다. git diff도 깔끔하게 나온다.
개발 워크플로우가 깔끔해졌다. WSL에서 Claude Code로 Swift 코드를 쓰고 git push. macOS에서 git pull 받아서 xcodegen generate && xcodebuild. macOS 네이티브 프로젝트를 리눅스에서 개발하려면 이런 코드 기반 프로젝트 생성 도구가 필수라는 걸 배웠다.
API를 폴링하는 macOS 위젯 패턴은 GPU 모니터 말고도 쓸 데가 많다. 서버 상태 대시보드, CI 빌드 결과, 심지어 날씨 앱도 같은 구조로 만들 수 있다. TimelineProvider에서 fetch하고, .after()로 다음 갱신 시점을 잡고, Shared 디렉토리로 앱과 코드를 공유하면 된다.
그리고 Claude Code에서 Swift를 쓸 수 있다. xcodegen만 있으면 Xcode 없이도 프로젝트 구조를 잡을 수 있으니까. 빌드만 macOS에서 하면 된다.