5분 AI.
TRENDSHIFT #5 딥다이브 · 2026-08-24 분석

Screendrop 딥다이브
— Loom·CleanShot을 대체하면서 서버까지 내 계정에 두는 법

맥 메뉴바에 사는 스크린샷·화면녹화 앱입니다. 기능만 보면 흔한 카테고리인데, 코드를 열면 교과서가 "쓰지 말라"고 한 옛날 API를 이유를 적어 놓고 쓰고, 최신 프레임워크는 이유를 적어 놓고 안 씁니다. 배울 게 많은 쪽은 기능이 아니라 그 판단들입니다. (저장소: fayazara/Screendrop · ⭐986 · Swift 174파일 49,605줄 · 2026-08-24 기준)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

Screendrop이 정확히 무엇을 하는 물건인가

Screendrop맥 메뉴바에 사는 화면 캡처·녹화 앱입니다. 창이 없습니다. Dock에도 안 뜹니다. 단축키를 누르면 화면을 찍고, 찍은 것이 화면 구석에 카드처럼 떠오르고, 거기서 바로 화살표를 그리거나 개인정보를 가리거나 영상을 잘라내고, 복사하거나 저장하거나 링크로 공유합니다.

기능만 나열하면 CleanShot X나 Loom과 겹칩니다. 다른 점은 공유 링크를 만드는 서버가 개발자 것이 아니라 내 것이라는 데 있습니다. Screendrop은 서버를 운영하지 않습니다. 대신 Cloudflare에 올릴 수 있는 작은 서버 코드를 따로 주고, 앱은 그 주소와 비밀번호 한 개만 들고 있습니다.

한 컷 비유

"사진관이 아니라, 카메라 + 내 이름으로 계약한 창고"

Loom 같은 서비스는 사진관입니다. 찍어 주고, 보관해 주고, 링크도 만들어 줍니다. 편한 대신 필름이 전부 사진관 창고에 쌓입니다. 사진관이 문을 닫거나 가격을 올리면 방법이 없습니다.

Screendrop은 카메라만 파는 가게입니다. 찍는 건 내 맥에서 끝나고, 파일도 내 맥에 남습니다. 남에게 보여줄 게 생기면 그때 내 이름으로 계약한 창고(내 Cloudflare 계정)에 넣고 주소를 뽑습니다. 앱이 아는 건 창고 주소와 열쇠 하나뿐이고, 열쇠는 맥의 금고(키체인)에 있습니다.

숫자로 보면 이렇습니다. Swift 174개 파일 49,605줄, Go 2개 파일 1,061줄. 외부 라이브러리는 딱 2개(자동 업데이트용 Sparkle, Dock 아이콘 진행률용 DockProgress). 나머지 — 네트워크, JSON, 키체인, 이미지·영상 인코딩, 문자 인식, 음성 인식, 전역 단축키 — 는 전부 애플 기본 프레임워크로 직접 짰습니다.

용어
메뉴바 앱 (menu bar app)
맥 화면 맨 위 오른쪽 아이콘 줄에만 사는 앱. 배터리·와이파이 아이콘이 있는 그 줄입니다. Dock에 안 뜨고, 창도 없고, 클릭하면 작은 메뉴만 내려옵니다. 코드에서는 NSApp.setActivationPolicy(.accessory) 한 줄이 "나는 Dock에 안 나갈래"라는 선언입니다.
용어
로컬 우선 (local-first)
데이터의 정본이 내 기기에 있고, 서버는 있어도 되고 없어도 되는 설계. 반대는 "클라우드 우선" — 서버가 정본이라 인터넷이 끊기면 아무것도 못 합니다. Screendrop은 로컬 우선입니다: 업로드 버튼을 누르기 전까지 네트워크를 아예 건드리지 않습니다.

2왜 주목받는가

트렌딩 이유와 경쟁 제품 대비 강점

① 이 카테고리는 전부 유료거나 전부 클라우드다

비교 대상비용·구조Screendrop과의 차이
Loom구독제. 영상이 Loom 서버에 저장Screendrop은 영상이 내 맥에 남고, 공유할 때만 내 Cloudflare로 간다
CleanShot X유료. 클라우드는 별도 구독기능 겹침이 가장 큰 상대. Screendrop은 무료 + 소스 공개 + 서버 자가호스팅
Kap오픈소스이지만 Electron 기반Screendrop은 네이티브 Swift — 메모리·배터리·캡처 지연에서 유리
맥 기본 ⌘⇧5공짜, 이미 깔려 있음기본 도구엔 주석·블러·업로드·히스토리·타임라인 편집이 없다

② 개발 속도가 눈에 보인다

자동 업데이트 목록 파일(appcast.xml)을 세어 보면 릴리스가 26개입니다. 첫 릴리스가 2026-05-26, 최신이 2026-08-20 — 86일 동안 3.3일에 한 번씩 냈다는 뜻입니다. 저장소를 보는 사람이 "이거 방치된 프로젝트인가"를 판단할 때 별 개수보다 정확한 신호가 이것입니다.

③ 진짜 볼거리 — 최신 API를 이유를 적어 놓고 안 쓴다

애플은 2022년부터 화면 캡처는 ScreenCaptureKit을 쓰라고 안내합니다. Screendrop은 스크린샷에는 안 씁니다. 대신 맥에 원래 들어 있는 명령줄 도구 /usr/sbin/screencapture를 실행합니다. 그리고 그 이유가 주석에 적혀 있습니다.

무슨 문제였나

맥에서 창에는 그림자가 깔립니다. 흰 배경 위에 흰 창이 떠 있어도 그림자 덕분에 경계가 보이죠.

그런데 ScreenCaptureKit은 화면에 이미 그려진 그림을 그대로 가져오는 게 아니라 창들을 다시 합성해서 만듭니다. 이 과정에서 시스템이 창 사이에 넣어 준 그림자가 빠집니다. 결과: 밝은 배경 위 창이 경계가 없는 하얀 덩어리가 됩니다.

screencapture는 화면에 실제로 표시된 결과를 그대로 뜹니다. 그림자가 그대로 남습니다. "최신 API"보다 "결과가 맞는 쪽"을 고른 것입니다.

/// ScreenCaptureKit의 디스플레이 캡처는 창을 스스로 재합성하면서
/// WindowServer가 넣어 준 창 그림자를 떨어뜨린다. 그래서 밝은 배경 위
/// 창은 경계를 잃는다. screencapture는 실제 화면 합성 결과를 뜬다.
func captureFullscreen(displayID: CGDirectDisplayID?) async -> URL? {
    let displayIndex = Self.screencaptureDisplayIndex(for: displayID)
    guard let url = await runScreencapture(args: ["-D", "\(displayIndex)"]) else { return nil }
    trimEmptyMenuBarIfNeeded(at: url, displayID: displayID)
    return url
}

