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

Easydict 딥다이브
— 선택한 한 문장이 번역이 되기까지

데스크탑 앱

빠른 판단

무엇
선택 텍스트·화면 OCR을 여러 사전·번역·AI 엔진에 전달해 비교하는 macOS 번역 앱
이럴 때
문서를 읽다가 선택한 문장이나 화면 글자를 조회하고 서로 다른 번역 서비스의 응답을 함께 확인할 때
입력
선택한 텍스트·스크린샷 또는 직접 입력한 질의
결과
여러 사전·번역·AI 서비스의 결과
단어를 선택하거나 화면을 캡처하면 여러 사전·번역 서비스의 결과를 한 창에 보여 주는 macOS 앱입니다. 이 글은 입력 → 언어 감지 → 서비스 선택 → 결과 갱신을 소스 코드로 따라갑니다. SwiftUI와 Objective-C의 연결, 중단 가능한 스트리밍, 문단을 복원하는 OCR에서 실제 제품을 만드는 방법을 배웁니다. 저장소: tisfeng/Easydict · ⭐15,047 · 포크 794 · Swift · GPL-3.0 · 2026-10-23 GitHub 기준. 코드 해설은 커밋 69600ed 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

번역 엔진과 번역 앱의 차이부터 이해하기

Easydict는 macOS에서 텍스트를 가져와 사전·번역·AI 서비스에 연결하는 통합 작업 도구입니다. 자체적으로 거대한 번역 모델을 학습시키기보다, 사용자의 입력과 여러 엔진 사이를 연결하는 데 집중합니다. 이 저장소의 핵심은 번역 알고리즘보다, 번역까지 이어지는 사용자의 동작을 하나의 흐름으로 만드는 설계입니다.

예를 들어 영어 문서를 읽다가 한 문장을 선택하고 Option + D를 누르면 선택 영역을 읽어 질의 창으로 보냅니다. 텍스트를 선택할 수 없는 이미지라면 Option + S로 화면 일부를 캡처합니다. Apple Vision이 글자를 읽고, 결과를 번역 서비스에 전달합니다. 입력 창의 기본 단축키는 Option + A입니다.

한 문장 비유

여러 통역사가 앉아 있는 안내 데스크

손님이 종이 문서를 가져오든, 사진을 가져오든 안내 직원이 먼저 읽을 수 있는 문장으로 정리합니다. 그다음 필요한 통역사에게 요청하고, 먼저 도착한 답부터 보여 줍니다.

Easydict의 선택·OCR 기능은 문장을 받아 적는 직원, QueryServiceFactory는 통역사 명부, 질의 창은 답을 보여 주는 게시판에 해당합니다. 통역사를 추가해도 문장을 받는 절차를 매번 새로 만들 필요가 없습니다.

사용자 행동입력의 형태소스에서 찾아볼 지점
직접 입력문자열EZBaseQueryViewController의 startQueryText:actionType:
다른 앱에서 선택접근성 API·브라우저 등의 선택 영역SelectionWorkflow.getSelectedTextSnapshot
스크린샷 번역이미지 → OCR 문자열AppleOCREngine.recognizeText
조용한 OCR이미지 → 클립보드startOCRImage:actionType:autoQuery:의 ScreenshotOCR 분기

단축키의 실제 기본값은 Easydict/Swift/Feature/Shortcut/Model/ShortcutManager+Default.swift의 setDefaultGlobalShortcutKeys()에서 확인할 수 있습니다. 표의 Objective-C 컨트롤러는 Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m에 있습니다.

용어
OCR과 번역은 서로 다른 단계
OCR은 이미지 속 글자를 문자열로 바꾸는 작업입니다. 번역은 그 문자열을 다른 언어로 바꾸는 작업입니다. 글자를 기기에서 읽었다고 해서, 이어지는 번역까지 기기 안에서 수행된다는 뜻은 아닙니다.
읽는 목표
한 번의 질의에 어떤 객체가 관여하는지 설명해 보기

처음에는 모든 Swift 문법을 알 필요가 없습니다. 입력을 담는 QueryModel, 실행을 담당하는 QueryService, 표시할 내용을 담는 QueryResult의 역할부터 구분하면 이후 코드가 훨씬 잘 읽힙니다.

2왜 주목받는가

트렌딩 순위와, 코드에서 읽을 수 있는 제품의 매력

2-1. 번역 사이트를 여는 비용을 줄인다

2026-10-23 분석에서 GitHub Trending Swift Weekly #10으로 선정된 프로젝트입니다. 스타 15,047개와 포크 794개는 관심의 규모를 보여 주지만, 어떤 기능 때문에 순위가 올랐는지까지 증명하지는 않습니다. 아래 이유는 실제 기능과 구현을 바탕으로 한 해석입니다.

번역은 문장을 복사하고, 창을 전환하고, 붙여 넣는 짧은 동작이 반복되는 작업입니다. Easydict는 선택 영역과 화면 캡처를 입력으로 받아 이 반복을 줄입니다. 자주 쓰는 도구의 가치는 한 번의 놀라운 결과뿐 아니라, 매번 반복하던 작은 동작을 얼마나 줄여 주는가에서도 나옵니다.

2-2. 하나의 엔진에 묶이지 않는 구조

README는 20개 이상의 사전·번역·AI·로컬 모델·CLI 서비스를 소개합니다. QueryServiceFactory.swift의 등록 목록은 29개 항목입니다. 여기에는 번역 공급자뿐 아니라 SummaryService와 PolishingService 같은 작업도 포함됩니다. 이를 모두 서로 다른 번역 회사 29곳으로 세면 안 됩니다.

Apple Dictionary와 MDict를 통한 로컬 사전 조회, Apple 시스템 번역, OpenAI·Gemini·Claude 같은 원격 서비스, Ollama, CLI 기반 서비스가 같은 앱에 공존합니다. 이용 가능한 언어·요금·로그인 조건은 서비스마다 다릅니다. 여러 결과를 모으는 구조가 모든 공급자의 품질이나 가용성을 보장하지는 않습니다.

비교 대상주요 쓰임Easydict에서 얻는 장점대가·조건
웹 번역기 단독 사용한 서비스에 텍스트 입력선택·OCR 입력과 여러 서비스 결과를 한 흐름에 연결앱 설치와 권한 설정 필요
macOS 사전 앱설치된 사전의 뜻 조회Apple Dictionary 결과와 문장 번역·AI 결과를 함께 활용사전 자체의 언어·콘텐츠 범위는 그대로 적용
단축어로 기능 조합개인이 정한 자동화창·서비스 설정·결과 상태 관리까지 갖춘 UI세부 동작은 앱이 제공하는 범위 안에서 설정
로컬 모델 직접 호출모델 서버에 요청 전송Ollama 응답도 기존 번역 창에 표시모델 설치·서버 운영·메모리는 별도 부담

이 비교는 작업 흐름의 차이입니다. 번역 정확도나 응답 속도를 측정한 경쟁 제품 벤치마크는 아닙니다. README는 Bob과 Saladict에서 영감을 받았고 초기 버전은 Bob 기반이라고 설명합니다. 그 사실만으로 현재 제품 간 우열을 정할 수는 없습니다.

