ZoomIt을 macOS로 옮긴 화면 확대·주석·녹화 도구.app 번들을 조립한다. 그래서 이 저장소는 "macOS 앱을 Xcode 없이 어떻게 만드는가"의 훌륭한 교과서이기도 하다.
(저장소: microsoft/ZoomitForMac · Swift 6 / macOS 14+ · SwiftPM 단일 패키지(라이브러리 1 + 실행 2) · 소스 38개 Swift 파일 ≈ 14,200줄 · 라이선스 MIT © 2026 Microsoft · TrendShift Daily #3)
ZoomIt은 원래 윈도우용 Sysinternals 도구로, 발표자·강사·개발자 사이에서 20년 가까이 사랑받아 온 화면 확대 & 주석 유틸리티다. 단축키 한 번이면 화면이 확대되고, 그 위에 펜으로 밑줄을 긋거나 사각형으로 강조하고, 타이핑으로 메모를 남길 수 있다. 이 macOS 판은 그 경험을 맥에 그대로 가져오면서, 최신 macOS API에 맞게 기능을 더 얹었다.
제공하는 기능을 크게 묶으면 다음과 같다. (1) 확대 — 화면을 얼려서 확대하는 정적 줌과, 실시간 화면을 확대하는 라이브 줌. (2) 주석 — 펜·직선·사각형·타원·화살표·형광펜, 그리고 화면 위에 글자를 치는 타이핑 주석. (3) 캡처 — 영역 스니핑(잘라내기), 화면 녹화(MP4, 시스템 오디오·마이크·웹캠 PiP 포함), 스크롤 파노라마 캡처. (4) 부가 — 선택 영역의 글자를 인식해 복사하는 OCR, 발표 쉬는 시간을 위한 브레이크 타이머.
기술적으로 한 줄 요약하면, "메뉴바에 상주하며 전역 단축키를 듣다가, 화면을 캡처해 투명한 오버레이 창에 확대·렌더링하고 그 위에 그림을 그리게 해 주는 순수 AppKit 앱"이다. 여기에 녹화(AVFoundation)·OCR(Vision)·파노라마 스티칭(Accelerate) 같은 무거운 기능이 모듈로 붙는다.
평소엔 메뉴바에 숨어 있다가, Control+1을 누르면 화면 전체 위에 투명한 유리판(오버레이 창)이 깔린다. 그 유리판은 방금 찍은 화면 사진을 확대해 보여주고(돋보기), 마우스로 그 위에 그림을 그릴 수 있다(형광펜). Esc를 누르면 유리판이 사라지고 원래 화면으로 돌아온다.
녹화·파노라마·OCR도 같은 원리다 — "화면을 캡처해서 → 무언가를 하고 → 결과를 클립보드나 파일로 내보낸다"는 한 가지 뼈대를 모드만 바꿔 재사용한다.
ZoomIt for Mac이 2026년 TrendShift Daily 상위에 오른 이유는 기능 자체의 신박함보다 "오랫동안 윈도우에만 있던 필수 발표 도구가, 서드파티 클론이 아니라 마이크로소프트 Sysinternals 공식으로 macOS에 나왔다"는 사건성에 있다. 맥에서 발표·강의하는 사람들이 수년간 아쉬워하던 빈자리를 정확히 채웠다.
단순한 취미 프로젝트가 아니라 Microsoft 공식 저장소, MIT 라이선스다. 코드를 전부 읽고 고치고 배포할 수 있으며, 설치는 brew install --cask zoomit 한 줄이다. "대기업이 만든 프로덕션 품질의 macOS 앱 소스를 통째로 볼 수 있다"는 점이 학습자에게 특히 매력적이다.
요즘 신규 맥 앱은 SwiftUI로 시작하는 경우가 많은데, 이 앱은 UI 전부를 AppKit으로 짰다. 전역 단축키·투명 오버레이 창·커서 제어·저수준 그리기처럼 SwiftUI로는 까다로운 저수준 제어가 핵심이기 때문이다. "언제 AppKit을 골라야 하는가"에 대한 실전 답안지다.
대부분의 맥 앱 튜토리얼은 Xcode 프로젝트에서 시작한다. 이 저장소엔 .xcodeproj가 없다. Package.swift 하나로 빌드하고, Scripts/build-app.sh가 실행 파일을 받아 아이콘·Info.plist·리소스를 붙여 .app 번들로 조립한 뒤 코드 서명까지 한다. "앱 번들이란 실제로 무엇으로 이뤄지는가"를 스크립트 수준에서 낱낱이 보여준다.
소스 곳곳의 주석이 단순 설명이 아니라 macOS의 함정과 그 회피 이유를 적어 놨다. 예: 라이브 줌 오버레이가 자기 자신을 다시 캡처해 무한 확대되는 피드백 루프를 막으려고 창을 sharingType = .none으로 둔 이유, TCC(권한) 기록이 번들 ID와 코드 서명 조건에 함께 묶여 있어 개발용 빌드와 정식 빌드의 ID를 분리해야 하는 이유 등. 그 자체로 훌륭한 macOS 실무 문서다.
"맥 사용자가 오래 기다린 기능적 공백"을 "대기업 공식 + 오픈소스 + 현대 macOS API의 모범 사례"라는 형태로 채웠기 때문에, 실사용자와 학습자 양쪽에서 동시에 관심을 받았다.
이 앱은 서버·프론트 같은 구분이 없는 단일 네이티브 데스크톱 앱이다. 그래서 스택은 "언어·빌드 → UI 뼈대 → 기능별 시스템 프레임워크"의 층으로 읽는 게 정확하다. 핵심은 UI는 전부 AppKit 한 가지로 통일하고, 확대·녹화·OCR·파노라마 같은 기능마다 애플이 제공하는 전용 프레임워크를 골라 붙였다는 점이다.
| 항목 | 내용 |
|---|---|
| 언어 | Swift 6.0 — StrictConcurrency 업커밍 기능 활성화(데이터 경합을 컴파일 단계에서 차단) |
| 플랫폼 | macOS 14(Sonoma) 이상 |
| 빌드 도구 | Swift Package Manager 단독 — Package.swift 하나. Xcode 프로젝트 없음 |
| 산출물 | 라이브러리 ZoomItMacCore 1개 + 실행 파일 2개(ZoomIt 본체, ZoomItMacSelfTest 테스트) |
| 배포 | Homebrew Cask(brew install --cask zoomit), Universal 바이너리(arm64 + x86_64) |
| 레이어 | 프레임워크 | 맡은 일 |
|---|---|---|
| UI 뼈대 | AppKit | 메뉴바 아이콘, 오버레이 창(NSWindow), 그리기 뷰(NSView), 설정 창 — UI 전부. 25개 파일이 import |
| 화면 캡처 | ScreenCaptureKit · CoreGraphics | 디스플레이를 이미지/스트림으로 캡처. 특정 창 제외가 필요하면 SCK, 단순 전체 캡처는 CGDisplayCreateImage |
| 녹화 · 영상 | AVFoundation · AVKit · CoreMedia | MP4 녹화(AVAssetWriter), 녹화 후 편집기(트림·페이드·미리보기) |
| 전역 단축키 | Carbon.HIToolbox | RegisterEventHotKey로 시스템 전역 핫키 등록(앱이 포커스 없어도 반응) |
| 문자 인식 | Vision | 온디바이스 OCR(선택 영역의 글자를 인식해 클립보드로). 추가 권한 불필요 |
| 영상 처리 | Accelerate · CoreImage · ImageIO | 파노라마 스티칭의 신호 처리(NCC/SAD), 이미지 인코딩·내보내기 |
| 시스템 통합 | ServiceManagement · IOKit | 로그인 시 자동 실행(SMAppService), 녹화·타이머 중 절전 방지(전원 어서션) |
| 부가 | UniformTypeIdentifiers · AVFoundation | 저장 패널의 파일 타입 지정, 오디오 장치 열거 |
AppKit이 건물의 골조와 벽(공간 자체)이라면, 나머지 프레임워크는 방마다 들여놓은 전문 장비다 — 캡처실엔 ScreenCaptureKit, 녹화실엔 AVFoundation, 판독실엔 Vision. 골조는 하나로 통일하고, 방(기능)마다 최적의 장비를 골라 배치한 셈이다.
이 앱의 심장은 ModeCoordinator라는 상태기계다. 모든 입력(전역 단축키)은 AppCommand라는 명령으로 변환되어 ModeCoordinator로 들어가고, 코디네이터는 현재 AppMode(9개 모드 중 하나)에 따라 적절한 하위 컨트롤러로 일을 넘긴다. "입력을 명령으로 정규화하고, 상태기계가 그 명령을 모드 전환·컨트롤러 호출로 번역한다"는 단방향 흐름이 전체 설계를 지탱한다.
AppCommand는 "무엇을 하라"는 의도(activateStaticZoom, snipRegion, toggleRecording…)만 담은 enum이고, AppMode는 "지금 어떤 상태인가"(idle, staticZoom, recording…)를 담은 enum이다. 단축키·메뉴·설정 등 입력 경로가 무엇이든 전부 AppCommand로 모이므로, 새 입력을 추가해도 상태기계 로직은 건드릴 필요가 없다. 입력과 상태를 분리한 전형적인 커맨드 패턴이다.
확대 화면은 테두리 없는 전체화면 NSWindow를 .screenSaver 레벨(거의 최상단)에 띄워 그린다. 여기서 macOS 특유의 함정이 하나 있다.
라이브 줌은 "실시간 화면을 캡처해 오버레이에 확대 표시"한다. 그런데 그 오버레이 창도 화면의 일부라서, 다음 캡처에 오버레이가 다시 찍히면 확대된 화면이 또 확대되는 거울 속 거울 현상이 생긴다.
해결: 라이브 줌 창은 window.sharingType = .none으로 두어 ScreenCaptureKit이 이 창을 캡처 대상에서 빼도록 한다. 반대로 정적 줌·주석 창은 .readOnly로 둔다 — 녹화 중 화면에 그린 주석은 영상에 남아야 하기 때문이다.
화면 캡처(ScreenCaptureService)와 권한(PermissionService)은 프로토콜로 추상화돼 있다. 실제 앱은 ScreenCaptureKitCaptureService·SystemPermissionService 구현을 쓰지만, 테스트에선 가짜 구현을 끼울 수 있다. 덕분에 화면·권한 같은 "OS에 묶인" 부분을 실제 화면 없이도 검증할 수 있다.
UI를 만지는 거의 모든 타입에 @MainActor가 붙어 있어 메인 스레드 접근이 컴파일 단계에서 보장된다. 반대로 파노라마 스티처는 의도적으로 AppKit 의존을 끊어(Sendable 값 타입만 사용) 백그라운드에서 무거운 계산을 돌리고, 결과만 메인으로 넘긴다. StrictConcurrency가 이 경계를 강제한다.
저장소 최상위는 단순하다. Package.swift(빌드 정의), Sources/(모든 코드), Scripts/(번들 조립·서명 스크립트), ZoomItInfo.plist(권한 설명 문구), 문서 파일들. 코드는 전부 Sources/ 아래 세 타깃에 들어 있다.
| 모듈 | 규모 | 핵심 파일 / 역할 |
|---|---|---|
| Capture | 12파일 · ≈5,833줄 | RecordingController(1,251) · PanoramaStitcher(1,608) · VideoClipEditorController(821). 앱에서 가장 무거운 심장부 |
| Settings | 2파일 · ≈2,115줄 | SettingsWindowController(1,563) 탭형 설정 UI · SettingsStore(552) UserDefaults 저장 |
| SelfTest | 1파일 · ≈1,731줄 | SelfTestRunner — 화면 없이 돌리는 회귀 테스트 모음 |
| Overlay | 4파일 · ≈1,677줄 | ZoomCanvasView(962) 확대·주석 렌더링 · OverlayWindowController(177) 창 관리 |
| App | 8파일 · ≈1,207줄 | AppDelegate(221) 의존성 조립 · DemoTypeController(572) 자동 타이핑 시연 |
| Core | 3파일 · ≈546줄 | ModeCoordinator(512) 상태기계 + AppMode·AppCommand 두 enum |
| Annotations | 2파일 · ≈401줄 | AnnotationController(332) 펜/도형/타이핑 그리기 로직 |
| Hotkeys | 1파일 · 368줄 | HotkeyService — Carbon 전역 핫키 |
| Permissions / Display | 109 / 32줄 | TCC 권한 확인 · 디스플레이 관리 |
구조를 읽는 요령: 진입점은 얇고(main 8줄), 로직은 라이브러리(Core)에 몰아넣고, 기능은 Capture에 집중돼 있다. 즉 "어떤 기능이 궁금하면 Capture/를, 앱이 어떻게 조립되는지 궁금하면 App/AppDelegate.swift와 Core/ModeCoordinator.swift를 보라"가 지도의 핵심이다.
이 앱의 진짜 가치는 기능이 아니라 "Xcode 없이 SwiftPM만으로 서명·공증 가능한 .app을 만들고, macOS의 권한(TCC) 시스템과 정면으로 싸워 이기는" 실무 노하우가 코드와 스크립트에 그대로 박제돼 있다는 점이다. 아래는 주제별로 무엇을 배울지와 곧바로 실습할 거리다.
build-app.sh는 swift build 산출물(맨몸 실행 파일)을 받아 Contents/MacOS에 넣고, sips+iconutil로 아이콘 세트를 만들고, Info.plist를 써 넣고, codesign으로 서명한다. ".app이 사실은 규칙에 맞게 정리된 폴더일 뿐"임을 눈으로 배운다.
백미: 맨몸 SwiftPM 바이너리엔 Info.plist가 없어 마이크 권한 프롬프트가 못 뜨는데, 이를 링커 플래그(-sectcreate __TEXT __info_plist)로 실행 파일 안에 plist를 직접 심어 해결한다. 실습: 내 CLI 도구에 이 기법으로 권한 설명을 심어 보기.
README가 사실상 "맥 앱 배포 실무 가이드"다. 공증에 필요한 하드닝 런타임에서는 카메라·마이크가 엔타이틀먼트 없이는 TCC 승인을 받아도 거부된다는 점, 화면 녹화는 순수 TCC라 엔타이틀먼트가 필요 없다는 점을 명확히 구분한다.
그래서 개발용 임시 서명 빌드와 정식 Developer ID 빌드가 같은 번들 ID(com.sysinternals.zoomitmac)를 쓰면, 시스템 설정엔 "허용"으로 보여도 실제로는 캡처가 거부되는 유령 상태가 생긴다. 이 앱은 개발 빌드를 ...zoomitmac.dev로 번들 ID 자체를 분리해 근본 원인을 없앴다. tccutil reset ScreenCapture <bundle-id>로 초기화하는 법까지 문서화돼 있다.
특정 창을 제외해야 하면(예: 자기 오버레이 빼기) ScreenCaptureKit을, 단순히 디스플레이 전체를 한 장 찍으면 되면 CGDisplayCreateImage를 쓴다 — 상황에 맞는 API 선택 기준을 코드로 보여준다. 실습: 두 방식으로 각각 스크린샷을 찍어 속도·유연성을 비교.
앱이 포커스가 없어도 반응하는 시스템 전역 단축키는 아직도 Carbon의 RegisterEventHotKey가 정석이다. 이 앱은 라이브 줌 중에만 Option+↑/↓를 임시 등록/해제하는데, Control+↑/↓는 macOS가 미션 컨트롤에 예약해 뺏기지 않기 때문이라는 이유까지 주석에 있다. 실습: 나만의 전역 단축키 등록/해제 미니 앱 만들기.
macOS엔 스크롤 캡처 API가 없어서, 윈도우 ZoomIt의 알고리즘을 Swift로 이식했다. 프레임을 루마(밝기)로 줄이고, 엣지 밀도 1D 신호를 만든 뒤, 정규화 상호상관(NCC)으로 대략 정렬하고 SAD로 미세 보정한다. Accelerate로 가속하고, 반복 프레임·고정 헤더를 걸러낸다. 이미지 정렬의 교과서적 예제다.
GUI 앱은 자동 테스트가 어렵다. 이 앱은 ZoomItMacSelfTest라는 별도 실행 파일을 두어, 화면 없이 뷰포트 수학·주석 생명주기·설정 저장·파노라마 스티처 회귀를 검증한다(swift run ZoomItMacSelfTest). "테스트하기 쉬운 부분(순수 로직)을 UI에서 떼어내는" 설계의 실물이다.
| 구분 | 요구사항 |
|---|---|
| 운영체제 | macOS 14(Sonoma) 이상 — Package.swift가 .macOS(.v14)로 못 박음(ScreenCaptureKit·SMAppService 최신 API 사용) |
| CPU | Apple Silicon 또는 Intel. release 빌드는 기본 Universal(둘 다), debug는 빌드 머신 네이티브만(속도 우선) |
| 빌드 도구(기본) | Xcode 커맨드라인 툴(swift build·swift run이면 충분) |
| 빌드 도구(Universal) | Universal은 Xcode의 xcbuild를 거치므로 풀 Xcode 필요. CLT만 있으면 스크립트가 경고 후 네이티브 빌드로 폴백 |
| 권한 | 필요 시점 |
|---|---|
| 화면 녹화(Screen Recording) | 필수 — 모든 캡처·확대 기능의 전제. 순수 TCC라 엔타이틀먼트는 불필요 |
| 마이크(Microphone) | 녹화에 마이크 오디오를 켤 때만. 하드닝 런타임에선 엔타이틀먼트도 함께 필요 |
| 카메라(Camera) | 웹캠 PiP 오버레이를 켤 때만. 마이크와 동일하게 엔타이틀먼트 필요 |
Info.plist의 LSUIElement=true, 코드의 setActivationPolicy(.accessory)). 그래서 실행해도 Dock에 안 뜨고, 커서 숨김 같은 동작을 위해 필요할 때만 NSApp.activate로 잠깐 활성화한다.swift run ZoomIt으로 띄워 보자git clone 후 swift build → swift run ZoomItMacSelfTest를 돌려 테스트가 전부 통과하는지 본다. 그다음 Scripts/build-app.sh로 .app을 만들어 open ".build/ZoomIt (Dev).app"으로 실행. "SwiftPM 산출물이 어떻게 앱이 되는가"를 손으로 겪는 게 목표.
Settings/SettingsStore.swift의 기본 단축키 값을 하나 골라 바꾼 뒤(HotkeyService가 이를 읽어 등록), 재실행해 새 단축키가 먹는지 확인한다. 명령이 AppCommand로 흐르는 경로를 따라가 보는 게 핵심.
AppCommand에 setColor/setTool 항목이 있고 키보드 R/G/B…로 색을 고른다. 새 색(예: 보라)이나 새 도형을 하나 추가해 AnnotationController의 렌더링까지 이어 본다. enum → 상태기계 → 뷰 렌더링의 전 구간을 관통하는 연습.
PanoramaStitcher는 AppKit 의존이 없어 순수 함수처럼 테스트할 수 있다. 세로로 조금씩 밀리는 합성 프레임(색 그라디언트)을 코드로 만들어 스티칭 결과가 기대대로 이어 붙는지 셀프테스트를 추가해 본다. NCC/SAD 정렬을 눈으로 이해하는 과제.
SnipController가 캡처한 이미지를 ImageExporter로 클립보드/PNG에 내보내는 경로를 따라가, 예를 들어 저장 시 파일명에 타임스탬프 자동 삽입 같은 기능을 더한다. 나아가 OCR 결과를 텍스트 파일로도 저장하는 옵션을 OcrService 경로에 붙여 본다. 여러 모듈을 가로지르는 실전 확장.
| 주차 | 주제 | 무엇을 · 어디서 |
|---|---|---|
| 1주차 | Swift + AppKit 기초 | Swift 6 문법, NSApplication·NSWindow·NSView·NSStatusItem(메뉴바). 이 앱의 main.swift·AppDelegate를 읽으며 "AppKit 앱의 뼈대"를 익힌다. |
| 2주차 | 창·오버레이·그리기 | 테두리 없는 창, 창 레벨/스페이스, NSView.draw 커스텀 렌더링, NSCursor 제어. Overlay/·Annotations/ 정독. |
| 3주차 | 화면 캡처 · 녹화 | ScreenCaptureKit(SCStream/스크린샷), CGDisplayCreateImage, AVFoundation AVAssetWriter로 MP4 쓰기. Capture/ScreenCaptureService·RecordingController. |
| 4주차 | 시스템 통합 | Carbon 전역 핫키, Vision OCR, SMAppService 로그인 시작, IOKit 절전 방지. Hotkeys/·OcrService·LaunchAtLogin. |
| 5주차 | 배포 · 서명 · 공증 | codesign, 엔타이틀먼트, 하드닝 런타임, notarytool/stapler, TCC 리셋. Scripts/build-app.sh와 README 배포 절 정독. |
| 6주차 | 동시성 + 컴퓨터 비전 | Swift 6 @MainActor/Sendable/StrictConcurrency, Accelerate로 신호 처리. PanoramaStitcher의 NCC/SAD 정렬을 손으로 재현. |
1~2주는 "화면에 무언가를 그린다"(UI), 3~4주는 "화면과 시스템을 읽고 쓴다"(캡처·권한·입력), 5주는 "남에게 나눠준다"(배포), 6주는 "무겁고 어려운 계산"(동시성·CV) 순이다. 각 단계가 이전 단계의 결과물 위에서만 의미가 있으므로 순서를 건너뛰지 않는 게 좋다.
| 용어 | 뜻 |
|---|---|
| Sysinternals | 마이크로소프트의 고급 윈도우 시스템 유틸리티 모음. ZoomIt이 그 대표 도구 중 하나 |
| AppKit | macOS의 전통적 UI 프레임워크(창·뷰·컨트롤). SwiftUI 이전부터 쓰인 저수준·고제어 API |
| ScreenCaptureKit | macOS 12.3+의 최신 화면 캡처 API. 특정 창 제외 등 세밀한 제어 가능(SCK로 줄임) |
| CGDisplayCreateImage | 디스플레이 한 장을 통째로 캡처하는 CoreGraphics 함수. 단순 전체 캡처에 사용 |
| TCC | Transparency, Consent, and Control. macOS의 개인정보 권한 시스템(화면 녹화·마이크·카메라 승인) |
| Hardened Runtime | 코드 주입·메모리 조작을 막는 실행 보호. 공증(notarization)의 전제 조건 |
| Notarization(공증) | 애플에 앱을 제출해 악성 여부 검사를 받고 티켓을 받는 절차. 없으면 Gatekeeper가 실행을 막음 |
| Entitlement | 앱이 특정 보호 자원(카메라·마이크 등)에 접근할 수 있음을 서명에 박아 넣는 권리 선언 |
| Designated Requirement | "이 앱이 진짜 이 앱인지" 판별하는 코드서명 조건. TCC 권한이 번들 ID와 함께 여기에 묶임 |
| ad-hoc 서명 | 인증서 없이(-) 하는 임시 서명. 개발용. 재빌드 시 권한이 초기화될 수 있음 |
| Universal 바이너리 | Apple Silicon(arm64)과 Intel(x86_64) 코드를 한 파일에 담아 두 기종에서 네이티브 실행 |
| SMAppService | macOS 13+의 로그인 시작(login item) 등록 API. 앱 번들일 때만 동작 |
| RegisterEventHotKey | Carbon의 전역 단축키 등록 함수. 앱이 포커스 없어도 반응하는 시스템 핫키에 사용 |
| @MainActor | Swift 동시성에서 "이 코드는 메인 스레드에서만"을 컴파일러가 보장하게 하는 표시 |
| Sendable | 스레드 경계를 안전하게 넘어갈 수 있는 값 타입 표시. 스티처가 백그라운드로 넘길 데이터에 사용 |
| NCC / SAD | 정규화 상호상관 / 절대차 합. 두 이미지의 어긋남(shift)을 찾는 정렬 지표. 파노라마 스티칭의 핵심 |
| LSUIElement | Dock 아이콘 없이 백그라운드/메뉴바에만 있는 앱임을 알리는 Info.plist 키 |
| sharingType | NSWindow가 화면 캡처에 잡히는 방식(.none=제외 / .readOnly=포함). 자기 캡처 피드백 방지에 사용 |
| 구분 | 링크 |
|---|---|
| 저장소 | github.com/microsoft/ZoomitForMac |
| 설치(Homebrew) | brew install --cask microsoft/sysinternalstap/zoomit |
| 원조 ZoomIt(Windows) | learn.microsoft.com/sysinternals/downloads/zoomit |
| ScreenCaptureKit | developer.apple.com/documentation/screencapturekit |
| 공증(Notarization) | developer.apple.com/documentation/security/notarizing-macos-software-before-distribution |
| 로그인 시작(SMAppService) | developer.apple.com/documentation/servicemanagement/smappservice |
| Vision 텍스트 인식 | developer.apple.com/documentation/vision/recognizing-text-in-images |
| 라이선스 | MIT © 2026 Microsoft Corporation |