영상 녹화에는 반대로 ScreenCaptureKit을 씁니다. 녹화는 프레임을 초당 60번 실시간으로 받아야 하는데, 그건 screencapture로는 불가능하기 때문입니다. 같은 앱 안에서 용도에 따라 다른 답을 골랐고, 왜 그랬는지가 코드에 남아 있습니다.

배울 점
"최신 API가 항상 정답은 아니다"의 실물 증거

대부분의 튜토리얼은 "옛날 방식 → 요즘 방식"으로만 가르칩니다. 실무에서는 요즘 방식이 특정 상황에서 결과를 망치는 일이 자주 있고, 그때 필요한 건 판단과 그 판단을 남기는 주석입니다. 이 저장소는 그 예시를 최소 세 군데(스크린샷 API, 전역 단축키, 커서 처리) 갖고 있습니다.

④ 저장소가 AI 에이전트용 사용설명서를 같이 담고 있다

.agents/skills/ 폴더에 SKILL.md 4개가 들어 있습니다. "이 프로젝트를 빌드하는 법", "빌드해서 실행하는 법", "릴리스를 내는 법", 그리고 "맥 노치(카메라 홈) 모양 UI를 만드는 법". 앞의 셋은 이 저장소 전용이지만, 마지막 하나는 앱 이름이 {{AppName}} 자리표시자로 되어 있어 다른 앱에도 그대로 갖다 쓸 수 있게 쓰여 있습니다.

즉 이 프로젝트는 "나를 어떻게 빌드·배포하는가"를 README 옆에 흘려 쓴 게 아니라, 버전 관리되는 별도 문서로, AI 협업자를 독자로 두고 관리합니다. 2026년 오픈소스 저장소가 실제로 어떻게 바뀌고 있는지 보여주는 표본입니다.

함정
그런데 정작 AGENTS.md는 낡았다

저장소 루트의 AGENTS.md(에이전트·기여자용 아키텍처 설명서)를 소스와 하나씩 대조해 봤더니 5개 항목 중 4개가 현재 코드와 다릅니다. "SPM 의존성 없음"(→ 지금 2개), "Screendrop/에 하위 폴더 없음"(→ Engine/ 29개 파일), "전체화면은 ScreenCaptureKit"(→ 아님), "좌표는 0~1로 정규화"(→ 아님, 픽셀 좌표).

이건 이 저장소를 깎아내리는 얘기가 아니라 이 문서를 읽는 사람이 가져갈 가장 실용적인 교훈입니다: 빠르게 크는 코드베이스에서 사람이 손으로 쓴 설명 문서는 반드시 코드보다 뒤처집니다. 그래서 이 딥다이브의 모든 수치와 구조는 AGENTS.md가 아니라 git clone한 소스에서 직접 확인했습니다.

3기술 스택 전체 지도

무엇으로 만들었고, 무엇을 일부러 안 썼는가

앱 본체 (Swift)

사용 기술메모
UISwiftUI (MenuBarExtra + WindowGroup)메뉴바 아이콘을 직접 만들지 않고 SwiftUI 선언형으로
상태 관리@Observable (Swift Observation)구형 ObservableObject는 딱 한 곳만 — 뒤에서 설명
동시성async/await + @MainActor 기본값이 프로젝트의 핵심 설정. 아래 표 참조
스크린샷/usr/sbin/screencapture (Process 실행)ScreenCaptureKit 미사용 — 그림자 보존 때문
화면 녹화ScreenCaptureKit SCStream화면·시스템오디오·마이크 3트랙 동시
영상 인코딩AVFoundation AVAssetWriterHEVC, 60fps, 2초 조각 단위 기록
웹캠AVFoundation별도 camera.mov 트랙으로 저장
전역 단축키Carbon RegisterEventHotKey2001년 API. 왜 아직도 이걸 쓰는지는 6장
키 입력 자막CGEventTap + NSEvent 모니터둘 다 켜고 중복 제거
문자 인식Vision (OCR)화면 속 글자를 찾아 자동 가리기 후보 제안
음성 인식Speech자막·프롬프터·"말로 편집" 기능
비밀 저장Security (Keychain)업로드 토큰만. 나머지는 UserDefaults
자동 업데이트Sparkle 2.9.1 (SPM)EdDSA 서명 검증
Dock 진행률DockProgress 5.1.0 (SPM)내보내기·업로드 중 아이콘에 링 표시

외부 의존성이 2개뿐이라는 게 이 스택의 성격을 요약합니다. HTTP 클라이언트 라이브러리도, 이미지 처리 라이브러리도, 단축키 라이브러리도 없습니다. URLSession·Codable·SecItemAdd를 직접 씁니다.

동시성 설정 — 이 프로젝트에서 가장 배울 만한 빌드 설정

Xcode 프로젝트 파일(project.pbxproj)에 이렇게 박혀 있습니다.

SWIFT_VERSION = 5.0
SWIFT_APPROACHABLE_CONCURRENCY = YES
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES
용어
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
"별말 없으면 모든 코드는 메인 스레드에서 돈다"고 컴파일러에게 선언하는 설정. 보통은 반대입니다 — 기본이 "아무 스레드나"이고, UI를 만질 때만 @MainActor를 붙이죠. 그러다 하나 빠뜨리면 UI가 이상하게 깨지거나 크래시가 납니다. 이 설정을 켜면 실수의 방향이 뒤집힙니다: 빠뜨리면 "느려질" 뿐, "깨지지" 않습니다.

그러면 진짜로 백그라운드에서 돌아야 하는 것들(스트림 콜백, 인코더, 합성기)은 어떻게 할까요? 명시적으로 빠져나옵니다.

nonisolated final class ScreenRecordingCapture: NSObject,
        SCStreamOutput, SCStreamDelegate, @unchecked Sendable { ... }

이렇게 메인 스레드 밖으로 나간 클래스가 저장소 전체에 12개입니다 — 스트림 캡처(ScreenRecordingCapture), 파일 기록(ScreenRecordingWriter), 프레임 합성(StudioFrameCompositor), 웹캠(CameraCaptureEngine) 같은, 정말로 백그라운드여야 하는 것들뿐입니다. 49,605줄짜리 앱에서 동시성 버그를 찾을 때 들여다볼 곳이 nonisolated 한 단어 검색으로 12군데로 좁혀집니다.

릴리스 도구 (Go)

cmd/screendrop-release/main.go, 992줄, 외부 패키지 0개(Go 표준 라이브러리만). 이 하나가 빌드→공증→DMG→서명→appcast 갱신→GitHub 릴리스→Homebrew 캐스크 갱신까지 전부 합니다. 5장에서 단계별로 뜯습니다.

눈여겨볼 아이러니

Swift 앱 49,605줄에는 테스트가 하나도 없습니다(테스트 타깃 자체가 없음). 그런데 Go 릴리스 도구 992줄에는 단위 테스트가 2개 있습니다.