2-3. 학습용으로 흥미로운 이유: 제품의 성장 흔적

이 저장소에는 SwiftUI 화면 옆에 기존 AppKit·Objective-C 창이 남아 있고, 서비스 계층에는 콜백 API와 async/await가 함께 있습니다. @objc(EZQueryService)처럼 Swift 클래스에 기존 Objective-C 이름을 유지하는 코드도 보입니다. 전면 재작성 없이 제품을 발전시키는 현실적인 사례입니다.

Claude Code·Codex CLI·GitHub Copilot 연동은 번역 요청의 실행 경로를 넓힙니다. 다만 이 앱의 중심은 여전히 사전·번역 작업입니다. CLI를 연결했다는 이유로 범용 코딩 에이전트 플랫폼이라고 이해할 필요는 없습니다.

비유

오래 운영한 가게가 주방을 한 번에 철거하지 않고 계산대·배달 접수·메뉴판부터 바꾸는 모습과 닮았습니다. 오래된 코드가 있다는 사실보다 새 코드와 연결되는 경계가 어떻게 유지되는지를 보는 것이 좋은 공부가 됩니다.

3기술 스택 전체 지도

네이티브 화면 · 앱 내부 처리 · 선택적 HTTP 서버 · 빌드 도구

Easydict의 기본 실행 단위는 macOS 앱 프로세스입니다. 화면을 프론트엔드, 질의·저장·OCR을 백엔드 역할로 나누어 볼 수 있지만, 웹 서비스처럼 항상 별도 서버를 배포하는 구조는 아닙니다. 이 앱에서는 네이티브 UI와 처리 로직이 같은 프로세스에 있고, 외부 번역 API와 선택적 HTTP 서버가 그 경계를 넓힙니다.

3-1. 프론트엔드: SwiftUI와 AppKit을 함께 쓴다

기술확인한 값·버전실제 역할과 출처
Swift.swift-version: 5.9
SWIFT_VERSION: 5.0
버전 표기와 Xcode의 언어 모드 설정. 실행한 컴파일러 버전과는 구분
SwiftUI · Combine시스템 SDK 제공EasydictApp.swift의 메뉴 막대·설정 Scene, 생명주기 알림 구독
AppKit · Objective-C시스템 SDK 제공Easydict/objc/ViewController/의 질의 창·테이블·입력 뷰
SnapKit · Masonry5.6.0 · 1.1.0각각 Swift·Objective-C에서 제약 기반 레이아웃 구성
SettingsAccess · Defaults2.1.0 · 8.2.0설정 화면 열기, 타입이 있는 환경설정 키·값 관리
SelectedTextKit2.6.7다른 앱에서 선택된 텍스트를 읽는 기능의 기반
KeyHolder · Magnet4.2.0 · 3.4.0단축키 입력·등록 계층. Magnet은 잠금 파일에 포함

Swift Package Manager 버전은 Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved에서, Masonry 버전은 Podfile.lock에서 확인했습니다. 시스템 프레임워크에는 별도의 패키지 버전 번호를 붙이지 않고 macOS 가용 조건을 확인해야 합니다.

화면을 모두 웹으로 그리지도 않습니다. Easydict/Swift/Feature/Markdown/MarkdownRenderer.swift는 Markdown을 NSAttributedString으로 바꿉니다. 반면 사전 계층은 HTML 결과도 다룹니다. 결과의 종류에 맞추어 표시 경로가 달라지는 셈입니다.

3-2. 백엔드 역할: 질의·네트워크·인식·저장

기술잠긴 버전·설정소스에서 확인한 역할
Alamofire5.12.2번역 API와 모델 목록 등의 HTTP 요청
MacPaw/OpenAI0.5.1, 정확한 버전 지정ChatQuery·ChatStreamResult 모델. 스트리밍 전송은 별도 구현도 사용
AsyncAlgorithms1.0.3스트리밍 갱신 빈도 제어. 결과 처리의 기본 간격 0.3초
Vision · NaturalLanguagemacOS SDK이미지 글자·QR 인식, 문장의 언어 판단
TranslationmacOS 15+ 경로옵션이 켜져 있으면 Apple 오프라인 번역 사용
Vapor · SwiftNIO4.122.0 · 2.91.0앱 안에서 선택적으로 켜는 HTTP 서버와 네트워크 기반 계층
CryptoSwift1.8.3암호화 관련 유틸리티 의존성
로컬 저장Defaults · JSONL · 파일설정·질의 이력·선택적 단어장·사전·로그. 별도 DB 서버 필수 구성 없음

QueryRecordManager.swift는 이력과 즐겨찾기를 Defaults 키로 관리하고 이력 상한을 1000으로 둡니다. VocabularyNotebookService.swift는 활성화된 경우 words.jsonl에 한 줄씩 기록합니다. 잠금 파일의 leveldb 같은 전이 의존성이 있다고 앱의 질의 이력을 LevelDB에 저장한다고 해석하면 틀립니다.

OpenAIStreamTransport는 stream 메서드에서 URLSession으로 전송하며, makeRequest가 만드는 요청의 제한 시간은 AppConstants.llmRequestTimeoutInterval = 60초를 참조합니다. SDK를 가져왔어도 전송·응답 검증을 제품 요구에 맞게 직접 감쌀 수 있다는 사례입니다.

용어
의존성 선언과 잠금 파일
선언은 “이 범위의 버전을 허용한다”는 조건입니다. 잠금 파일은 “이번 의존성 해석에서 선택한 버전”입니다. 예를 들어 Sparkle의 선언 하한은 2.6.4지만 잠긴 값은 2.8.1입니다. 새로 해석하지 않은 상태의 재현 기준을 알려 주는 파일이 잠금 파일입니다.

3-3. 인프라·빌드: 두 패키지 관리 체계의 공존

파일·기술확인한 내용배울 점
Podfile.lockMasonry 1.1.0, ReactiveObjC 3.1.1, JLRoutes 2.1기존 Objective-C 의존성을 유지하는 CocoaPods 경계
GemfileCocoaPods 1.16.2 고정라이브러리뿐 아니라 설치 도구의 버전도 기록
BuildTools/Package.resolvedSwiftFormat 0.63.0포매터를 앱 런타임 의존성과 분리
SwiftLintPlugins0.65.1소스 품질 검사 의존성. 앱 실행 기능과는 구분
Sparkle2.8.1 · appcast.xml데스크톱 앱의 자동 업데이트 피드
관측 도구CocoaLumberjack 3.7.4, Sentry 8.44.0, Firebase SDK 11.13.0로그·충돌·분석 기능도 제품 운영의 일부

Easydict.xcodeproj/project.pbxproj가 앱 타깃·소스·프레임워크·빌드 설정을 연결합니다. CocoaPods까지 포함하는 진입점은 Easydict.xcworkspace입니다. 이 프로젝트에는 앱을 정의하는 최상위 Package.swift가 없으며, BuildTools/Package.swift는 개발 도구용입니다.

.github/workflows/codeql.yml은 Swift를 수동 빌드 모드로 분석합니다. 실행기 레이블은 xcode-27이고 Xcode·macOS 버전이 27.*인지 검사하며, 빌드에 ARCHS=arm64를 지정합니다. 이는 설정된 분석 환경이지 모든 플랫폼에서 테스트가 통과했다는 결과 보고는 아닙니다.

