데스크탑 앱
69600ed 기준입니다.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에 있습니다.
처음에는 모든 Swift 문법을 알 필요가 없습니다. 입력을 담는 QueryModel, 실행을 담당하는 QueryService, 표시할 내용을 담는 QueryResult의 역할부터 구분하면 이후 코드가 훨씬 잘 읽힙니다.
2026-10-23 분석에서 GitHub Trending Swift Weekly #10으로 선정된 프로젝트입니다. 스타 15,047개와 포크 794개는 관심의 규모를 보여 주지만, 어떤 기능 때문에 순위가 올랐는지까지 증명하지는 않습니다. 아래 이유는 실제 기능과 구현을 바탕으로 한 해석입니다.
번역은 문장을 복사하고, 창을 전환하고, 붙여 넣는 짧은 동작이 반복되는 작업입니다. Easydict는 선택 영역과 화면 캡처를 입력으로 받아 이 반복을 줄입니다. 자주 쓰는 도구의 가치는 한 번의 놀라운 결과뿐 아니라, 매번 반복하던 작은 동작을 얼마나 줄여 주는가에서도 나옵니다.
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 기반이라고 설명합니다. 그 사실만으로 현재 제품 간 우열을 정할 수는 없습니다.
이 저장소에는 SwiftUI 화면 옆에 기존 AppKit·Objective-C 창이 남아 있고, 서비스 계층에는 콜백 API와 async/await가 함께 있습니다. @objc(EZQueryService)처럼 Swift 클래스에 기존 Objective-C 이름을 유지하는 코드도 보입니다. 전면 재작성 없이 제품을 발전시키는 현실적인 사례입니다.
Claude Code·Codex CLI·GitHub Copilot 연동은 번역 요청의 실행 경로를 넓힙니다. 다만 이 앱의 중심은 여전히 사전·번역 작업입니다. CLI를 연결했다는 이유로 범용 코딩 에이전트 플랫폼이라고 이해할 필요는 없습니다.
오래 운영한 가게가 주방을 한 번에 철거하지 않고 계산대·배달 접수·메뉴판부터 바꾸는 모습과 닮았습니다. 오래된 코드가 있다는 사실보다 새 코드와 연결되는 경계가 어떻게 유지되는지를 보는 것이 좋은 공부가 됩니다.
Easydict의 기본 실행 단위는 macOS 앱 프로세스입니다. 화면을 프론트엔드, 질의·저장·OCR을 백엔드 역할로 나누어 볼 수 있지만, 웹 서비스처럼 항상 별도 서버를 배포하는 구조는 아닙니다. 이 앱에서는 네이티브 UI와 처리 로직이 같은 프로세스에 있고, 외부 번역 API와 선택적 HTTP 서버가 그 경계를 넓힙니다.
| 기술 | 확인한 값·버전 | 실제 역할과 출처 |
|---|---|---|
| Swift | .swift-version: 5.9SWIFT_VERSION: 5.0 | 버전 표기와 Xcode의 언어 모드 설정. 실행한 컴파일러 버전과는 구분 |
| SwiftUI · Combine | 시스템 SDK 제공 | EasydictApp.swift의 메뉴 막대·설정 Scene, 생명주기 알림 구독 |
| AppKit · Objective-C | 시스템 SDK 제공 | Easydict/objc/ViewController/의 질의 창·테이블·입력 뷰 |
| SnapKit · Masonry | 5.6.0 · 1.1.0 | 각각 Swift·Objective-C에서 제약 기반 레이아웃 구성 |
| SettingsAccess · Defaults | 2.1.0 · 8.2.0 | 설정 화면 열기, 타입이 있는 환경설정 키·값 관리 |
| SelectedTextKit | 2.6.7 | 다른 앱에서 선택된 텍스트를 읽는 기능의 기반 |
| KeyHolder · Magnet | 4.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 결과도 다룹니다. 결과의 종류에 맞추어 표시 경로가 달라지는 셈입니다.
| 기술 | 잠긴 버전·설정 | 소스에서 확인한 역할 |
|---|---|---|
| Alamofire | 5.12.2 | 번역 API와 모델 목록 등의 HTTP 요청 |
| MacPaw/OpenAI | 0.5.1, 정확한 버전 지정 | ChatQuery·ChatStreamResult 모델. 스트리밍 전송은 별도 구현도 사용 |
| AsyncAlgorithms | 1.0.3 | 스트리밍 갱신 빈도 제어. 결과 처리의 기본 간격 0.3초 |
| Vision · NaturalLanguage | macOS SDK | 이미지 글자·QR 인식, 문장의 언어 판단 |
| Translation | macOS 15+ 경로 | 옵션이 켜져 있으면 Apple 오프라인 번역 사용 |
| Vapor · SwiftNIO | 4.122.0 · 2.91.0 | 앱 안에서 선택적으로 켜는 HTTP 서버와 네트워크 기반 계층 |
| CryptoSwift | 1.8.3 | 암호화 관련 유틸리티 의존성 |
| 로컬 저장 | Defaults · JSONL · 파일 | 설정·질의 이력·선택적 단어장·사전·로그. 별도 DB 서버 필수 구성 없음 |
QueryRecordManager.swift는 이력과 즐겨찾기를 Defaults 키로 관리하고 이력 상한을 1000으로 둡니다. VocabularyNotebookService.swift는 활성화된 경우 words.jsonl에 한 줄씩 기록합니다. 잠금 파일의 leveldb 같은 전이 의존성이 있다고 앱의 질의 이력을 LevelDB에 저장한다고 해석하면 틀립니다.
OpenAIStreamTransport는 stream 메서드에서 URLSession으로 전송하며, makeRequest가 만드는 요청의 제한 시간은 AppConstants.llmRequestTimeoutInterval = 60초를 참조합니다. SDK를 가져왔어도 전송·응답 검증을 제품 요구에 맞게 직접 감쌀 수 있다는 사례입니다.
2.6.4지만 잠긴 값은 2.8.1입니다. 새로 해석하지 않은 상태의 재현 기준을 알려 주는 파일이 잠금 파일입니다.| 파일·기술 | 확인한 내용 | 배울 점 |
|---|---|---|
Podfile.lock | Masonry 1.1.0, ReactiveObjC 3.1.1, JLRoutes 2.1 | 기존 Objective-C 의존성을 유지하는 CocoaPods 경계 |
Gemfile | CocoaPods 1.16.2 고정 | 라이브러리뿐 아니라 설치 도구의 버전도 기록 |
BuildTools/Package.resolved | SwiftFormat 0.63.0 | 포매터를 앱 런타임 의존성과 분리 |
| SwiftLintPlugins | 0.65.1 | 소스 품질 검사 의존성. 앱 실행 기능과는 구분 |
| Sparkle | 2.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·업데이트 피드가 각각 다루어집니다. 일반 앱 학습을 시작할 때 이 배포 절차부터 이해할 필요는 없습니다.
앱 진입점은 Easydict/App/EasydictApp.swift의 EasydictCmpatibilityEntry.main()입니다. 경로 마이그레이션·인자 처리·충돌 기록·OCR 예열 등을 준비한 뒤 EasydictApp.main()으로 넘어갑니다. SwiftUI의 @NSApplicationDelegateAdaptor는 기존 AppDelegate와 연결하고, 메뉴 막대와 설정 화면은 SwiftUI Scene으로 구성합니다.
일반 질의의 순서는 컨트롤러의 startQueryText:actionType: → queryCurrentModel → 필요하면 언어 감지 → queryAllSerives: → queryWithModel:service:입니다. queryAllSerives라는 철자는 소스 그대로입니다. 서비스마다 비동기 요청을 시작하므로, 한 서비스가 끝날 때까지 모두 기다린 뒤 다음 서비스를 시작하는 구조가 아닙니다.
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의 쓰임을 함께 보세요.
StreamService 계층은 조금씩 도착하는 AI 응답을 처리합니다. Easydict/Swift/Service/OpenAI/StreamService+AsyncStream.swift는 문자열 조각을 ChatStreamResult 또는 누적 QueryResult로 바꿉니다. StreamService+UpdateResult.swift의 throttleUpdateResultText는 기본 간격 0.3초로 갱신 빈도를 제어합니다.
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은 소비가 끝났을 때 내부 작업도 취소하도록 연결합니다. 스트림을 만드는 것과 내부 작업의 수명을 끝까지 관리하는 것은 함께 설계해야 합니다.
사용자가 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()가 이 동작의 의도를 보여 줍니다. 전자는 취소 중 재등록을, 후자는 개별 취소와 일괄 취소가 겹치는 경우를 다룹니다. 테스트 소스를 읽으면 복잡해 보이는 순서가 왜 필요한지 알 수 있습니다.
식당 주문표의 번호와 같습니다. 손님이 주문을 바꿨는데 예전 음식이 늦게 나오면, 직원은 현재 주문 번호와 비교하고 다른 테이블에 잘못 놓지 않습니다. 취소는 주방에 보내는 연락이고, 세대 번호는 서빙 직전 확인하는 주문표입니다.
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로 분기 |
| 결과 통일 | EZRecognizedTextObservation | Vision 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하지 않게 합니다. 실패 경로까지 포함한 “정확히 한 번 완료”는 콜백 변환에서 중요한 계약입니다.
사용 가이드는 Apple OCR 실패 시 선택적으로 Youdao를 시도한다고 설명합니다. 하지만 이 커밋의 DetectManager.ocr와 ocrAndDetectText는 기본 OCR 서비스를 직접 호출합니다. Youdao 대체 로직은 비공개 deepOCR가 사용하는 handleOCRResult 안에 있고, deepOCR의 호출부는 이 커밋의 앱·테스트 코드에 없습니다.
일반 OCR의 공개 진입점은 이 대체 로직을 거치지 않습니다. 이는 커밋 69600ed의 정적 호출 경로를 기준으로 한 설명입니다. 기능 설명과 실제 진입점을 함께 읽어야 하는 이유를 보여 줍니다.
위 트리의 첫 번째 Easydict/는 앱 소스 디렉토리이며, 그 아래의 테스트·프로젝트·문서 디렉토리는 저장소 루트에 있습니다. Service는 특정 엔진이 요청을 처리하는 곳, Feature는 사용자가 어떤 작업을 하는지 표현하는 곳, Utility는 여러 기능이 공유하는 도구로 읽으면 됩니다.
모든 파일이 이 분류에 완벽하게 들어맞지는 않습니다. 예를 들어 이력을 관리하는 QueryRecordManager.swift는 Easydict/Swift/View/SettingView/Tabs/Model/에 있습니다. 디렉토리 이름만 보고 저장 로직이 없다고 결론 내리기보다, 타입과 호출자를 함께 검색하세요.
| 범위·언어 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| 앱 Swift | 371 | 64,622줄 |
앱 Objective-C 구현 .m | 100 | 19,416줄 |
앱 헤더 .h | 106 | 3,679줄 |
| 앱 JavaScript | 4 | 243줄 |
| 앱 HTML | 1 | 538줄 |
| 테스트 Swift | 57 | 12,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의 코드 규모를 나타내며, 개발 속도나 주간 증가량을 뜻하지 않습니다.
EasydictApp.swift에서 앱 시작과 메뉴·설정 화면을 찾습니다.QueryModel.swift와 QueryResult.swift에서 입력·출력을 구분합니다.QueryServiceFactory.swift에서 관심 있는 서비스 하나를 고릅니다.AppleService.swift 또는 OllamaService.swift에서 구체적인 실행을 읽습니다.EZBaseQueryViewController.m에서 화면과 이 객체들이 연결되는 곳을 확인합니다.처음부터 모든 공급자의 API 요청 코드를 읽으면 차이만 늘어나고 공통 구조는 잘 보이지 않습니다. 같은 입력이 어느 경계에서 어떤 형태로 바뀌는지 추적하면, 배운 설계를 다른 앱에도 옮길 수 있습니다.
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가 붙고, 화면에는 콜백 기반 메서드를 따로 제공하는지도 설명해 봅니다.
QueryService.startQueryStream(_:completionHandler:)는 스트림을 순회하고 MainActor.run 안에서 화면용 콜백을 호출합니다. SelectionWorkflow.getSelectedTextSnapshot의 완료 콜백에는 @MainActor가 선언되어 있지 않습니다. 같은 “콜백”이라도 실행 문맥의 계약이 다릅니다.
작은 실습: 네트워크 대기, 문자열 누적, 결과 상태 변경, UI 표시 네 단계를 다른 색으로 표시해 보세요. async라는 키워드만 보고 무거운 계산이 자동으로 백그라운드에서 처리된다고 생각하지 않는 것이 출발점입니다. 비동기 실행과 스레드의 관계는 Swift 공식 동시성 문서에서 이어서 읽을 수 있습니다.
OCRConstants.swift에는 들여쓰기 판단용 1.8글자, 일반 줄 간격 상한 비율 1.2, 중국어 시의 한 줄 최대 글자 수 12.0, 영어 시의 한 줄 최대 단어 수 7.0 등이 있습니다. 이는 하드웨어 요구량이 아니라 레이아웃 판단을 위한 기준값입니다.
작은 실습: 문단·시·두 단으로 된 문서를 각각 골라 줄바꿈을 유지해야 하는 이유를 적어 보세요. OCRTextProcessingTests.swift, OCRTextProcessingChineseTests.swift, OCRPunctuationTests.swift의 입력·기대값을 비교하면 규칙이 어떤 문장을 보호하려는지 보입니다.
OCR 엔진이 낱말이 적힌 종잇조각을 건네면 후처리는 원래 페이지를 다시 조립하는 작업입니다. 같은 줄바꿈도 문단에서는 지워야 하고 시에서는 남겨야 합니다. 읽힌 글자 수만으로 품질을 평가하기 어려운 이유입니다.
Easydict/Swift/Service/Dictionary/MDict/MDictReader/MDictReader.swift는 MDX·MDD 사전 형식을 다룹니다. RecordBlockRange는 압축된 데이터와 압축 해제된 데이터의 위치를 저장하고, 필요한 레코드 블록을 찾는 데 사용됩니다. 상수에는 단일 압축 해제 블록 크기 상한 256 * 1024 * 1024바이트, 키 블록 캐시 16개, 레코드 블록 캐시 8개·50MiB가 보입니다.
작은 실습: 사전을 통째로 메모리에 올리는 방식과 필요한 블록만 읽는 방식의 비용을 비교하세요. MDictSearchIndex.candidates(for:limit:)는 기본 후보 8개를 접두어 → 부분 문자열 → 편집 거리 순서로 찾습니다. 편집 거리 상한은 2입니다. 정확한 검색과 비슷한 단어 제안을 서로 다른 경로로 나누는 이유를 배울 수 있습니다.
VocabularyNotebookService.append는 현재 화면에서 계속 바뀌는 입력보다 결과에 붙은 result.queryText를 기록에 사용합니다. 기록 작업은 전용 직렬 큐에 넣고, flush()는 앞서 들어온 쓰기가 끝나기를 기다립니다. 앱 종료 알림에서 동기적으로 flush()하는 코드가 EasydictApp.swift에 있습니다.
작은 실습: “번역이 끝남 → 파일 기록 예약 → 앱 종료” 순서를 그려 보세요. 예약만 하고 종료하면 왜 마지막 단어를 잃을 수 있는지, 마지막 줄에 개행이 없는 JSONL 파일에 바로 덧붙이면 왜 두 기록이 붙어 버리는지 설명해 봅니다.
| 읽을 테스트 | 확인할 요구사항 | 응용할 수 있는 곳 |
|---|---|---|
QueryModelCancellationTests.swift | 중복 취소·재등록·동시 접근 | 검색창·파일 업로드·다운로드 취소 |
OCRQRCodeTests.swift | QR만 있는 이미지·혼합 이미지·중복 QR | 스캔 앱의 복합 입력 처리 |
MarkdownRendererTests.swift | 스트리밍 도중의 불완전한 서식 입력 | 채팅·실시간 문서 미리보기 |
MDictReaderTests.swift | 사전 파일 읽기와 조회 | 바이너리 파서·대형 파일 탐색 |
테스트의 입력·기대값은 요구사항을 이해하는 단서입니다. 테스트가 존재한다는 사실만으로 실행 성공률이나 코드 커버리지를 판단할 수는 없습니다.
| 사용 범위 | 요구사항·조건 | 확인 근거 |
|---|---|---|
| 현재 앱 | 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+ · CocoaPods | docs/user-docs/en/GUIDE.md |
앱의 공식 최소 RAM·디스크·GPU 수치는 프로젝트 문서에 명시되어 있지 않습니다. 따라서 8GB나 16GB를 공식 최소 사양으로 적을 근거는 없습니다. CodeQL 설정의 arm64도 분석 대상 아키텍처이며, 배포 파일의 Intel 호환성을 보장하는 값은 아닙니다.
로컬 OCR을 쓰는 데 필요한 자원과, 로컬 LLM을 실행하는 데 필요한 자원은 별개입니다. 기본 OCR은 Vision을 이용합니다. 반면 Ollama의 모델 크기·정밀도·문맥 길이에 따른 메모리 요구량은 별도로 확인해야 합니다. OllamaService의 기본 주소는 http://localhost:11434/v1/chat/completions이며, 앱이 자체 추론 엔진을 내장했다는 뜻은 아닙니다.
Defaults.Keys+Extension.swift의 enableAppleOfflineTranslation은 false입니다. AppleService.translateAsync는 조건을 만족할 때 Translation 경로를 사용하고, 그렇지 않으면 AppleScript로 단축어를 호출합니다. 두 경로의 준비 조건을 하나로 뭉뚱그리지 마세요.
사용 가이드는 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의 엄격한 동시성 모드가 전면 적용되었다고 해석해서도 안 됩니다.
모델·팩토리·취소·SSE·파일 형식은 텍스트 편집기만으로 분석할 수 있습니다. 실제 AppKit 창·Vision OCR·접근성 권한 동작을 확인하는 단계에서는 해당 기능을 지원하는 Mac이 필요합니다.
실습은 원본 저장소와 분리한 학습용 예제에서 진행하는 구성을 권합니다. 앞의 두 과제는 소스 읽기만으로 완성할 수 있고, 뒤의 과제는 가짜 응답을 사용해 API 키와 유료 호출 없이 구현할 수 있습니다. 완료 기준을 눈에 보이는 산출물로 정하면, 코드를 읽었다는 느낌을 넘어 실제로 이해했는지 확인할 수 있습니다.
읽을 곳: QueryServiceFactory.swift, QueryService.swift, AppleService.swift, OllamaService.swift.
할 일: Apple Dictionary·Apple 번역·Ollama의 식별자, 키 요구 여부, 데이터 처리 위치, 필요한 외부 준비를 표로 적습니다. apiKeyRequirement: .none이 왜 오프라인 보장을 뜻하지 않는지도 설명합니다.
완료 기준: 서비스 세 행의 비교표와 “새 공급자를 등록하려면 생성·실행·설정 중 어디를 바꾸는가”에 대한 5문장 설명. 앱을 실행하지 않아도 코드로 근거를 제시할 수 있어야 합니다.
읽을 곳: Easydict/Swift/Feature/Markdown/MarkdownRenderer.swift의 render(_:)와 EasydictTests/Feature/Markdown/MarkdownRendererTests.swift.
할 일: 아래 입력을 한 줄씩 읽으며 inFence·codeBuffer가 어떻게 바뀌는지 적습니다. 마지막 코드 울타리가 아직 닫히지 않았을 때 실행되는 분기를 찾으세요.
## 오늘 배운 것
**아직 도착 중인 답변**
```swift
let count = 3
완료 기준: 줄마다 상태를 적은 표와, 닫는 울타리가 나중에 도착해도 앱이 중간 내용을 표시해야 하는 이유. 모든 Markdown 기능을 지원한다고 확대 해석하지 않고, 이 렌더러가 구현한 문법 범위도 목록으로 적습니다.
읽을 곳: QueryService.resetServiceResult(), StreamService+UpdateResult.swift, QueryModelCancellationTests.swift.
할 일: 네트워크 대신 AsyncThrowingStream<String, Error>에서 짧은 문자열을 차례로 내보냅니다. 입력이 바뀔 때 작업을 취소하고 세대 번호를 올리세요. 화면 갱신 직전에는 시작할 때의 세대와 현재 세대를 비교합니다.
완료 기준: A의 조각이 B의 결과에 섞이지 않는 시연, 취소 후 로딩 상태가 끝나는 화면, 스트림 종료 시 내부 작업을 정리하는 코드. 지연 시간의 우연한 순서에만 의존하지 말고, 조각을 원하는 순서로 주입할 수 있게 설계합니다.
더 생각할 점: 취소 요청을 보낸 직후에도 A의 조각이 올 수 있다면 어떤 검사가 마지막 방어선인지 설명해 보세요.
읽을 곳: 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좌표만으로 정렬했을 때 생기는 오류를 한 개 이상 보여 주세요. 좌표 원점·읽기 방향을 먼저 정하고 규칙을 설명할 수 있어야 합니다.
읽을 곳: Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift, QueryServiceFactory.swift, OpenAIStreamTransport.swift, VocabularyNotebookService.swift.
할 일: 입력을 그대로 돌려주는 엔진과 한 글자씩 응답하는 엔진을 별도 예제에 만듭니다. 일반 응답과 스트림 응답의 경로를 나누고, 알 수 없는 서비스·빈 입력·중도 취소·오류·정상 종료를 각각 설계합니다. 결과를 기록한다면 정상 완료만 저장하도록 합니다.
완료 기준: 요청·응답 형식, 상태 전이 그림, 다섯 상황의 기대 동작표, 중도 실패가 성공 기록으로 남지 않는 시연. 네트워크 서버를 구현하는 경우 루프백 바인딩과 인증 정책도 설계에 명시하세요.
더 생각할 점: 스트리밍 가능한 클래스라도 특정 요청은 비스트리밍 전송을 쓸 수 있습니다. 원본 /translate가 클래스 종류만 보지 않고 usesStreamingTransport를 확인하는 이유를 설명합니다.
정상 응답만 보이는 예제보다 “취소했을 때”, “빈 결과일 때”, “예전 결과가 늦게 왔을 때”를 함께 보여 주는 예제가 더 많은 설계 판단을 드러냅니다. 원본에서 읽은 해결책과 자신이 단순화한 부분을 구분해서 기록하세요.
하루에 새 라이브러리를 하나씩 추가하는 방식보다, 입력과 결과 사이의 계약을 점차 넓히는 순서가 초보자에게 적합합니다. 작은 동기 함수에서 시작해 비동기 결과, 취소, 파일 기록, 시스템 통합 순으로 확장해 보세요.
| 주차 | 학습 주제 | Easydict 읽기 지점 | 남길 산출물 |
|---|---|---|---|
| 1주 | Swift의 struct·class·enum·Optional·오류 처리 | QueryModel·QueryResult·QueryError | 입력·출력·오류 모델과 데이터 이동 그림 |
| 2주 | 상속·팩토리·공통 절차와 재정의 | QueryServiceFactory·AppleService·OllamaService | 더미 엔진 두 개와 같은 호출 인터페이스 |
| 3주 | async/await·AsyncSequence·취소·MainActor | StreamService+AsyncStream·QueryModel | 세대 번호가 있는 스트리밍 검색 예제 |
| 4주 | HTTP·SSE·응답 검증·오류 분류 | OpenAIStreamTransport·HTTPServer/Vapor | 분할된 이벤트를 복원하는 파서와 응답 표 |
| 5주 | OCR 관측값·좌표·언어·문단 복원 | AppleOCREngine·OCRTextProcessor | 세 종류 레이아웃의 병합 결과 비교 |
| 6주 | 저장·종료 처리·네이티브 앱 통합·배포 개념 | VocabularyNotebookService·EasydictApp·appcast.xml | 정상·오류·취소·종료까지 설명 가능한 작은 앱 |
매주 “함수가 무엇을 받는가, 무엇을 돌려주는가, 실패하면 누가 처리하는가”를 세 줄로 적으세요. 셋 중 하나라도 불분명하면 전체 파일을 더 읽기 전에 호출자와 결과 사용 지점을 찾아보는 편이 좋습니다.
CLI 쪽을 더 읽는다면 CodexCLIService.contentStreamTranslate가 좋은 입구입니다. 이 커밋에는 로컬 codex exec --json 경로와 관리형 App Server 경로가 함께 있습니다. 주석상 관리형 경로는 프로세스를 재사용하면서 번역마다 독립적인 임시 대화를 만들도록 설계되었습니다. 로컬 프로세스를 호출하는 것과 모델 추론이 로컬에서 일어나는 것은 구분해야 합니다.
| 용어 | 쉬운 설명 |
|---|---|
| SwiftUI | 현재 상태에 맞는 화면을 선언하는 Apple UI 프레임워크. 이 앱의 메뉴 막대·설정 화면에서 볼 수 있습니다. |
| AppKit | macOS의 창·뷰·입력 이벤트를 다루는 프레임워크. 기존 질의 창의 기반입니다. |
| Objective-C 브리지 | Swift와 Objective-C가 이름·타입·메서드를 맞추어 서로 호출하는 연결면입니다. |
| 팩토리 | 식별자를 받아 적절한 객체를 생성하는 창구. 서비스 생성 규칙을 한곳에 모읍니다. |
| 템플릿 메서드 | 공통 처리 순서를 정하고 일부 단계를 하위 클래스가 바꾸게 하는 패턴입니다. |
| 어댑터 | 서로 다른 입력·응답 형태를 사용자가 기대하는 공통 형태로 바꾸는 연결 코드입니다. |
| async / await | 기다리는 동안 실행을 잠시 양보할 수 있는 함수와 그 대기 지점을 표시합니다. |
| MainActor | UI처럼 함께 다뤄야 할 상태의 접근을 메인 실행 문맥에 모으는 격리 경계입니다. |
| AsyncThrowingStream | 값이 차례로 도착하고, 도중에 오류로 끝날 수도 있는 비동기 통로입니다. |
| 협력적 취소 | 취소 요청을 받은 작업이 적절한 지점에서 이를 확인하고 종료하는 방식입니다. |
| 세대 번호 | 이번 요청과 이전 요청을 구별하는 번호. 늦게 온 옛 결과를 버리는 데 사용합니다. |
| 재진입 | 콜백 실행 도중 같은 객체의 메서드가 다시 호출되는 상황입니다. |
| 잠금 | 공유 상태의 특정 읽기·변경 구간에 여러 실행 흐름이 동시에 들어오지 못하게 합니다. |
| continuation | 콜백이 끝났을 때 중단된 비동기 함수를 이어 주는 손잡이. 완료 처리는 한 번만 해야 합니다. |
| SSE | HTTP 연결에서 서버가 이벤트를 연속으로 보내는 형식. 이 앱은 AI 응답 조각을 읽는 데 사용합니다. |
| throttle | 잦은 이벤트의 전달 빈도를 제한하는 처리. 결과 화면을 너무 자주 갱신하지 않도록 돕습니다. |
| Vision 관측 결과 | 이미지에서 읽은 글자와 위치·신뢰도 등의 묶음. 최종 문단은 별도 후처리로 만듭니다. |
| 휴리스틱 | 길이·간격 같은 경험적 규칙으로 판단하는 방법. 모든 입력에 정답을 보장하지는 않습니다. |
| JSONL | 각 줄이 독립된 JSON인 텍스트 파일 형식. 단어장을 한 건씩 덧붙이기 편합니다. |
| MDX / MDD | MDict 사전의 항목과 리소스를 저장하는 파일 형식입니다. |
| 전이 의존성 | 직접 선택한 라이브러리가 다시 필요로 해서 함께 들어오는 라이브러리입니다. |
| deployment target | 앱이 지원 대상으로 삼은 최소 OS 버전. 빌드에 사용하는 최신 SDK 버전과는 다릅니다. |
| 공증 · appcast | 공증은 macOS 배포 검증 절차의 하나이며, appcast는 앱에 새 버전 정보를 알리는 업데이트 피드입니다. |
| 항목 | 상태 |
|---|---|
| 선정 | 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)와는 구분 |
| 최근 push | 2026-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 소스 제외 · 주석·빈 줄 포함 |
| 최소 실행 OS | macOS 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 기준입니다. 공개 릴리스와 이후 개발 소스의 동작은 다를 수 있습니다.