이상해 보이지만 이유는 분명합니다. 앱은 사람이 실행해 보면 틀린 걸 압니다. 릴리스 스크립트는 틀려도 릴리스가 나간 뒤에야 압니다. 되돌리기 어려운 쪽에 안전장치를 걸었습니다.

공유 인프라 (Cloudflare, 별도 저장소)

구성요소역할
Worker업로드 요청을 받아 토큰 검증 → R2로 스트리밍 전달(메모리에 안 담음) → 공유 URL 반환
R2실제 이미지·영상 파일 보관. 나갈 때 트래픽 요금이 없는 게 이 조합의 핵심
D1업로드 기록(누가·언제·어떤 파일) 저장용 가벼운 SQL 데이터베이스

앱이 아는 비밀은 업로드 토큰 하나뿐입니다. R2 접근 키 같은 건 앱에 없습니다 — Worker 안에만 있습니다. 예전 버전은 S3 자격증명을 앱이 들고 있었는데, 그 시절 키체인 항목(cloud_s3_access_key_id 등)을 삭제하는 코드가 아직 남아 있어서 설계가 바뀐 흔적을 그대로 볼 수 있습니다.

4아키텍처 심화 분석

시스템 구조도 + 핵심 설계 패턴 6가지

전체 흐름

┌─────────────────────────────────────────────────────────────────┐ │ ScreendropApp (@main) │ │ ├─ MenuBarExtra ─────────── MenuBarView (메뉴바 드롭다운) │ │ ├─ WindowGroup(id:"ANNOTATION_EDITOR", for: URL.self) │ │ └─ WindowGroup(id:"VIDEO_EDITOR", for: URL.self) │ │ AppDelegate: .accessory 정책 · 단축키 등록 · Sparkle 시작 │ │ · 종료 시 "녹화 중이면 못 끄게" 방어 │ └───────────────┬─────────────────────────────────────────────────┘ │ ⌥1 / ⌥2 / ⌥3 (Carbon 전역 단축키) ▼ ┌──────────────────────┐ │ CaptureCoordinator │ 캡처 요청을 받아 알맞은 매니저로 └──────┬────────┬──────┘ │ │ 스크린샷 │ │ 녹화 ▼ ▼ ┌────────────────┐ ┌──────────────────────────────────────┐ │ScreenshotMgr │ │ ScreenRecordingManager │ │ Process 실행 │ │ SCStream ──→ 화면 / 시스템오디오 / │ │ /usr/sbin/ │ │ 마이크 3트랙 │ │ screencapture │ │ AVAssetWriter ──→ HEVC 60fps │ │ -D / -w / -s │ │ movieFragmentInterval = 2초 │ └───────┬────────┘ │ + RecordingPointerCapture │ │ │ (커서 좌표를 input.json에 별도) │ │ └────────────┬─────────────────────────┘ │ │ ▼ ▼ ┌───────────────────────────────────────────────────────────────┐ │ 저장 (전부 JSON · DB 프레임워크 없음) │ │ ① history.json 목록 전체 (Application Support) │ │ ② foo.png.screendrop 스크린샷 1장의 주석 (사이드카) │ │ ③ bar.screendroprec/ 녹화 1건 = 폴더 통째 (아래 참조) │ └───────────────┬───────────────────────────────────────────────┘ ▼ ┌──────────────────────────────────┐ │ PreviewPanelPresenter │ 화면 구석 떠 있는 카드 더미 │ borderless · nonactivatingPanel │ sharingType = .none └──────┬──────────────┬────────────┘ (= 자기 자신은 캡처에 안 찍힘) │ │ 주석 편집 영상 편집 ▼ ▼ ┌────────────────┐ ┌────────────────────────────────┐ │ Engine/ (29파일)│ │ RecordingStudioModel (2,087줄) │ │ 벡터 도형 엔진 │ │ 자르기·속도·줌·자막·필러제거 │ │ 스냅샷 되돌리기 │ │ UndoManager 역함수 등록 │ └───────┬────────┘ └───────────┬────────────────────┘ │ ▼ │ ┌──────────────────────────────────┐ │ │ RecordingStudioExporter (1,237줄)│ │ │ CVPixelBuffer에 직접 픽셀 합성 │ │ │ 화면+캠+커서+키자막+줌 → 60fps │ │ └───────────┬──────────────────────┘ └──────────┬─────────┘ ▼ ┌────────────────────────┐ PUT /api/upload │ CloudUploader │ ───────────────────────▶ 내 Cloudflare │ Bearer <토큰> │ ① Worker가 토큰 검증 │ 토큰: Keychain │ ② R2로 스트리밍 저장 │ 주소: UserDefaults │ ③ D1에 기록 → 짧은 URL 반환 └────────────────────────┘

패턴 ① 창을 "파일 주소"로 여는 SwiftUI 구조

@main
struct ScreendropApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        MenuBarExtra("Screendrop", image: "MenuBarIcon", isInserted: $showMenuBarIcon) {
            MenuBarView()
        }
        WindowGroup("Screendrop Annotate", id: "ANNOTATION_EDITOR", for: URL.self) { value in
            AnnotationEditorWindow(url: value)
        }
        WindowGroup("Screendrop Recording Editor", id: "VIDEO_EDITOR", for: URL.self) { value in
            RecordingStudioWindow(url: value)
        }
    }
}

WindowGroup(for: URL.self)가 핵심입니다. "주석 편집기 창"이 아니라 "이 파일을 편집하는 창"으로 정의됩니다. 그러면 같은 파일을 두 번 열어도 창이 하나만 뜨고, 앱을 껐다 켜면 열려 있던 창이 알아서 복원됩니다. 창 목록을 직접 관리하는 코드가 필요 없습니다.

다만 SwiftUI만으로 안 되는 것들이 있어 AppDelegate가 따로 붙습니다: Dock에 안 뜨게 하기, Carbon 단축키 등록, Sparkle 시작, 그리고 "녹화 중에 종료 누르면 막기". 마지막 것이 특히 실무적입니다 — 경고창을 띄우고 .terminateLater를 돌려줘서 파일 쓰기가 끝날 때까지 앱이 안 죽게 붙잡습니다.

패턴 ② 커서를 일부러 안 찍고, 나중에 그린다

녹화 설정에 showsCursor = false가 있습니다. 마우스 커서를 영상에 안 담습니다. 대신 커서가 어디 있었는지를 좌표로만 따로 기록해 두고(input.json), 편집·내보내기 때 가짜 커서를 부드럽게 그려 넣습니다.

왜 이렇게 하나

화면에 찍힌 커서는 지울 수 없습니다. 픽셀에 섞여 버렸으니까요. 확대(줌)를 걸면 커서도 같이 뭉개져 커지고, 커서 모양을 바꾸거나 클릭 효과를 넣을 수도 없습니다.