배포 절차는 .agents/skills/release-easydict/scripts/의 release-build.sh, release-package.sh, release-appcast.py 등에 나뉩니다. Archive·Developer ID 서명·공증·DMG·업데이트 피드가 각각 다루어집니다. 일반 앱 학습을 시작할 때 이 배포 절차부터 이해할 필요는 없습니다.

4아키텍처 심화 분석

입력에서 결과까지 따라가며 네 가지 설계를 읽기

앱 진입점은 Easydict/App/EasydictApp.swift의 EasydictCmpatibilityEntry.main()입니다. 경로 마이그레이션·인자 처리·충돌 기록·OCR 예열 등을 준비한 뒤 EasydictApp.main()으로 넘어갑니다. SwiftUI의 @NSApplicationDelegateAdaptor는 기존 AppDelegate와 연결하고, 메뉴 막대와 설정 화면은 SwiftUI Scene으로 구성합니다.

[키보드 입력] [다른 앱의 선택 영역] [화면 캡처] | | | | EventMonitor / | | SelectionWorkflow | | | AppleOCREngine | | QR + Vision OCR | | OCRTextProcessor +------------------+---------------------+ | EZBaseQueryViewController QueryModel + DetectManager | queryAllSerives: 로 서비스별 시작·이력 추가 | 기존 QueryService 인스턴스 | +----------------+----------------+ | | | 로컬 사전 원격 번역 API StreamService Apple / MDict Google / DeepL AI / Ollama / CLI | | | +----------------+----------------+ | QueryResult | AppKit 결과 셀 / 표시 계층 | 첫 서비스 결과로 이력 보완 · 선택적 JSONL 단어장 [선택적 Vapor HTTP 서버] --> 같은 서비스 계층 재사용 /translate /streamTranslate /ocr /detect /selectedText

일반 질의의 순서는 컨트롤러의 startQueryText:actionType: → queryCurrentModel → 필요하면 언어 감지 → queryAllSerives: → queryWithModel:service:입니다. queryAllSerives라는 철자는 소스 그대로입니다. 서비스마다 비동기 요청을 시작하므로, 한 서비스가 끝날 때까지 모두 기다린 뒤 다음 서비스를 시작하는 구조가 아닙니다.

4-1. 팩토리와 템플릿 메서드: 공급자를 바꿀 수 있는 이유

Easydict/Swift/Service/Model/QueryServiceFactory.swift는 서비스 식별자와 클래스의 등록부입니다. service(withTypeId:)는 식별자를 분해하고 알맞은 클래스를 찾아 init()한 뒤 인스턴스의 uuid를 지정합니다. # 뒤의 UUID를 분리하는 구조는 같은 종류의 사용자 정의 서비스를 여러 개 구성하는 데 쓰입니다. 일반 질의에서는 컨트롤러에 이미 구성된 서비스 목록을 순회합니다.

// QueryServiceFactory.swift: 등록 목록 중 일부
.init(.appleDictionary, AppleDictionary.self,
      "apple_dictionary", apiKeyRequirement: .none),
.init(.openAI, OpenAIService.self, "openai_translate"),
.init(.customOpenAI, CustomOpenAIService.self,
      "custom_openai", allowsMultipleInstances: true),

상위 클래스 QueryService의 startQuery(_:)는 모델 연결 → 입력·언어 추출 → 공통 전처리 순서로 진행하고, 전처리에서 완료되지 않으면 translate(_:from:to:)를 호출합니다. 하위 클래스는 마지막 번역 동작을 재정의합니다. 이처럼 공통 절차는 상위 클래스가 정하고 일부 단계는 하위 클래스에 맡기는 방식이 템플릿 메서드 패턴입니다.

이 프로젝트의 공급자 확장은 추상 프로토콜만으로 이루어지지 않습니다. 실제 중심은 NSObject를 상속하는 QueryService 클래스와 재정의 메서드입니다. 코드에 없는 프로토콜 기반 구조를 가정하기보다 required init, override, @objcMembers의 쓰임을 함께 보세요.

용어
팩토리: 생성 방법을 한곳에 모으는 창구
화면이 모든 서비스 클래스를 직접 알면 새 공급자가 생길 때 화면도 계속 바뀝니다. 팩토리는 “이 식별자의 서비스를 만들어 줘”라는 요청을 받습니다. 생성 규칙이 한곳에 모이지만, 새 서비스의 등록·설정·지원 언어까지 자동 완성되는 것은 아닙니다.

4-2. 어댑터: 서로 다른 응답을 같은 스트림으로

StreamService 계층은 조금씩 도착하는 AI 응답을 처리합니다. Easydict/Swift/Service/OpenAI/StreamService+AsyncStream.swift는 문자열 조각을 ChatStreamResult 또는 누적 QueryResult로 바꿉니다. StreamService+UpdateResult.swift의 throttleUpdateResultText는 기본 간격 0.3초로 갱신 빈도를 제어합니다.

서버의 바이트 조각 --> SSE 이벤트 경계 복원 --> ChatStreamResult 디코딩 --> content 문자열 조각 --> 누적 문장 --> QueryResult 갱신 빈도 제어 --> MainActor에서 UI 콜백

OpenAIStreamTransport.swift의 stream(query:url:apiKey:onResult:)는 HTTP 상태 코드를 먼저 검사하고, 정상 응답의 MIME 유형이 text/event-stream인지 확인합니다. completedEvent(from:)는 빈 줄 경계를 찾고, decodeEvent(dataLines:)는 data: 줄을 모아 JSON으로 해석합니다. [DONE]은 종료 신호입니다.

네트워크에서 한 번 받은 데이터가 JSON 하나와 정확히 일치한다고 가정하면 안 됩니다. 한 이벤트가 여러 조각으로 나뉘거나 여러 줄의 데이터로 표현될 수 있습니다. 이 전송 계층은 바이트 수신과 의미 있는 이벤트의 경계를 구분합니다. 실패 응답 본문을 읽는 상한도 64 * 1024바이트로 둡니다.

// AsyncThrowingStream 안에서 사용하는 정리 구조를 축약한 예시
AsyncThrowingStream { continuation in
    let task = Task {
        do {
            for try await content in contentStream {
                continuation.yield(content)
            }
            continuation.finish()
        } catch {
            continuation.finish(throwing: error)
        }
    }
    continuation.onTermination = { _ in task.cancel() }
}

yield는 “다음 조각이 왔다”, finish는 “더 이상 오지 않는다”라는 신호입니다. onTermination은 소비가 끝났을 때 내부 작업도 취소하도록 연결합니다. 스트림을 만드는 것과 내부 작업의 수명을 끝까지 관리하는 것은 함께 설계해야 합니다.

4-3. 세대 번호와 협력적 취소: 오래된 답이 새 화면을 덮지 않게

사용자가 A를 질의한 직후 B를 질의했는데 A의 응답이 늦게 도착할 수 있습니다. 단순히 마지막에 도착한 답을 표시하면 B의 창에 A의 번역이 나타납니다. 비동기 UI에서는 요청을 중단하는 것과, 이미 도착 중인 오래된 결과를 무시하는 것을 함께 처리해야 합니다.