좌표만 갖고 있으면 전부 나중에 정할 수 있습니다 — 커서를 크게, 클릭할 때 파문 효과, 손떨림 보정, 아예 숨기기까지. "픽셀로 굽기 전에 데이터로 남긴다"는 원칙이고, 이건 영상 편집 도구 설계의 반복되는 교훈입니다.

패턴 ③ 크래시해도 영상이 살아남는 기록 방식

let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
// 조각 단위 무비: 중간에 죽어도 마지막 조각까지는 재생 가능한 파일이 남는다.
writer.movieFragmentInterval = CMTime(seconds: 2, preferredTimescale: 600)
let bitRate = max(20_000_000, videoWidth * videoHeight * 4)
let videoSettings: [String: Any] = [
    AVVideoCodecKey: AVVideoCodecType.hevc,
    AVVideoWidthKey: videoWidth, AVVideoHeightKey: videoHeight,
    AVVideoCompressionPropertiesKey: [
        AVVideoAverageBitRateKey: bitRate,
        AVVideoExpectedSourceFrameRateKey: 60,
        AVVideoMaxKeyFrameIntervalKey: 60
    ] as [String: Any]
]

보통 동영상 파일은 맨 마지막에 목차를 써야 완성됩니다. 녹화 중에 앱이 죽으면 목차가 없어서 파일 전체가 열리지 않습니다. movieFragmentInterval = 2초2초마다 작은 목차를 붙이라는 뜻이고, 그러면 죽어도 직전 2초까지는 멀쩡히 재생됩니다.

소스 주석에 이 원칙이 한 줄로 적혀 있습니다: "시작된 녹화는 조용히 버려지지 않는다." 같은 원칙이 저장 위치에도 적용돼 있어서, 녹화 폴더는 임시 폴더가 아니라 ~/Library/Application Support에 만들어집니다 — OS가 임시 폴더를 청소하다 영상을 지우는 일을 막기 위해서입니다.

패턴 ④ 좌표계를 하나로 고정하고, 화면은 "카메라"로 본다

주석(화살표·사각형·블러 등)의 좌표를 어디에 맞출 것인가는 이런 편집기의 최대 설계 결정입니다. Screendrop의 답은 Engine/Shape.swift 주석에 있습니다.

두 가지 방법

방법 A(정규화): 좌표를 0~1로 저장. "이미지 가로의 30% 지점". 해상도가 달라져도 유지되지만, 도형을 회전·확대할 때마다 가로세로 비율 때문에 계산이 지저분해집니다.

방법 B(픽셀 고정, Screendrop이 택한 쪽): 좌표를 그 스크린샷의 픽셀 그대로 저장. 화면에 보이는 확대·이동은 "카메라"라는 별도 계산으로만 처리하고, 도형 데이터는 절대 안 건드립니다. 내보낼 때는 카메라를 끄고 1:1로 그리면 끝입니다.

이 선택의 보상이 주석에 그대로 적혀 있습니다 — "캔버스가 그 위의 순수한 카메라가 되고, 그래서 모든 도구가 같은 회전/크기조절 동작을 공짜로 얻는다." 도구를 새로 추가할 때 회전·크기조절 코드를 다시 안 짜도 된다는 뜻입니다.

흥미로운 건 같은 앱 안에서 녹화 쪽은 반대로 0~1 정규화를 씁니다. 커서 좌표(input.json)는 나중에 1080p로도 4K로도 렌더링될 수 있으니 해상도에 독립적이어야 하거든요. 규칙을 통일한 게 아니라, 각 데이터가 살아갈 미래에 맞춰 다르게 정한 것입니다.

패턴 ⑤ 되돌리기를 두 가지 방식으로 구현했다

주석 엔진 (Engine/Document.swift)영상 스튜디오 (RecordingStudioModel)
방식문서 전체를 통째로 스냅샷UndoManager + 역함수 클로저 등록
코드snapshot() / restore(_:)이전 값을 캡처해 같은 함수를 다시 호출하는 클로저 등록
장점단순. 버그가 거의 안 생긴다맥 기본 편집 메뉴에 "자르기 취소"처럼 이름이 뜬다
단점도형이 많아지면 메모리를 먹는다모든 변경 함수마다 역함수를 손으로 등록해야 한다
private func applyClipTimeline(_ requested: RecordingClipTimeline, ...,
                               actionName: String) {
    let next = requested.normalized(to: sourceDuration)
    guard !next.segments.isEmpty, next != clipTimeline else { return }
    let previousTimeline = clipTimeline          // 이전 값을 붙잡아 두고
    let previousSelection = selectedClipID
    registerUndo(actionName) { target in         // 그 값으로 자기 자신을 다시 호출
        target.applyClipTimeline(previousTimeline, selectedID: previousSelection, ...)
    }
    clipTimeline = next
}

등록한 클로저가 같은 함수를 다시 부르기 때문에, 되돌리기를 실행하는 순간 "다시 실행(redo)"이 자동으로 등록됩니다. 한 함수로 undo/redo가 둘 다 되는 구조입니다. 한 앱 안에 두 방식이 공존하니 비교 학습 자료로 드물게 좋습니다.

패턴 ⑥ 녹화 한 건 = 폴더 한 개 (자체 포맷)

static let directoryExtension    = "screendroprec"
static let screenFileName       = "screen.mov"      // 화면
static let cameraFileName       = "camera.mov"      // 웹캠
static let pointerCaptureFileName = "input.json"    // 커서·키 입력 기록
static let captureManifestFileName = "capture.json" // 촬영 당시 설정
static let editDocumentFileName = "edit.json"       // 저장한 편집 내용
static let draftDocumentFileName = "edit.draft.json"// 자동 저장 (별도!)
static let renderStampFileName  = "render.json"     // 내보낸 파일이 최신인지 증명
static let posterFileName       = "poster.jpg"      // 썸네일

맥의 .rtfd.pages처럼 겉으론 파일 하나로 보이지만 실제로는 폴더인 구조입니다. 애플이 제공하는 NSDocument/FileWrapper를 안 쓰고 FileManager로 직접 만들었습니다.

여기서 눈여겨볼 세 가지:

5디렉토리 구조 해부