QueryService.resetServiceResult()는 resultGeneration을 증가시킵니다. 스트림을 시작할 때 기억한 targetGeneration과 현재 값을 비교하고, 다르면 결과 갱신을 버립니다. NSRecursiveLock으로 세대 확인과 결과 변경을 함께 보호하며, UI 콜백 직전에도 세대를 다시 검사합니다.

// StreamService+UpdateResult.swift: 오래된 스트림 차단
if let targetGeneration, targetGeneration != resultGeneration {
    return
}

// QueryModel.swift: 등록을 제거한 다음, 잠금 밖에서 취소 호출
func stopServiceRequest(_ serviceType: String) {
    let stopBlock = stopBlockLock.withLock {
        stopBlockDictionary.removeValue(forKey: serviceType)
    }
    stopBlock?()
}

Easydict/Swift/Model/QueryModel.swift의 잠금은 취소 콜백 등록부를 보호합니다. 모델의 모든 필드가 스레드 안전하다는 뜻은 아닙니다. stopAllService()도 현재 콜백을 배열로 옮기고 등록부를 비운 뒤 잠금을 풀어 호출합니다. 콜백이 다시 새 콜백을 등록해도 그 등록을 지워 버리지 않도록 하는 설계입니다.

EasydictTests/Model/QueryModelCancellationTests.swift의 singleStopPreservesReentrantRegistration()과 concurrentStopsInvokeCallbackOnce()가 이 동작의 의도를 보여 줍니다. 전자는 취소 중 재등록을, 후자는 개별 취소와 일괄 취소가 겹치는 경우를 다룹니다. 테스트 소스를 읽으면 복잡해 보이는 순서가 왜 필요한지 알 수 있습니다.

비유

식당 주문표의 번호와 같습니다. 손님이 주문을 바꿨는데 예전 음식이 늦게 나오면, 직원은 현재 주문 번호와 비교하고 다른 테이블에 잘못 놓지 않습니다. 취소는 주방에 보내는 연락이고, 세대 번호는 서빙 직전 확인하는 주문표입니다.

4-4. 파이프라인과 호환 계층: OCR은 글자 인식 다음이 길다

Easydict/Swift/Service/Apple/AppleOCREngine/AppleOCREngine.swift의 recognizeText는 이미지 확인 → QR 해독 → Vision OCR → 언어 판단 → 텍스트 병합으로 이어집니다. OCRTextProcessor.setupOCRResult는 관측 결과를 받아 가로 영역과 구역을 분석하고, 줄·문단을 합쳐 최종 문자열을 만듭니다.

단계함수·클래스설계상 의미
QR 인식detectQRCodePayloads(on:)정규화한 내용으로 중복 판단, 원래 내용은 보존
OS별 OCR 선택performVisionOCR(on:language:)macOS 26+는 현대 API, 그 이전은 기존 API로 분기
결과 통일EZRecognizedTextObservationVision API가 달라도 후처리는 공통 형식으로 진행
공간·문단 복원OCRTextProcessor·OCRSectionMerger·OCRBandMerger글자 목록을 읽을 순서와 문장으로 바꾸는 별도 책임
조건부 재인식selectBestOCRResult·performConcurrentOCR정확도 옵션과 낮은 확신 조건이 겹칠 때 후보 언어 비교

짧은 문장은 언어를 확신하기 어렵습니다. 코드의 hasEnoughLength는 mergedText.count > 50이고, requiresAccurateRecognition 기본값은 false입니다. 여러 언어로 다시 읽는 경로가 있어도 기본 호출마다 반드시 수행되지는 않습니다. AppleService.ocr는 이 옵션을 별도로 켜지 않고 호출합니다.

또한 현대 Vision OCR 메서드는 @available(macOS 15.0, *)로 선언되어 있지만, 실제 선택 조건은 #available(macOS 26.0, *)입니다. 코드 주석은 호환 문제에 따른 보수적 선택이라고 설명합니다. API가 제공되는 OS 버전과 앱이 실제 사용하기 시작하는 버전은 다를 수 있습니다.

커밋 69600ed의 QR 경로는 동기 Vision 작업을 DispatchQueue.global(qos: .utility)에 보내고 명시적 QoS로 실행합니다. 호출자는 continuation에서 기다리며, 작업 전후의 Task.checkCancellation()로 취소를 전달합니다. 동기 Vision 호출이 즉시 중단된다는 뜻은 아니며, 완료되어도 취소된 결과를 버리는 방식입니다.

기존 OCR API를 비동기 코드로 연결하는 ContinuationGate는 잠금으로 continuation을 한 번만 꺼냅니다. 완료 콜백과 perform()의 오류가 모두 발생해도 두 번 resume하지 않게 합니다. 실패 경로까지 포함한 “정확히 한 번 완료”는 콜백 변환에서 중요한 계약입니다.

문서와 호출 경로의 차이
Youdao OCR 자동 대체를 모든 입력 경로에 있다고 가정하지 않기

사용 가이드는 Apple OCR 실패 시 선택적으로 Youdao를 시도한다고 설명합니다. 하지만 이 커밋의 DetectManager.ocr와 ocrAndDetectText는 기본 OCR 서비스를 직접 호출합니다. Youdao 대체 로직은 비공개 deepOCR가 사용하는 handleOCRResult 안에 있고, deepOCR의 호출부는 이 커밋의 앱·테스트 코드에 없습니다.

일반 OCR의 공개 진입점은 이 대체 로직을 거치지 않습니다. 이는 커밋 69600ed의 정적 호출 경로를 기준으로 한 설명입니다. 기능 설명과 실제 진입점을 함께 읽어야 하는 이유를 보여 줍니다.

5디렉토리 구조 해부

폴더 이름을 외우기보다 데이터의 이동에 맞추어 읽기
Easydict/ +-- App/ 진입점·AppDelegate·리소스·권한 설정 +-- Swift/ | +-- Feature/ 사용자 작업과 제품 기능 | | +-- Shortcut/ 단축키 설정·실행 | | +-- Screenshot/ 캡처·선택 영역 UI | | +-- HTTPServer/ Vapor API와 서비스 연결 | | +-- Configuration/ 설정 키·공통 설정 | | +-- Markdown/ AI 결과의 서식 변환 | | +-- VocabularyNotebook/ JSONL 단어장 기록 | +-- Model/ QueryModel·감지·오류·기록 모델 | +-- Service/ | | +-- Model/ QueryService·팩토리·결과·저장 | | +-- Apple/ 시스템 번역·언어 감지·OCR | | +-- Dictionary/ Apple Dictionary·MDict | | +-- OpenAI/ 공통 스트리밍 처리·전송 | | +-- CodexCLI/ CLI·관리형 런타임 연결 | | +-- ... 공급자별 요청·응답 처리 | +-- Utility/ 이벤트·경로·로그 등 공통 도구 | +-- View/ 설정 화면·공유 뷰 +-- objc/ 기존 창·뷰·언어·오디오·라이브러리 EasydictTests/ 기능·서비스·모델별 테스트 Easydict.xcodeproj/ 타깃·빌드 단계·소스 참조 Easydict.xcworkspace/ CocoaPods·SwiftPM 통합 진입점 Pods/ 포함된 CocoaPods 의존성 BuildTools/ 개발 도구용 Swift 패키지 .github/workflows/ CodeQL·문서 및 운영 자동화 .agents/skills/release-easydict/ 배포 절차와 관련 도구 docs/ 사용법·설계·개발 규칙·기록