어디에 무엇이 있고, 어디부터 읽어야 하는가
Screendrop/ 3.1MB · 216파일 (.git 제외) ├── Screendrop/ ← 앱 본체. 188파일 중 174개가 .swift │ ├── Engine/ 29파일 · 벡터 도형 엔진 (유일한 하위 폴더) │ ├── *.swift 나머지 145개가 여기 평면 배치 │ ├── Screendrop.icon/ Xcode 26 Icon Composer 포맷 (SVG 레이어) │ ├── Info.plist 권한 문구 4종 + Sparkle 설정 │ └── Screendrop.entitlements 키 딱 2개 (마이크·카메라) ├── .agents/skills/ AI 에이전트용 SKILL.md 4개 │ ├── build-screendrop/ "빌드는 이 명령 하나" │ ├── build-and-run-screendrop/ 스킴 3개 자동 판별 + 재실행 │ ├── release-screendrop/ 릴리스 운영 매뉴얼 + 문제해결표 │ └── macos-notch-ui/ 노치 UI 레시피 (다른 앱에도 재사용 가능) │ └── references/ NotchShape.swift · NotchWindow.swift ├── cmd/screendrop-release/ Go 릴리스 CLI (992줄 + 테스트 69줄) ├── Screendrop.xcodeproj/ 스킴 3개: Screendrop / Dev / Demo ├── OpenShot.xcodeproj/ ⚠︎ 빈 껍데기 — 옛 이름의 흔적 ├── appcast.xml Sparkle 업데이트 목록 (26개 릴리스) ├── AGENTS.md ⚠︎ 낡음 (2장 참조) ├── go.mod go 1.25.4 · 외부 패키지 0개 └── LICENSE CC0-1.0

파일 크기 순위 — 어디에 살이 붙어 있는가

줄 수파일무엇
3,193RecordingStudioWindow.swift영상 편집기 화면 전체. 한 파일 안에 뷰 구조체 28개(27개가 private)
2,087RecordingStudioModel.swift편집기 창 하나의 상태·로직 전부를 담은 단일 클래스
1,350ScreenRecordingManager.swiftSCStream 캡처 + AVAssetWriter 기록
1,237RecordingStudioExporter.swift최종 합성·내보내기 (픽셀 직접 조작)
857ScreenshotPreviewStack.swift떠 있는 미리보기 카드 더미
823AnnotationEditorModel.swift스크린샷 주석 편집기 모델
774RecordingPointerCapture.swift녹화 중 커서·클릭 좌표 기록 (4장 패턴 ②)
770RecordingClipTimelineView.swift영상 편집 타임라인 UI

이어서 AnnotationCanvas.swift(769) · Engine/AnnoEditorInteraction.swift(717)가 붙습니다. 2천~3천 줄짜리 파일이 상위에 있는 게 눈에 띌 겁니다. 교과서적 MVVM이라면 RecordingStudioModel 하나를 열댓 개로 쪼갰을 텐데, 이 프로젝트는 편집기 창 하나당 "만능 객체" 하나로 갑니다. 재생·자르기·속도·줌·자막·필러 제거·내보내기·공유가 전부 그 안에 있습니다.

이게 나쁜 설계인가?

정답이 있는 문제가 아닙니다. 쪼개면 파일당 읽기는 쉬워지지만 "자르기를 바꾸면 줌 큐도 같이 옮겨야 한다" 같은 연쇄를 여러 파일에 흩뿌리게 됩니다. 영상 편집기는 상태끼리 서로 얽혀 있는 대표적인 도메인이라, 한곳에 모아 두는 쪽이 실제로 덜 틀릴 수 있습니다.

다만 대가는 분명합니다: 여러 명이 동시에 손대면 충돌하고, 테스트가 어렵고(그래서 없고), 신규 기여자의 진입 문턱이 높습니다. 1인 개발 + 3.3일마다 릴리스라는 이 프로젝트의 조건에는 맞는 선택입니다.

Go 릴리스 도구가 하는 일 (992줄 단계별)

go run ./cmd/screendrop-release ① ensureXcodeDeveloperDir Xcode 경로 확인 ② bumpVersion project.pbxproj의 버전 문자열을 직접 수정 → 커밋 ③ xcodebuild archive 빌드 ④ xcodebuild -exportArchive Developer ID로 서명해 .app 추출 ⑤ xcrun notarytool submit --wait 애플에 공증 제출 후 결과 대기 ⑥ xcrun stapler staple 공증 도장을 앱에 박음 ⑦ create-dmg DMG 이미지 제작 ⑧ sign_update (Sparkle) EdDSA 서명 → 출력에서 서명값 정규식 추출 ⑨ writeAppcast appcast.xml 맨 앞에 새 <item> 삽입 (테스트 有) ⑩ gh release create/edit GitHub 릴리스 생성 — 이미 있으면 교체(멱등) ⑪ updateHomebrewCask 별도 tap 저장소의 캐스크 갱신 + push

runCmdRetry라는 헬퍼가 네트워크가 필요한 단계들을 재시도로 감쌉니다. 그리고 이 도구의 존재 이유가 release-screendrop/SKILL.md에 적혀 있습니다 — "채팅 세션에서 바로 릴리스를 낼 수 있도록". 사람이 아니라 AI 에이전트가 실행할 것을 전제로 설계된 릴리스 파이프라인입니다.

먼저 읽으면 좋은 순서

  1. ScreendropApp.swift (220줄) — 앱 전체 구조가 한 화면에 들어온다
  2. ScreenshotManager.swift — 2장의 "왜 ScreenCaptureKit을 안 쓰나" 주석을 원문으로
  3. PreviewWindowCaptureExclusion.swift — 가장 짧고 가장 놀라운 파일 (아래 6장)
  4. Engine/Shape.swift — 좌표계 결정과 그 근거
  5. RecordingSession.swift — 폴더 번들 포맷 정의
  6. CloudCredentialStore.swift — 키체인 사용법의 교과서적 예시

6학습 포인트 (기술별)

이 저장소에서 실제로 훔쳐 갈 수 있는 것들

① 한 줄짜리 마법 — 내 창을 화면 캡처에서 빼기

화면 녹화 앱의 고전적 문제: 녹화하는 앱 자신의 UI가 녹화 화면에 찍힙니다. 미리보기 카드, 녹화 컨트롤바 같은 것들이요. Screendrop의 해결책 전부가 이 한 줄입니다.

private func applyCaptureVisibility(to window: NSWindow) {
    window.sharingType = Self.includesAppWindowsInCaptures ? .readOnly : .none
}

NSWindow.sharingType = .none은 시스템에게 "이 창은 화면 공유·녹화 대상에서 빼 달라"고 말하는 속성입니다. ScreenCaptureKit 쪽에서 필터로 걸러낼 필요도, 좌표를 계산해 잘라낼 필요도 없습니다. 화면 녹화 도구를 만들 때 이걸 모르면 며칠을 헤맵니다.

② 2001년 API가 아직도 정답인 자리 — 전역 단축키

용어
전역 단축키 (global hotkey)
내 앱이 화면 뒤에 있어도 눌리면 반응하는 단축키. 어려운 건 "감지"가 아니라 "가로채기"입니다 — 다른 앱에 그 키가 전달되지 않게 막아야 하거든요.
방법감지가로채기권한 필요
NSEvent.addGlobalMonitor없음
Carbon RegisterEventHotKey없음
CGEventTap입력 모니터링 허용 필요