위 트리의 첫 번째 Easydict/는 앱 소스 디렉토리이며, 그 아래의 테스트·프로젝트·문서 디렉토리는 저장소 루트에 있습니다. Service는 특정 엔진이 요청을 처리하는 곳, Feature는 사용자가 어떤 작업을 하는지 표현하는 곳, Utility는 여러 기능이 공유하는 도구로 읽으면 됩니다.

모든 파일이 이 분류에 완벽하게 들어맞지는 않습니다. 예를 들어 이력을 관리하는 QueryRecordManager.swift는 Easydict/Swift/View/SettingView/Tabs/Model/에 있습니다. 디렉토리 이름만 보고 저장 로직이 없다고 결론 내리기보다, 타입과 호출자를 함께 검색하세요.

5-1. 직접 센 코드 규모

범위·언어파일 수물리적 줄 수
앱 Swift37164,622줄
앱 Objective-C 구현 .m10019,416줄
앱 헤더 .h1063,679줄
앱 JavaScript4243줄
앱 HTML1538줄
테스트 Swift5712,301줄

집계 기준: 앱은 Easydict/, 테스트는 EasydictTests/ 아래의 해당 확장자가 대상입니다. 줄 수는 개행 문자 기준이며 주석·빈 줄을 포함합니다. 앱 내부 Easydict/objc/Libraries/의 복사된 라이브러리도 포함하고, Pods/·문서·이미지 등 다른 확장자의 리소스·외부 SwiftPM 소스는 제외합니다. 헤더에는 C 스타일 선언도 섞일 수 있습니다.

# 저장소 루트에서 규모를 확인하는 읽기 전용 명령 예시
find Easydict -type f -name '*.swift' -exec wc -l {} +
find Easydict -type f -name '*.m' -exec wc -l {} +
find EasydictTests -type f -name '*.swift' -exec wc -l {} +

앱의 위 다섯 확장자는 합계 88,498줄입니다. 테스트 줄 수는 학습할 사례의 양을 보여 줄 뿐, 코드 커버리지나 신뢰성을 직접 나타내는 지표는 아닙니다. 이 표는 커밋 69600ed의 코드 규모를 나타내며, 개발 속도나 주간 증가량을 뜻하지 않습니다.

5-2. 처음 읽을 때 권하는 순서

  1. EasydictApp.swift에서 앱 시작과 메뉴·설정 화면을 찾습니다.
  2. QueryModel.swift와 QueryResult.swift에서 입력·출력을 구분합니다.
  3. QueryServiceFactory.swift에서 관심 있는 서비스 하나를 고릅니다.
  4. AppleService.swift 또는 OllamaService.swift에서 구체적인 실행을 읽습니다.
  5. 마지막으로 EZBaseQueryViewController.m에서 화면과 이 객체들이 연결되는 곳을 확인합니다.

6학습 포인트 (기술별)

한 기능을 읽고, 더 작은 독립 예제로 다시 표현하기

처음부터 모든 공급자의 API 요청 코드를 읽으면 차이만 늘어나고 공통 구조는 잘 보이지 않습니다. 같은 입력이 어느 경계에서 어떤 형태로 바뀌는지 추적하면, 배운 설계를 다른 앱에도 옮길 수 있습니다.

6-1. Swift와 Objective-C 상호운용

QueryService.swift의 @objc(EZQueryService)와 QueryModel.swift의 Objective-C 노출을 살펴보세요. 기존 컨트롤러는 EZQueryService·EZQueryModel이라는 이름으로 Swift 객체를 사용합니다. Easydict/App/Easydict-Bridging-Header.h는 반대 방향으로 Objective-C 선언을 Swift에 연결합니다.

작은 실습: Swift 클래스 하나의 Swift 이름·Objective-C 이름·노출 메서드를 표로 적어 보세요. 왜 스트림 같은 Swift 전용 API에 @nonobjc가 붙고, 화면에는 콜백 기반 메서드를 따로 제공하는지도 설명해 봅니다.

용어
브리지: 언어가 다른 두 코드 사이의 연결면
연결면에서는 이름뿐 아니라 표현 가능한 타입과 호출 방식도 맞아야 합니다. Swift의 모든 문법을 Objective-C가 이해하는 것은 아닙니다. 그래서 내부에서는 비동기 스트림을 사용하고 외부 연결면에는 콜백을 제공할 수 있습니다.

6-2. async/await와 UI 격리

QueryService.startQueryStream(_:completionHandler:)는 스트림을 순회하고 MainActor.run 안에서 화면용 콜백을 호출합니다. SelectionWorkflow.getSelectedTextSnapshot의 완료 콜백에는 @MainActor가 선언되어 있지 않습니다. 같은 “콜백”이라도 실행 문맥의 계약이 다릅니다.

작은 실습: 네트워크 대기, 문자열 누적, 결과 상태 변경, UI 표시 네 단계를 다른 색으로 표시해 보세요. async라는 키워드만 보고 무거운 계산이 자동으로 백그라운드에서 처리된다고 생각하지 않는 것이 출발점입니다. 비동기 실행과 스레드의 관계는 Swift 공식 동시성 문서에서 이어서 읽을 수 있습니다.

6-3. OCR 후처리와 휴리스틱

OCRConstants.swift에는 들여쓰기 판단용 1.8글자, 일반 줄 간격 상한 비율 1.2, 중국어 시의 한 줄 최대 글자 수 12.0, 영어 시의 한 줄 최대 단어 수 7.0 등이 있습니다. 이는 하드웨어 요구량이 아니라 레이아웃 판단을 위한 기준값입니다.

작은 실습: 문단·시·두 단으로 된 문서를 각각 골라 줄바꿈을 유지해야 하는 이유를 적어 보세요. OCRTextProcessingTests.swift, OCRTextProcessingChineseTests.swift, OCRPunctuationTests.swift의 입력·기대값을 비교하면 규칙이 어떤 문장을 보호하려는지 보입니다.

비유

OCR 엔진이 낱말이 적힌 종잇조각을 건네면 후처리는 원래 페이지를 다시 조립하는 작업입니다. 같은 줄바꿈도 문단에서는 지워야 하고 시에서는 남겨야 합니다. 읽힌 글자 수만으로 품질을 평가하기 어려운 이유입니다.

6-4. 바이너리 파일과 제한된 메모리

Easydict/Swift/Service/Dictionary/MDict/MDictReader/MDictReader.swift는 MDX·MDD 사전 형식을 다룹니다. RecordBlockRange는 압축된 데이터와 압축 해제된 데이터의 위치를 저장하고, 필요한 레코드 블록을 찾는 데 사용됩니다. 상수에는 단일 압축 해제 블록 크기 상한 256 * 1024 * 1024바이트, 키 블록 캐시 16개, 레코드 블록 캐시 8개·50MiB가 보입니다.