Screendrop은 캡처 단축키에 Carbon을 씁니다. 2001년에 나온 C API이고, 애플이 "레거시"라 부르지만 권한 없이 키를 가로챌 수 있는 유일한 방법이라 2026년에도 표준입니다. C 콜백에서 Swift 객체를 되찾는 방식이 볼 만합니다.

// 등록할 때: self의 주소를 그냥 포인터로 넘긴다
Unmanaged.passUnretained(self).toOpaque()

// C 콜백 안에서: 그 포인터를 다시 Swift 객체로 되돌린다
let manager = Unmanaged<HotkeyManager>.fromOpaque(userData).takeUnretainedValue()
manager.handleHotKey(id: hotKeyID.id)

한편 키 입력 자막 기능은 어떤 키를 눌렀는지 표시만 하면 되므로 가로챌 필요가 없고, 대신 모든 키를 봐야 하니 CGEventTapNSEvent 모니터를 둘 다 켭니다. 그러면 같은 키가 두 번 들어오는데, 이걸 이렇게 처리합니다.

/// 같은 물리 키 입력이 CGEventTap과 NSEvent 모니터 양쪽에서 도착할 수 있다.
/// duplicateEventWindow 안의 중복은 버린다.
private var lastProcessedEventTimes: [Int: TimeInterval] = [:]
private static let duplicateEventWindow: TimeInterval = 0.05

③ 키체인 쓰는 법 — 30줄이면 끝난다

private static func setKeychainItem(key: String, value: String) {
    let data = Data(value.utf8)
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecAttrService as String: keychainService,
    ]
    SecItemDelete(query as CFDictionary)        // 있으면 지우고
    var newQuery = query
    newQuery[kSecValueData as String] = data
    SecItemAdd(newQuery as CFDictionary, nil)   // 새로 넣는다 (update보다 단순)
}

"수정"이라는 개념 없이 지우고 다시 넣습니다. SecItemUpdate는 항목이 있는지 먼저 확인해야 해서 분기가 늘어나는데, 삭제는 없어도 에러가 아니라서 이 방식이 더 짧고 안전합니다.

중요한 건 무엇을 어디 넣었는가입니다: 업로드 토큰(비밀) → 키체인, Worker 주소(비밀 아님) → UserDefaults. 비밀만 금고에 넣습니다. 전부 키체인에 넣으면 느려지고 디버깅이 괴로워집니다.

④ 외부 프로그램 실행 시 절대 잊으면 안 되는 것

49,605줄 전체에서 Process()를 쓴 곳은 딱 2곳입니다 — screencapture와, 배경 이미지 팩을 푸는 /usr/bin/ditto. 그 두 번째에 교훈이 하나 붙어 있습니다.

함정
Pipe를 비우기 전에 waitUntilExit()를 부르면 멈춘다

외부 프로그램의 출력을 Pipe로 받으면서 waitUntilExit()를 먼저 부르면 영원히 안 끝날 수 있습니다. 파이프 버퍼(보통 64KB)가 꽉 차면 상대 프로그램이 "누가 좀 읽어 가야 계속 쓰지"하며 멈추고, 내 쪽은 "끝나야 읽지"하며 기다립니다. 서로를 기다리는 교착 상태입니다.

Screendrop은 파이프를 먼저 다 읽고 나서 waitUntilExit()를 부릅니다. 이 순서가 정답입니다.

그리고 비동기 처리 방식도 볼 만합니다 — Process는 옛날 블로킹 API인데, 이걸 async/await 세계로 끌어올리는 표준 다리가 withCheckedContinuation입니다.

return await withCheckedContinuation { continuation in
    DispatchQueue.global(qos: .userInitiated).async {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
        process.arguments = args + ["-x", "-t", "png", filePath]
        try? process.run()
        process.waitUntilExit()
        continuation.resume(returning: process.terminationStatus == 0
            ? URL(fileURLWithPath: filePath) : nil)
    }
}

⑤ 왜 딱 한 클래스만 옛날 방식(Combine)을 쓰는가

앱 전체가 @Observable인데 UpdaterManager 하나만 ObservableObject + Combine입니다. 실수가 아닙니다 — Sparkle이 canCheckForUpdatesKVO(옛날 Objective-C 관찰 방식)로 노출하기 때문이고, KVO를 Swift 값으로 바꾸는 다리가 Combine의 .publisher(for:)입니다. "낡은 걸 남겨 둔 게 아니라, 외부 라이브러리와 맞닿은 한 지점에만 남긴 것"입니다.

⑥ 주석 엔진의 출처 — 이 폴더는 통째로 재사용품이다

Engine/ 29개 파일의 헤더 주석에 출처가 적혀 있습니다: "드로잉 앱의 Model/Shape.swift에서 이식, 화이트보드엔 없는 도구(가리기·스포트라이트·번호 콜아웃)까지 넓힘." 저자가 예전에 만든 별도 프로젝트의 2D 벡터 엔진을 통째로 가져와 확장한 것입니다.

그 안에는 perfect-freehand(tldraw·Excalidraw가 쓰는 필압 자유곡선 알고리즘)의 Swift 포팅도 있습니다 — thinning·smoothing·streamline 같은 필드명과 펜 감속 공식 t*0.65 + sin(t*π/2)*0.35가 그대로 남아 있어 확인됩니다.

⑦ 픽셀에 직접 그리는 영상 합성

최종 내보내기(RecordingStudioExporter.swift)는 AVVideoComposition이나 Core Animation 레이어를 쓰지 않습니다. CVPixelBuffer의 메모리 주소를 잠그고 거기에 직접 픽셀을 씁니다. 화면·웹캠·가짜 커서·키 입력 자막·자막·줌 화면을 프레임마다 손으로 합칩니다.

튜토리얼에서는 거의 안 나오는 저수준 기법입니다. 대신 합성 규칙을 100% 내 마음대로 정할 수 있습니다 — 프레임워크가 지원하지 않는 효과라도 넣을 수 있죠. 대가는 성능·정확도를 전부 직접 책임진다는 것.

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

"일단 돌려 보자"가 대부분 실패하는 이유
먼저 확인
macOS 26.4 이상 — 이게 제일 큰 벽입니다

README와 appcast의 sparkle:minimumSystemVersion26.4를 요구합니다. 빌드 설정에는 MACOSX_DEPLOYMENT_TARGET = 26.0도 섞여 있어 완전히 일치하지는 않는데, 배포 쪽 기준은 26.4입니다. 2026년 8월 기준 상당수 맥이 아직 여기 못 미칩니다. 구형 맥에서는 소스를 읽는 것까지가 현실적인 한계입니다.

항목요구비고
OSmacOS 26.4+일부 기능은 26.4에서만 되는 API를 씀 (음성 분석 등)
빌드Xcode 26.4 베타정식판으로는 안 열릴 수 있음
Swift언어 모드 5.0SWIFT_DEFAULT_ACTOR_ISOLATION 등 최신 옵션 사용
샌드박스꺼짐 (ENABLE_APP_SANDBOX = NO)화면 캡처 + 외부 프로그램 실행 때문에 불가피
Hardened Runtime켜짐공증(notarization)에 필요
권한(엔타이틀먼트)마이크·카메라 2개뿐샌드박스가 꺼져 있어 나머지는 불필요
사용 권한 문구카메라·마이크·화면기록·입력모니터링Info.plist의 4개 NS...UsageDescription
GPU/CPUApple Silicon 권장HEVC 60fps 인코딩 + 실시간 합성 부하
디스크녹화 길이에 비례해 빠르게 증가비트레이트 최소 20Mbps — 10분이면 1.5GB 안팎

배포된 앱을 처음 열면 "확인되지 않은 개발자" 경고가 뜰 수 있습니다. 앱을 우클릭 → 열기, 또는 시스템 설정 → 개인정보 보호 및 보안에서 허용하면 됩니다.

확인 필요
문서와 코드가 어긋난 지점 2가지 (2026-08-24 소스 기준)

① FFmpeg: README는 압축·변환에 brew install ffmpeg가 필요하다고 합니다. 그런데 소스 전체에서 ffmpeg 실행 호출이 하나도 없습니다. 자르기는 AVAssetExportSession, 인코딩은 AVAssetWriter — 전부 애플 기본 프레임워크입니다. 소스 주석도 ffmpeg를 "ffmpeg 시절의 트림 서비스"처럼 과거형으로 언급합니다. 내부적으로 네이티브 전환이 끝났는데 README가 안 따라온 것으로 보입니다(추정이며, 그렇게 적힌 설명은 못 찾았습니다).

② 라이선스: 저장소 라이선스 파일은 CC0-1.0(사실상 퍼블릭 도메인)인데, README 하단에는 "첫 공개 릴리스 전에 라이선스 정보를 추가할 예정"이라고 적혀 있습니다. 상업적으로 쓸 계획이라면 이 상태 그대로 믿지 말고 저자에게 확인하는 게 안전합니다.

조사 방법 주의
GitHub 웹페이지의 숫자는 캐시가 낡을 수 있습니다

이 문서를 쓰면서 실제로 걸린 함정입니다. GitHub 저장소 페이지를 그대로 읽었더니 별 240개·릴리스 최신 v0.21.3(6월)로 나왔는데, API로 다시 확인하니 별 986개였고 최신 릴리스는 appcast와 같은 0.31.2(8월 20일)였습니다. 렌더링된 페이지가 몇 시간~며칠 묵은 캐시였던 것입니다.

빠르게 뜨는 저장소를 조사할 때는 웹페이지가 아니라 API나 클론한 소스를 기준으로 삼으세요. 위 수치들은 그렇게 다시 확인한 값입니다.

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

난이도 순 5개 — 맥이 있으면 1·2번은 오늘 됩니다
과제 1

메뉴바에만 사는 앱 만들기 난이도 ★☆☆☆☆ · 30분

Xcode에서 macOS App 템플릿으로 새 프로젝트를 만들고, WindowGroup을 지운 뒤 MenuBarExtra만 남깁니다. AppDelegate에서 NSApp.setActivationPolicy(.accessory)를 호출하세요.

확인: 실행했을 때 Dock에 아이콘이 안 뜨고 메뉴바에만 보이면 성공. 막히는 지점: .accessoryapplicationDidFinishLaunching이 아닌 곳에서 부르면 Dock 아이콘이 잠깐 깜빡입니다.

과제 2

스크린샷 두 방식을 직접 비교하기 난이도 ★★☆☆☆ · 1~2시간

같은 화면을 (A) Process/usr/sbin/screencapture -D 1 -x -t png out_a.png, (B) SCScreenshotManager로 각각 찍어 나란히 놓고 봅니다. 밝은 배경 위에 밝은 창을 하나 띄워 두는 게 요령입니다.

확인할 것: 창 그림자가 A에는 있고 B에는 없는가? 2장에서 읽은 주석이 사실인지 내 눈으로 검증하는 과제입니다. 덤: 소요 시간도 재 보세요 — Process 방식이 프로세스 실행 오버헤드로 더 느릴 겁니다. 그 대가를 왜 감수했는지 이해하게 됩니다.

과제 3

"내 창은 캡처에 안 찍히기" 재현 난이도 ★★☆☆☆ · 2시간

과제 1의 앱에 떠 있는 작은 패널을 하나 붙입니다(NSPanel, styleMask: [.borderless, .nonactivatingPanel], isFloatingPanel = true). 그 상태로 ⌘⇧5로 화면을 녹화해 패널이 찍히는지 봅니다.

그다음 panel.sharingType = .none 한 줄을 넣고 다시 녹화합니다. 확인: 화면에는 보이는데 녹화본에는 없으면 성공. 이어서: canBecomeKeytrue로, canBecomeMainfalse로 오버라이드하면 무엇이 달라지는지 체감해 보세요(키보드는 받되 앱의 주 창은 되지 않음).

과제 4

Cloudflare Worker + R2 업로드 엔드포인트 직접 만들기 난이도 ★★★☆☆ · 반나절

Screendrop 앱 없이 서버 쪽만 만들어 봅니다. Worker에 PUT /api/upload를 열고, Authorization: Bearer 헤더의 토큰을 검증한 뒤, 요청 본문을 메모리에 담지 말고 그대로 R2로 흘려보냅니다(스트리밍). D1에 파일명·크기·시각을 한 줄 남기고 짧은 URL을 돌려주세요.

테스트: curl -X PUT --data-binary @test.png -H "Authorization: Bearer 토큰" https://내워커/api/upload. 핵심 학습: "왜 스트리밍인가" — 500MB 영상을 메모리에 올리면 Worker 메모리 한도에서 죽습니다. 덤: GET /api/ping도 만들어 "연결 확인" 버튼의 서버 쪽을 완성해 보세요.

과제 5

커서를 "나중에 그리는" 녹화기 만들기 난이도 ★★★★★ · 며칠

SCStream으로 showsCursor = false로 녹화하면서, 동시에 타이머로 마우스 좌표를 0~1로 정규화해 JSON에 기록합니다. 녹화가 끝나면 그 JSON을 읽어 CVPixelBuffer에 직접 원을 그려 커서를 합성한 영상을 만듭니다.

단계별로: ① 좌표 기록만 → ② 정지 이미지에 커서 그리기 → ③ 프레임마다 그리기 → ④ 좌표 사이를 보간해 부드럽게. 막히는 지점: 녹화 프레임 시각과 커서 기록 시각을 맞추는 것(CMTime 기준으로 통일해야 합니다).