작은 실습: 사전을 통째로 메모리에 올리는 방식과 필요한 블록만 읽는 방식의 비용을 비교하세요. MDictSearchIndex.candidates(for:limit:)는 기본 후보 8개를 접두어 → 부분 문자열 → 편집 거리 순서로 찾습니다. 편집 거리 상한은 2입니다. 정확한 검색과 비슷한 단어 제안을 서로 다른 경로로 나누는 이유를 배울 수 있습니다.

6-5. 저장과 종료 시점의 신뢰성

VocabularyNotebookService.append는 현재 화면에서 계속 바뀌는 입력보다 결과에 붙은 result.queryText를 기록에 사용합니다. 기록 작업은 전용 직렬 큐에 넣고, flush()는 앞서 들어온 쓰기가 끝나기를 기다립니다. 앱 종료 알림에서 동기적으로 flush()하는 코드가 EasydictApp.swift에 있습니다.

작은 실습: “번역이 끝남 → 파일 기록 예약 → 앱 종료” 순서를 그려 보세요. 예약만 하고 종료하면 왜 마지막 단어를 잃을 수 있는지, 마지막 줄에 개행이 없는 JSONL 파일에 바로 덧붙이면 왜 두 기록이 붙어 버리는지 설명해 봅니다.

6-6. 테스트에서 요구사항을 역으로 읽기

읽을 테스트확인할 요구사항응용할 수 있는 곳
QueryModelCancellationTests.swift중복 취소·재등록·동시 접근검색창·파일 업로드·다운로드 취소
OCRQRCodeTests.swiftQR만 있는 이미지·혼합 이미지·중복 QR스캔 앱의 복합 입력 처리
MarkdownRendererTests.swift스트리밍 도중의 불완전한 서식 입력채팅·실시간 문서 미리보기
MDictReaderTests.swift사전 파일 읽기와 조회바이너리 파서·대형 파일 탐색

테스트의 입력·기대값은 요구사항을 이해하는 단서입니다. 테스트가 존재한다는 사실만으로 실행 성공률이나 코드 커버리지를 판단할 수는 없습니다.

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

앱 실행 조건과 선택한 엔진의 조건을 분리해서 보기
사용 범위요구사항·조건확인 근거
현재 앱macOS 13.0 이상README·Podfile·앱 타깃의 MACOSX_DEPLOYMENT_TARGET
Apple 오프라인 번역macOS 15+ · 기능 활성화 · 필요한 언어팩AppleService.translateAsync와 공식 사용 가이드
현대 Vision OCR 경로이 커밋에서는 macOS 26+에서 선택performVisionOCR의 실제 분기
선택 영역 읽기접근성 권한, 대상 앱의 텍스트 접근 가능 여부SelectionWorkflow·사용 가이드
화면 캡처화면 기록 권한사용 가이드의 권한 안내
브라우저·단축어 제어해당 기능 사용 시 자동화 권한AppleScript·Shortcuts 경로
클라우드 번역·AI네트워크·공급자별 계정이나 키서비스의 apiKeyRequirement()
Ollama 연동별도 모델 서버와 모델에 맞는 메모리·디스크OllamaService의 기본 로컬 endpoint
소스 빌드가이드 기준 macOS 13+ · Xcode 16+ · CocoaPodsdocs/user-docs/en/GUIDE.md

앱의 공식 최소 RAM·디스크·GPU 수치는 프로젝트 문서에 명시되어 있지 않습니다. 따라서 8GB나 16GB를 공식 최소 사양으로 적을 근거는 없습니다. CodeQL 설정의 arm64도 분석 대상 아키텍처이며, 배포 파일의 Intel 호환성을 보장하는 값은 아닙니다.

로컬 OCR을 쓰는 데 필요한 자원과, 로컬 LLM을 실행하는 데 필요한 자원은 별개입니다. 기본 OCR은 Vision을 이용합니다. 반면 Ollama의 모델 크기·정밀도·문맥 길이에 따른 메모리 요구량은 별도로 확인해야 합니다. OllamaService의 기본 주소는 http://localhost:11434/v1/chat/completions이며, 앱이 자체 추론 엔진을 내장했다는 뜻은 아닙니다.

7-1. 오프라인 기능을 읽을 때 확인할 세 가지

  1. 인식: 이미지의 글자는 기본적으로 시스템 Vision이 읽습니다.
  2. 번역: Apple 오프라인 번역은 기본값이 꺼져 있습니다. macOS 15+와 언어팩·옵션 조건을 확인해야 합니다.
  3. 결과의 이동: 뒤이어 원격 서비스를 실행하면 인식된 텍스트가 해당 서비스로 전달될 수 있습니다.

Defaults.Keys+Extension.swift의 enableAppleOfflineTranslation은 false입니다. AppleService.translateAsync는 조건을 만족할 때 Translation 경로를 사용하고, 그렇지 않으면 AppleScript로 단축어를 호출합니다. 두 경로의 준비 조건을 하나로 뭉뚱그리지 마세요.

7-2. 개발 도구 버전과 실행 OS 버전은 다른 축

사용 가이드는 Xcode 16+를 안내하지만 CodeQL 워크플로는 Xcode 27·macOS 27을 검사합니다. 앱은 macOS 13을 배포 대상으로 설정합니다. 실행 가능한 OS, 빌드에 쓰는 SDK·컴파일러, CI가 선택한 환경은 서로 다른 값입니다. 이 설정만으로 Xcode 16에서 현재 의존성까지 모두 빌드된다고 보장할 수는 없습니다.

또한 .swift-version의 5.9와 SWIFT_VERSION = 5.0은 실제 swiftc --version 측정값이 아닙니다. 저장소에 Swift 6의 엄격한 동시성 모드가 전면 적용되었다고 해석해서도 안 됩니다.

준비 순서
코드 읽기는 Mac 없이도 시작할 수 있다

모델·팩토리·취소·SSE·파일 형식은 텍스트 편집기만으로 분석할 수 있습니다. 실제 AppKit 창·Vision OCR·접근성 권한 동작을 확인하는 단계에서는 해당 기능을 지원하는 Mac이 필요합니다.

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

다섯 과제 · 코드 읽기에서 독립 구현까지

실습은 원본 저장소와 분리한 학습용 예제에서 진행하는 구성을 권합니다. 앞의 두 과제는 소스 읽기만으로 완성할 수 있고, 뒤의 과제는 가짜 응답을 사용해 API 키와 유료 호출 없이 구현할 수 있습니다. 완료 기준을 눈에 보이는 산출물로 정하면, 코드를 읽었다는 느낌을 넘어 실제로 이해했는지 확인할 수 있습니다.

실습 1 · 입문 · 예상 30~45분

서비스 세 개의 계약을 한 장에 정리하기

읽을 곳: QueryServiceFactory.swift, QueryService.swift, AppleService.swift, OllamaService.swift.

할 일: Apple Dictionary·Apple 번역·Ollama의 식별자, 키 요구 여부, 데이터 처리 위치, 필요한 외부 준비를 표로 적습니다. apiKeyRequirement: .none이 왜 오프라인 보장을 뜻하지 않는지도 설명합니다.

완료 기준: 서비스 세 행의 비교표와 “새 공급자를 등록하려면 생성·실행·설정 중 어디를 바꾸는가”에 대한 5문장 설명. 앱을 실행하지 않아도 코드로 근거를 제시할 수 있어야 합니다.

실습 2 · 입문 · 예상 45~60분

완성되기 전의 Markdown을 손으로 추적하기

읽을 곳: Easydict/Swift/Feature/Markdown/MarkdownRenderer.swift의 render(_:)와 EasydictTests/Feature/Markdown/MarkdownRendererTests.swift.

할 일: 아래 입력을 한 줄씩 읽으며 inFence·codeBuffer가 어떻게 바뀌는지 적습니다. 마지막 코드 울타리가 아직 닫히지 않았을 때 실행되는 분기를 찾으세요.

## 오늘 배운 것
**아직 도착 중인 답변**
```swift
let count = 3

완료 기준: 줄마다 상태를 적은 표와, 닫는 울타리가 나중에 도착해도 앱이 중간 내용을 표시해야 하는 이유. 모든 Markdown 기능을 지원한다고 확대 해석하지 않고, 이 렌더러가 구현한 문법 범위도 목록으로 적습니다.

실습 3 · 중급 · 예상 2~3시간

오래된 검색 결과를 버리는 작은 스트림 만들기

읽을 곳: QueryService.resetServiceResult(), StreamService+UpdateResult.swift, QueryModelCancellationTests.swift.

할 일: 네트워크 대신 AsyncThrowingStream<String, Error>에서 짧은 문자열을 차례로 내보냅니다. 입력이 바뀔 때 작업을 취소하고 세대 번호를 올리세요. 화면 갱신 직전에는 시작할 때의 세대와 현재 세대를 비교합니다.

시간 0: A 시작, 세대 1 시간 1: B 시작, 세대 2 / A 취소 요청 시간 2: B의 첫 조각 도착 --> 표시 시간 3: A의 늦은 조각 도착 --> 무시 시간 4: B 완료 --> 로딩 종료

완료 기준: A의 조각이 B의 결과에 섞이지 않는 시연, 취소 후 로딩 상태가 끝나는 화면, 스트림 종료 시 내부 작업을 정리하는 코드. 지연 시간의 우연한 순서에만 의존하지 말고, 조각을 원하는 순서로 주입할 수 있게 설계합니다.

더 생각할 점: 취소 요청을 보낸 직후에도 A의 조각이 올 수 있다면 어떤 검사가 마지막 방어선인지 설명해 보세요.

실습 4 · 중급 · 예상 2~4시간

OCR 없는 문단 복원기 만들기

읽을 곳: OCRTextProcessor.swift, OCRSectionMerger.swift, OCRConstants.swift.

할 일: Vision 대신 “문자열 + 사각형 좌표” 배열을 직접 준비합니다. 위에서 아래로 읽는 순서와 같은 문단의 줄을 합치는 규칙을 만듭니다. 문단·시·두 단 배치 세 입력을 준비하고, 규칙을 적용하기 전후를 비교하세요.

// 학습용 데이터 형태의 예시. 원본 타입의 복사본은 아닙니다.
struct TextBox {
    let text: String
    let x: Double
    let y: Double
    let width: Double
    let height: Double
}

완료 기준: 세 입력의 기대 결과와 실제 결과를 나란히 제시합니다. 두 단을 단순히 y좌표만으로 정렬했을 때 생기는 오류를 한 개 이상 보여 주세요. 좌표 원점·읽기 방향을 먼저 정하고 규칙을 설명할 수 있어야 합니다.

실습 5 · 고급 · 예상 1~2일

더미 엔진 두 개를 연결한 번역 게이트웨이 설계하기

읽을 곳: Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift, QueryServiceFactory.swift, OpenAIStreamTransport.swift, VocabularyNotebookService.swift.

할 일: 입력을 그대로 돌려주는 엔진과 한 글자씩 응답하는 엔진을 별도 예제에 만듭니다. 일반 응답과 스트림 응답의 경로를 나누고, 알 수 없는 서비스·빈 입력·중도 취소·오류·정상 종료를 각각 설계합니다. 결과를 기록한다면 정상 완료만 저장하도록 합니다.

완료 기준: 요청·응답 형식, 상태 전이 그림, 다섯 상황의 기대 동작표, 중도 실패가 성공 기록으로 남지 않는 시연. 네트워크 서버를 구현하는 경우 루프백 바인딩과 인증 정책도 설계에 명시하세요.

더 생각할 점: 스트리밍 가능한 클래스라도 특정 요청은 비스트리밍 전송을 쓸 수 있습니다. 원본 /translate가 클래스 종류만 보지 않고 usesStreamingTransport를 확인하는 이유를 설명합니다.

실습 결과 정리
기능 시연에 실패 경로도 하나 붙이기

정상 응답만 보이는 예제보다 “취소했을 때”, “빈 결과일 때”, “예전 결과가 늦게 왔을 때”를 함께 보여 주는 예제가 더 많은 설계 판단을 드러냅니다. 원본에서 읽은 해결책과 자신이 단순화한 부분을 구분해서 기록하세요.

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

6주 동안 하나의 데이터 흐름을 점점 넓혀 보기

하루에 새 라이브러리를 하나씩 추가하는 방식보다, 입력과 결과 사이의 계약을 점차 넓히는 순서가 초보자에게 적합합니다. 작은 동기 함수에서 시작해 비동기 결과, 취소, 파일 기록, 시스템 통합 순으로 확장해 보세요.

주차학습 주제Easydict 읽기 지점남길 산출물
1주Swift의 struct·class·enum·Optional·오류 처리QueryModel·QueryResult·QueryError입력·출력·오류 모델과 데이터 이동 그림
2주상속·팩토리·공통 절차와 재정의QueryServiceFactory·AppleService·OllamaService더미 엔진 두 개와 같은 호출 인터페이스
3주async/await·AsyncSequence·취소·MainActorStreamService+AsyncStream·QueryModel세대 번호가 있는 스트리밍 검색 예제
4주HTTP·SSE·응답 검증·오류 분류OpenAIStreamTransport·HTTPServer/Vapor분할된 이벤트를 복원하는 파서와 응답 표
5주OCR 관측값·좌표·언어·문단 복원AppleOCREngine·OCRTextProcessor세 종류 레이아웃의 병합 결과 비교
6주저장·종료 처리·네이티브 앱 통합·배포 개념VocabularyNotebookService·EasydictApp·appcast.xml정상·오류·취소·종료까지 설명 가능한 작은 앱

매주 “함수가 무엇을 받는가, 무엇을 돌려주는가, 실패하면 누가 처리하는가”를 세 줄로 적으세요. 셋 중 하나라도 불분명하면 전체 파일을 더 읽기 전에 호출자와 결과 사용 지점을 찾아보는 편이 좋습니다.

9-1. 관심사에 따른 다음 갈림길

CLI 쪽을 더 읽는다면 CodexCLIService.contentStreamTranslate가 좋은 입구입니다. 이 커밋에는 로컬 codex exec --json 경로와 관리형 App Server 경로가 함께 있습니다. 주석상 관리형 경로는 프로세스를 재사용하면서 번역마다 독립적인 임시 대화를 만들도록 설계되었습니다. 로컬 프로세스를 호출하는 것과 모델 추론이 로컬에서 일어나는 것은 구분해야 합니다.

학습 습관
변경이 퍼지는 범위를 예측하기
새 번역 엔진, 새 입력 방식, 새 저장 형식을 각각 추가한다고 상상해 보세요. 셋이 같은 파일들을 바꾸게 된다면 책임이 엉켜 있을 수 있습니다. 서로 다른 경계를 거친다는 것을 코드로 설명할 수 있으면 아키텍처를 이해한 것입니다.

10핵심 키워드 사전

코드 옆에 두고 읽을 쉬운 설명
용어쉬운 설명
SwiftUI현재 상태에 맞는 화면을 선언하는 Apple UI 프레임워크. 이 앱의 메뉴 막대·설정 화면에서 볼 수 있습니다.
AppKitmacOS의 창·뷰·입력 이벤트를 다루는 프레임워크. 기존 질의 창의 기반입니다.
Objective-C 브리지Swift와 Objective-C가 이름·타입·메서드를 맞추어 서로 호출하는 연결면입니다.
팩토리식별자를 받아 적절한 객체를 생성하는 창구. 서비스 생성 규칙을 한곳에 모읍니다.
템플릿 메서드공통 처리 순서를 정하고 일부 단계를 하위 클래스가 바꾸게 하는 패턴입니다.
어댑터서로 다른 입력·응답 형태를 사용자가 기대하는 공통 형태로 바꾸는 연결 코드입니다.
async / await기다리는 동안 실행을 잠시 양보할 수 있는 함수와 그 대기 지점을 표시합니다.
MainActorUI처럼 함께 다뤄야 할 상태의 접근을 메인 실행 문맥에 모으는 격리 경계입니다.
AsyncThrowingStream값이 차례로 도착하고, 도중에 오류로 끝날 수도 있는 비동기 통로입니다.
협력적 취소취소 요청을 받은 작업이 적절한 지점에서 이를 확인하고 종료하는 방식입니다.
세대 번호이번 요청과 이전 요청을 구별하는 번호. 늦게 온 옛 결과를 버리는 데 사용합니다.
재진입콜백 실행 도중 같은 객체의 메서드가 다시 호출되는 상황입니다.
잠금공유 상태의 특정 읽기·변경 구간에 여러 실행 흐름이 동시에 들어오지 못하게 합니다.
continuation콜백이 끝났을 때 중단된 비동기 함수를 이어 주는 손잡이. 완료 처리는 한 번만 해야 합니다.
SSEHTTP 연결에서 서버가 이벤트를 연속으로 보내는 형식. 이 앱은 AI 응답 조각을 읽는 데 사용합니다.
throttle잦은 이벤트의 전달 빈도를 제한하는 처리. 결과 화면을 너무 자주 갱신하지 않도록 돕습니다.
Vision 관측 결과이미지에서 읽은 글자와 위치·신뢰도 등의 묶음. 최종 문단은 별도 후처리로 만듭니다.
휴리스틱길이·간격 같은 경험적 규칙으로 판단하는 방법. 모든 입력에 정답을 보장하지는 않습니다.
JSONL각 줄이 독립된 JSON인 텍스트 파일 형식. 단어장을 한 건씩 덧붙이기 편합니다.
MDX / MDDMDict 사전의 항목과 리소스를 저장하는 파일 형식입니다.
전이 의존성직접 선택한 라이브러리가 다시 필요로 해서 함께 들어오는 라이브러리입니다.
deployment target앱이 지원 대상으로 삼은 최소 OS 버전. 빌드에 사용하는 최신 SDK 버전과는 다릅니다.
공증 · appcast공증은 macOS 배포 검증 절차의 하나이며, appcast는 앱에 새 버전 정보를 알리는 업데이트 피드입니다.

11참고 링크

제품 문서와 코드 근거를 나누어 다시 찾아보기

11-1. 공식 저장소와 사용 문서

11-2. 해설의 중심이 된 소스

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

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

항목상태
선정GitHub Trending Swift Weekly #10 · 2026-10-23 분석
스타 / 포크15,047 / 794 · 2026-10-23 GitHub 기준
생성일2022-11-15
주 언어Swift
최신 안정 릴리스2.24.0 · 2026-09-27
베타 업데이트2.25.0 · 2026-10-19 · 업데이트 피드(appcast.xml)의 베타 채널, 빌드 번호 68
소스의 앱 버전MARKETING_VERSION = 2.25.0 · 빌드 번호 68. 위 베타와 같은 버전이고, 안정 릴리스(2.24.0)와는 구분
최근 push2026-10-23 · GitHub 기준
분석한 최근 커밋69600ed · 2026-10-23 · OCR의 QR 작업 스케줄링 정렬과 취소 전달 수정
앱 코드 규모Swift 64,622줄 · Objective-C 구현 19,416줄 · 헤더 3,679줄 · JS 243줄 · HTML 538줄
테스트 규모Swift 57개 파일 · 12,301줄. 물리적 줄 수이며 커버리지와 구분
집계 범위앱 내부 라이브러리 포함 · Pods와 외부 SwiftPM 소스 제외 · 주석·빈 줄 포함
최소 실행 OSmacOS 13.0 · 일부 기능은 더 높은 버전 필요
라이선스GPL-3.0
쓰기 전에 알아둘 한계
기능·데이터 경로·검증 범위를 함께 보기

서비스 조건: 키가 필요 없다는 표시는 오프라인이나 무제한 이용을 뜻하지 않습니다. 원격 번역과 AI·CLI의 계정·요금·접근 조건은 공급자에 따라 달라집니다. 설치된 사전·언어팩과 OS 버전도 결과에 영향을 줍니다.

데이터 경로: 로컬 OCR 뒤에 원격 번역이 이어질 수 있습니다. OCR 엔진은 진단용 원본 이미지를 OCRConstants.snipImageFileURL에 기록하고, AppPathManager는 앱별 저장·로그 경로를 관리합니다. “로컬 기능이 있다”는 말로 저장과 전송 전체를 설명할 수는 없습니다.

분석 기능: AnalyticsService.swift에는 Release에서 Firebase와 Sentry를 초기화하는 코드가 있고, 이벤트 기록에는 allowAnalytics 조건이 있습니다. 이 구조가 있으므로 앱 전체가 아무 네트워크 통신도 하지 않는다고 단정할 수 없습니다.

HTTP 기능: 서버는 기본값이 꺼져 있고 기본 포트는 8080입니다. 확인한 configure.swift와 routes.swift에는 인증 미들웨어가 없으며, VaporServer.start()는 포트를 지정하지만 hostname은 지정하지 않습니다. 외부 공개용 인증·노출 정책이 갖추어져 있다고 가정하지 마세요.

버전 기준: 코드 설명은 커밋 69600ed 기준이며, GitHub 수치는 2026-10-23 기준입니다. 공개 릴리스와 이후 개발 소스의 동작은 다를 수 있습니다.