여기까지 하면 Screendrop의 가장 어려운 부분을 축소판으로 재현한 것이고, 영상 편집 도구가 왜 그렇게 만들어지는지 몸으로 알게 됩니다.

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

주차별 — 맥 네이티브 앱 개발로 방향을 잡았다면
주차주제목표 / 산출물
1주차 SwiftUI 앱 수명주기 App·Scene·WindowGroup·MenuBarExtra의 차이를 손으로. 산출물: 과제 1 완성 + 창이 있는 버전/없는 버전 둘 다
2주차 AppKit과의 다리 NSApplicationDelegateAdaptor·NSWindow·NSPanel·NSViewRepresentable. SwiftUI만으로 안 되는 일이 무엇인지 목록으로 정리해 보기. 산출물: 과제 3
3주차 Swift 동시성 async/await@MainActorSendablenonisolated. 목표는 문법이 아니라 "이 코드가 어느 스레드에서 도는지 컴파일러가 어떻게 아는가"를 설명할 수 있게 되는 것
4주차 권한과 시스템 접근 TCC(화면 기록·입력 모니터링·마이크) 권한 흐름, CGPreflightListenEventAccess, 샌드박스 켜고/끄고의 차이. 산출물: 권한 거부 상태를 정상 처리하는 앱
5~6주차 ScreenCaptureKit SCShareableContentSCContentFilterSCStream → 콜백에서 CMSampleBuffer 받기. 산출물: 화면을 창에 실시간 미리보기
7~8주차 AVFoundation 쓰기 AVAssetWriter·AVAssetWriterInput·CMTime·코덱 설정. 산출물: 5주차 스트림을 mov 파일로 저장 + movieFragmentInterval 있고/없고 강제 종료 실험
9주차 픽셀 다루기 CVPixelBuffer 잠그고 쓰기, 픽셀 포맷(BGRA), Core Image와의 관계. 산출물: 녹화본 위에 워터마크 합성
10주차 저장 설계 왜 SwiftData가 아니라 JSON인가. 스키마 진화(decodeIfPresent ?? 기본값), 사이드카 vs 폴더 번들, draft/saved 분리. 산출물: 버전 필드가 있는 문서 포맷 하나 설계
11주차 배포 파이프라인 서명 → 공증(notarization) → 스테이플 → DMG → Sparkle appcast → Homebrew 캐스크. 산출물: 내 앱을 실제로 서명·공증까지
12주차 자가호스팅 백엔드 Cloudflare Worker + R2 + D1, 토큰 인증, 스트리밍 업로드. 산출물: 과제 4 완성 + 앱에서 실제 업로드 연결
순서에 대한 조언
3주차(동시성)를 건너뛰지 마세요

5주차부터 나오는 SCStream 콜백은 백그라운드 스레드에서 옵니다. 동시성을 모른 채 들어가면 "가끔 크래시하는데 이유를 모르겠다"에서 몇 주를 씁니다. Screendrop이 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor를 켠 이유가 정확히 이 고통을 컴파일 오류로 앞당기기 위해서입니다.

10핵심 키워드 사전

이 문서에 나온 용어를 한 줄씩
용어
MenuBarExtraSwiftUI로 메뉴바 아이콘·드롭다운을 만드는 Scene. 예전엔 NSStatusItem으로 직접 만들어야 했다
@ObservableSwift의 최신 상태 관찰 방식. 클래스에 붙이면 SwiftUI가 바뀐 값만 골라 다시 그린다. 구형은 ObservableObject
@MainActor"이 코드는 메인(UI) 스레드에서만 돈다"는 표시. 컴파일러가 위반을 잡아 준다
nonisolated@MainActor 기본값에서 빠져나오는 표시. 백그라운드로 나가야 하는 곳에만 붙인다
Sendable"이 값은 스레드를 넘어가도 안전하다"는 약속. @unchecked Sendable은 "내가 책임진다"는 수동 선언
ScreenCaptureKit (SCStream)애플의 최신 화면 캡처 프레임워크. 실시간 프레임 스트림을 준다. Screendrop은 녹화에만 쓴다
screencapture맥에 기본 내장된 명령줄 스크린샷 도구. -D 전체화면 / -w 창 / -s 영역
AVAssetWriter프레임을 받아 영상 파일로 써 주는 AVFoundation 클래스
movieFragmentIntervalN초마다 재생 가능한 조각을 마감하는 설정. 크래시해도 그 지점까지는 살아남는다
HEVC (H.265)H.264 다음 세대 코덱. 같은 화질에 파일이 더 작지만 인코딩이 무겁다
CVPixelBuffer영상 프레임 한 장이 담긴 메모리 덩어리. 주소를 잠그고 직접 픽셀을 쓸 수 있다
Carbon / RegisterEventHotKey2001년 맥 API. 권한 없이 전역 단축키를 가로챌 수 있는 유일한 방법이라 아직 쓰인다
CGEventTap시스템 입력 이벤트를 엿보는 저수준 장치. 강력하지만 입력 모니터링 권한이 필요하다
UnmanagedSwift 객체를 C 포인터로 바꿔 넘기고 되찾는 도구. C 콜백과 대화할 때 쓴다
키체인 (Keychain)맥·아이폰의 비밀 저장소. kSecClassGenericPassword가 앱 토큰용 기본 종류
sharingType = .noneNSWindow에 붙이면 화면 공유·녹화에서 그 창이 빠진다
NSPanel보조용 떠 있는 창. nonactivatingPanel이면 클릭해도 앱이 앞으로 튀어나오지 않는다
사이드카 (sidecar) 파일원본 옆에 같은 이름 + 다른 확장자로 두는 부가 정보 파일. 여기선 foo.png.screendrop
폴더 번들겉으론 파일 하나, 속은 폴더인 구조. 맥의 .app·.rtfd가 그렇고 여기선 .screendroprec
Sparkle맥 앱 자동 업데이트 표준 라이브러리. appcast.xml을 읽고 EdDSA 서명을 검증한다
공증 (notarization)애플에 앱을 보내 악성코드 검사를 받는 절차. 안 하면 "확인되지 않은 개발자" 경고가 뜬다
Cloudflare R2S3 호환 파일 저장소. 다운로드 트래픽 요금이 없다 — 이 조합의 핵심
Cloudflare D1Worker에서 쓰는 가벼운 SQL 데이터베이스(SQLite 기반)
perfect-freehand필압에 따라 굵기가 변하는 자유곡선 알고리즘. tldraw·Excalidraw가 쓴다
Icon ComposerXcode 26의 새 앱 아이콘 포맷. png 대신 icon.json + SVG 레이어로 관리

11참고 링크

더 파고들 곳

이 프로젝트

저장소 안에서 먼저 읽을 파일

애플 공식 문서

배경 지식 · 비교해 볼 저장소