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

STranslate 딥다이브
— 단축키 한 번 뒤에 숨은 플러그인과 비동기 설계

데스크탑 앱

빠른 판단

무엇
단축키·선택 영역·클립보드·스크린샷을 번역·OCR 플러그인에 연결하는 Windows 앱
이럴 때
외국어 화면의 글자를 추출하거나 같은 문장을 여러 번역 엔진으로 처리하며 읽던 작업을 이어갈 때
입력
선택·클립보드 텍스트 또는 스크린샷
결과
플러그인별 OCR 텍스트·번역 결과
화면의 글자를 읽고 번역하는 Windows 도구를 따라가며, C# 앱이 입력·네트워크·데이터·화면을 연결하는 방법을 배웁니다. WPF와 MVVM부터 플러그인 로딩, 스트리밍 응답, 오래된 요청의 결과 차단까지 실제 코드로 살펴봅니다. 저장소 STranslate/STranslate · ⭐8,362 · 포크 438 · C# · MIT · 2026-10-23 GitHub 기준. 코드 설명은 커밋 75f616a 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

번역기를 만드는 코드보다, 번역기를 잘 연결하는 코드를 배웁니다

STranslate는 단축키·선택한 텍스트·클립보드·스크린샷을 번역과 OCR 서비스에 연결하는 Windows 데스크톱 앱입니다. 사용자가 읽던 화면을 벗어나는 일을 줄이고, 같은 입력을 여러 서비스로 처리할 수 있게 합니다.

이 저장소의 핵심 학습 주제는 여러 번역·인식 엔진을 하나의 안정적인 사용자 경험으로 묶는 설계입니다. 본체는 창과 설정, 요청의 수명, 결과 표시를 관리합니다. 실제 번역이나 문자 인식은 플러그인이 맡습니다.

한 문장 비유

STranslate는 통역사 한 명이 아니라, 여러 통역사를 연결하는 안내 데스크입니다

손님이 종이를 내밀면 직원은 글자를 읽고, 적절한 통역사에게 전달하고, 답을 받아 같은 창구에 돌려줍니다. 통역사가 바뀌어도 손님이 이용하는 창구는 그대로입니다.

여기서 안내 데스크는 WPF 앱, 업무 지침은 플러그인 인터페이스, 통역사는 Google·DeepL·OpenAI 등의 구현입니다. OCR은 사진 속 글자를 종이 위의 텍스트로 옮기는 첫 단계에 해당합니다.

용어 먼저
OCR과 번역은 서로 다른 작업입니다
OCR은 이미지의 픽셀에서 문자를 읽습니다. 번역은 읽어 낸 문장을 다른 언어로 바꿉니다. 사진 번역에는 두 단계가 필요하며, 번역문을 원래 위치에 표시하려면 문자의 좌표도 필요합니다.

독자의 행동으로 보는 세 가지 경로

하고 싶은 일입력과 결과코드에서 찾을 입구
문장 번역텍스트 → 번역 서비스들 → 결과 카드MainWindowViewModel.ExecuteTranslate()
화면 글자 추출스크린샷 → OCR → 복사하거나 편집할 텍스트IOcrPlugin.RecognizeAsync()
이미지 번역OCR 좌표 → 문단 분석 → 번역 → 원본 위의 글자ImageTranslateWindowViewModel.ExecuteAsync()

위 클래스의 본체는 src/STranslate/ViewModels/, 공통 인터페이스는 src/STranslate.Plugin/에 있습니다. 화면 처리와 엔진 호출을 서로 다른 파일에서 찾는 연습부터 시작하면 됩니다.

이 글을 읽고 얻을 것
버튼 뒤의 흐름을 끝까지 추적하는 능력

“입력값은 어디에 저장되는가?”, “두 요청의 순서가 뒤집히면 어떻게 되는가?”, “새 서비스 하나를 붙이려면 무엇을 구현해야 하는가?”를 소스에서 답할 수 있게 되는 것이 목표입니다.

2왜 주목받는가

관심의 배경과, 코드로 확인할 수 있는 장점을 구분합니다

2026-10-23 GitHub Trending C# Daily 6위에 선정되었고, 같은 날 GitHub 기준 스타 8,362개와 포크 438개를 기록했습니다. 다만 순위와 누적 스타만으로 당일 유입 경로나 이용자 수를 알 수는 없습니다. 아래는 기능과 설계를 근거로 해석한 관심 요인입니다.

  1. 작은 반복 작업을 줄입니다. 마우스로 고른 문장을 번역하거나 화면 일부를 OCR로 읽는 동작이 클립보드·전역 단축키와 이어집니다. 관련 코드는 src/STranslate/Helpers/ClipboardMonitor.cs, src/STranslate/Core/HotkeySettings.cs, src/STranslate/Services/MouseSelectionService.cs에 나뉩니다.
  2. 서비스를 바꿔도 앱을 계속 쓸 수 있습니다. 솔루션에는 번역·사전 계열 12개, OCR 7개, TTS 1개, 단어장 1개로 총 21개 플러그인 프로젝트가 있습니다. 이는 동시에 무료로 쓸 수 있는 서비스 수를 뜻하지는 않습니다.
  3. 일반 번역과 LLM 활용을 같은 틀에서 배울 수 있습니다. OpenAI 플러그인은 번역뿐 아니라 프롬프트를 통한 윤문·요약 설정도 갖습니다. 문장 처리 기능을 서비스 계약 위에 추가하는 사례입니다.
  4. 실제 제품에서 생기는 문제가 드러납니다. 요청 취소, 다중 모니터, 창의 활성화, 오른쪽에서 왼쪽으로 쓰는 글자, OCR 좌표 같은 문제에 구현과 테스트가 함께 있습니다.

대안과 비교하면 어디에 강점이 있나

대안대표 사용 흐름STranslate를 볼 이유
DeepL 데스크톱 앱화면을 캡처해 글자를 추출하고 DeepL로 번역여러 번역 서비스와 사용자별 설정을 한 앱에서 조합하는 구조를 공부하기 좋음
PowerToys Text Extractor화면의 문자를 OCR로 읽어 클립보드에 복사추출 뒤에 번역·이력·음성 읽기·이미지 위 결과 표시를 연결하는 흐름을 볼 수 있음
웹 번역 페이지브라우저에 문장을 붙여 넣고 결과를 확인전역 입력과 창의 수명까지 다루는 데스크톱 통합을 배울 수 있음
번역 API 직접 호출요청을 보내고 응답 문자열을 받음실패·취소·상태 표시·설정 저장까지 갖춘 응용 프로그램으로 확장하는 예제

비교의 기능 근거는 DeepL 화면 캡처 공식 설명과 PowerToys 공식 설명입니다. 번역 정확도·속도·가격의 우열을 측정한 비교는 아닙니다.

STranslate의 차별점은 번역 결과 하나보다, 입력부터 결과 활용까지 이어지는 작업 흐름과 교체 가능한 엔진 구조에 있습니다. 따라서 초보자에게도 네트워크 호출 한 개를 넘어 앱 전체를 읽는 교재가 됩니다.

소개 문구를 정확히 읽기
“바로 쓰는 도구”와 “모든 기능이 무설정·오프라인”은 다른 뜻입니다

README는 릴리스 파일을 내려받아 압축을 풀어 쓰는 방법을 안내합니다. 반면 소스에는 API 키·서비스 URL·모델·OCR 언어를 설정하는 코드가 있습니다. 어떤 엔진을 고르는지에 따라 계정, 네트워크, 비용과 지원 언어가 달라집니다.

플러그인의 이름에 BuiltIn이 있다고 모두 로컬에서 처리하는 것도 아닙니다. 내장 배포 여부와 네트워크 사용 여부는 별도로 확인해야 합니다.

3기술 스택 전체 지도

웹의 프론트·백엔드 구분을 데스크톱 앱에 맞게 다시 읽습니다

이 저장소의 핵심 실행 단위는 Windows 프로세스입니다. 웹 서버와 웹 프론트엔드를 따로 띄우는 구조가 아니라, WPF 화면과 C# 서비스가 한 앱 안에서 협력합니다. 여기서 “백엔드 역할”은 앱 내부의 처리·저장 계층을 가리킵니다.

언어와 실행 기반

항목실제 설정초보자가 읽어야 할 의미
본체 실행 대상net10.0-windows10.0.19041.0.NET 10과 Windows API를 대상으로 컴파일
화면 기술UseWPF=true, OutputType=WinExe브라우저가 아닌 Windows 창 프로그램
배포 아키텍처RuntimeIdentifier=win-x64본체 프로젝트가 지정한 런타임 식별자는 Windows x64
C# 설정LangVersion=preview, Nullable=enable미리 보기 언어 기능 허용, null 가능성을 컴파일러가 검사
플러그인 SDKnet10.0-windows, 패키지 1.0.15별도 프로젝트로 공유하는 인터페이스와 결과 모델

출처: 본체 프로젝트, 공통 빌드 설정, SDK 프로젝트. 이 설정들은 분석 커밋의 값이며, 이전 릴리스의 내부 구성을 대신 증명하지 않습니다.

용어 먼저
.csproj와 중앙 패키지 관리
.csproj는 프로젝트가 무엇을 만들고 어떤 패키지를 참조하는지 적은 명세입니다. 이 저장소는 src/Directory.Packages.props의 ManagePackageVersionsCentrally=true로 버전을 한곳에 모읍니다. 개별 프로젝트에 버전이 안 보여도 의존성이 없는 것은 아닙니다.

프론트엔드 역할: 화면, 바인딩, 입력

의존성선언 버전맡는 일
CommunityToolkit.Mvvm8.4.2관찰 가능한 속성과 명령 생성, MVVM의 반복 코드 축소
iNKORE.UI.WPF.Modern0.10.2.1현대적인 WPF 컨트롤·창 스타일
Hardcodet.NotifyIcon.Wpf2.0.1트레이 아이콘과 상주 앱의 진입점
NHotkey.Wpf / MouseKeyHook4.0.0 / 5.7.1전역 단축키와 마우스·키보드 입력 연동
ScreenGrab1.0.17화면 캡처 연동
Markdig.Wpf / WpfMath0.5.0.1 / 2.1.0마크다운·수식 표시 기능에 사용
XamlFlair.WPF1.2.13화면 전환과 애니메이션 관련 의존성

src/STranslate/Views/MainWindow.xaml은 창 위치와 폭을 설정 속성에 바인딩합니다. 다만 창 표시 애니메이션 전체가 XamlFlair에 맡겨진 것은 아닙니다. src/STranslate/Helpers/WindowShowAnimation.cs에는 네이티브 창을 직접 움직이는 별도 구현이 있습니다.

백엔드 역할: 통신, 저장, 플러그인 서비스

의존성선언 버전실제 책임
Microsoft.Extensions.Hosting10.0.9앱 서비스 구성의 출발점
Microsoft.Extensions.Http10.0.9IHttpClientFactory 기반 HTTP 클라이언트 공급
Microsoft.Data.Sqlite10.0.9로컬 번역 이력 데이터베이스
Dapper.Contrib2.0.78객체와 SQL 결과를 연결하는 가벼운 저장 계층
Serilog.Extensions.Logging10.0.0구조화된 로그 연동
Serilog.Sinks.File7.0.0날짜별 로그 파일 기록
NAudio / WebDav.Client2.3.0 / 2.9.0오디오 재생·WebDAV 백업 연동
WeChatOcr1.0.5WeChat OCR 플러그인의 문자 인식 의존성

두 표의 패키지 버전은 중앙 버전 파일의 선언값입니다. 설치 결과를 고정한 잠금 파일의 해석값과 혼동하지 마세요. 같은 파일에서 Microsoft.Extensions.Hosting.Abstractions는 9.0.7로 별도 선언되어 있어, 모든 패키지가 같은 버전이라고 가정할 수도 없습니다.

인프라·빌드: 소스에서 배포 파일까지

단계설정·도구읽을 포인트
솔루션src/STranslate.slnx본체·SDK·플러그인·테스트를 한 빌드 단위로 구성
릴리스 CIwindows-latest, .NET 10.0.x태그 push 또는 릴리스 공개 이벤트로 작동
소스 가져오기·SDKactions/checkout@v6, actions/setup-dotnet@v5액션 자체 버전과 앱 버전을 구분
컴파일build.ps1, Release 구성버전 정보를 넣고 솔루션을 다시 빌드
배포·업데이트Velopack 패키지 1.2.0, post_build.ps1vpk CLI는 버전 지정 없이 설치. 배포 패키지 생성과 이전 배포 다운로드에 사용
릴리스 업로드softprops/action-gh-release@v2publish/*를 GitHub 릴리스에 첨부
테스트xunit 2.9.3, Microsoft.NET.Test.Sdk 18.7.0테스트 프로젝트는 존재하지만 해당 배포 워크플로에 테스트 실행 단계는 없음

출처: 릴리스 워크플로, 빌드 스크립트, 패키징 스크립트. 이 CI 파일은 배포 자동화의 근거이지, 모든 테스트 통과의 근거는 아닙니다.

스택 읽기의 핵심

패키지 이름을 외우기보다 화면 → 상태 → 실행 조정 → 플러그인 → 통신·저장으로 책임을 나누세요. 새 라이브러리를 만나도 어느 층에 들어가는지 판단할 수 있습니다.

4아키텍처 심화 분석

실제 호출 경로와 네 가지 설계 원리

전체 구조: 같은 프로세스 안의 협력

[사용자]
   | 직접 입력 / 선택한 텍스트 / 클립보드 / 단축키
   v
[Views + Controls: WPF 화면]
   | Binding / Command                  | Binding / Command
   v                                    v
[MainWindowViewModel]                 [ImageTranslateWindowViewModel]
   | DebounceExecutor / 언어 판별         | OCR + 좌표 + 영역별 번역
   | TranslationOperation               |
   v                                    v
[ITranslatePlugin / IDictionaryPlugin]  [IOcrPlugin / ITranslatePlugin]
   |                                    |
   +-----------------+------------------+
                     |
         선택한 플러그인에 따라 호출
           [HttpService] / [WeChatOcr 등]
                |               |
           외부 서비스       로컬 의존성

[TranslationResultCoordinator] - 메인 번역의 최신 결과 중계
[TranslateService / OcrService] - 서비스 목록·선택 제공
[ServiceManager] - 서비스 생성 시 플러그인 + PluginContext 구성
[PluginManager] - plugin.json 탐색 + DLL 타입 로딩
[SqlService] --- 데이터 디렉터리의 Cache/history.db
[StorageBase] -- 설정 JSON + 임시 파일 + 백업
[App / DI 컨테이너] - 위 구성요소를 만들고 연결

도식의 외부 서비스는 선택한 플러그인에 따라 달라집니다. 본체가 자체 번역 서버를 내장하거나 모델 가중치를 학습하는 것으로 읽으면 안 됩니다. TranslateService는 이름과 달리 번역 알고리즘보다 서비스 목록·선택·설정 연결을 주로 담당합니다.

패턴 1. MVVM: 화면은 상태를 보고, 행동은 명령으로 전달

View는 XAML 창과 컨트롤, ViewModel은 입력·선택·명령 상태, Model은 요청·결과·설정 데이터입니다. 핵심 파일은 src/STranslate/Views/MainWindow.xaml과 src/STranslate/ViewModels/MainWindowViewModel.cs입니다.

// MainWindowViewModel.cs: 취소 가능한 명령을 생성하는 선언
[RelayCommand(IncludeCancelCommand = true, CanExecute = nameof(CanTranslate))]
private async Task TranslateAsync(object? force, CancellationToken cancellationToken)
{
    // 내부에서는 ExecuteAutomaticTranslationAsync()로 전달합니다.
}

// MainWindow.xaml: Esc 키와 ViewModel의 명령 연결 부분
<KeyBinding
    Key="Esc"
    Command="{Binding CancelCommand}"
    CommandParameter="{Binding ElementName=STWindow}" />

위 C# 예시는 선언을 보이기 위해 본문을 생략했습니다. [RelayCommand]는 컴파일 과정에서 TranslateCommand·TranslateCancelCommand를 생성합니다. Esc에 연결된 CancelCommand는 별도 Cancel() 메서드에서 생성되며, CancelAllOperations()를 거쳐 번역도 취소합니다. XAML에 연결된 명령과 같은 이름의 속성이 코드에 없다면, 먼저 생성기 특성을 확인하세요.

결과 객체도 연결점입니다. src/STranslate.Plugin/ITranslatePlugin.cs의 TranslateResult는 ObservableObject를 상속하고, Text·IsProcessing 등을 [ObservableProperty]로 선언합니다. 플러그인이 결과를 바꾸면 속성 변경 알림을 통해 화면에 반영할 수 있습니다.

용어 먼저
바인딩과 변경 알림
바인딩은 “이 칸에는 저 객체의 값을 보여 줘”라는 연결입니다. 변경 알림은 “값이 바뀌었으니 다시 읽어 줘”라는 신호입니다. 연결만 하고 신호가 없으면 화면이 이전 값을 계속 보여 줄 수 있습니다.

패턴 2. 의존성 주입: 조립 위치를 한곳으로 모으기

src/STranslate/App.xaml.cs 생성자는 Host.CreateDefaultBuilder()와 ConfigureServices()로 서비스를 등록합니다. OnStartup()은 플러그인 발견, 서비스 복원, DB 초기화, 메인 창 초기화를 이어 갑니다.

// App.xaml.cs: 실제 등록에서 핵심만 발췌
services.AddSingleton<PluginManager>();
services.AddSingleton<ServiceManager>();
services.AddSingleton<TranslateService>();
services.AddSingleton<IHttpService, HttpService>();
services.AddSingleton<SqlService>();
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<ImageTranslateWindowViewModel>();

Singleton은 컨테이너에서 같은 인스턴스를 공유하고, Transient는 요청할 때 새 인스턴스를 만듭니다. 서비스 관리자처럼 앱 전역에서 공유할 객체와 창별 상태를 나누는 기준을 읽을 수 있습니다. 다만 개별 번역 플러그인 인스턴스는 아래의 서비스 생성 경로가 따로 만듭니다.

네트워크도 공통으로 등록합니다. 이름 있는 HTTP 클라이언트의 기본 제한 시간은 30초이며 프록시 핸들러를 설정에서 구성합니다. 실제 호출에서는 HttpService.ConfigureClientTimeout()가 호출별 값 또는 Settings.HttpTimeout을 적용하므로 30초가 고정 불변값은 아닙니다.

패턴 3. 플러그인 계약과 전략 교체

PluginManager.LoadPlugins()가 플러그인 메타데이터를 모으고, PluginAssemblyLoader가 DLL에서 IPlugin을 구현한 구체 클래스를 찾습니다. ServiceManager.CreateService()는 고유한 ServiceID, 플러그인 객체, PluginContext를 묶습니다.

// ITranslatePlugin.cs: 번역 요청과 공통 호출 계약
public record TranslateRequest(string Text, LangEnum SourceLang, LangEnum TargetLang);

Task TranslateAsync(
    TranslateRequest request,
    TranslateResult result,
    CancellationToken cancellationToken = default);

마지막 선언은 인터페이스 내부의 메서드입니다. 호출자는 Google인지 OpenAI인지 몰라도 같은 계약으로 요청할 수 있습니다. 서로 다른 구현을 공통 방식으로 바꿔 쓰는 전략 패턴으로 이해할 수 있습니다.

하나의 플러그인과 하나의 서비스는 다릅니다. 같은 OpenAI 플러그인으로 모델이나 프롬프트가 다른 서비스 인스턴스를 만들 수 있습니다. ServiceManager.DuplicateService()도 새 ID를 발급하고 설정을 복사하며, 실패하면 새 데이터를 정리합니다.

비유로 구분하기

플러그인은 커피 머신의 설계와 제조사, 서비스는 그 머신에 저장한 나만의 추출 설정에 가깝습니다. 같은 제조사의 머신이라도 연하게·진하게 설정한 두 서비스를 별도로 운영할 수 있습니다.

PluginContext는 플러그인에 HTTP·로그·설정 저장·음성 재생 등을 제공합니다. 플러그인이 앱의 모든 내부 파일을 직접 참조하지 않아도 되는 연결점입니다. 하지만 DLL은 앱 프로세스 안에 로드됩니다. AssemblyLoadContext 사용을 보안 격리 실행으로 해석하면 안 됩니다.

패턴 4. 요청 스냅샷과 최신 결과만 반영하기

번역 A를 요청하고 곧바로 B를 요청했다고 합시다. B가 먼저 끝난 뒤 A가 도착하면 화면이 옛 문장으로 돌아갈 수 있습니다. CancellationToken으로 취소를 요청해도, 상대 서비스나 구현이 즉시 멈춘다는 보장은 없습니다.

이 앱은 요청을 취소하는 일과, 오래된 결과가 화면에 들어오지 못하게 막는 일을 별도 장치로 처리합니다. 핵심은 TranslationOperation과 TranslationResultCoordinator의 협력입니다.

  1. BeginOperation()이 Interlocked.Increment()로 증가하는 작업 번호를 발급합니다.
  2. TranslationOperation은 그때의 문장·원문 언어·목표 언어를 읽기 전용 속성에 보관합니다.
  3. 플러그인에는 요청별 임시 TranslateResult를 전달하고, 화면에 보이는 결과 객체와 구분합니다.
  4. Forward()가 속성 변경을 중계하되 TryPublish()가 현재 채널의 번호를 검사합니다.
  5. 검사와 쓰기를 같은 lock 안에서 처리해 두 동작 사이에 새 요청이 끼어드는 틈도 줄입니다.
// TranslationResultCoordinator.TryPublish(): 핵심 임계 구역
lock (state.SyncRoot)
{
    if (state.OperationId != operationId)
        return false;

    publish();
    return true;
}

작업별 입력을 고정하는 것과 결과별 게시 권한을 검사하는 것이 함께 필요합니다. 자동 복사와 언어 감지에도 별도 채널이 있어, 결과 문자열 외의 부수 효과도 오래된 작업과 분리하려는 설계가 드러납니다.

자동 번역 한 번을 실제 순서대로 따라가기

OnInputTextChanged()
  → AutoTranslate 설정 검사
  → DebounceExecutor.Execute(..., AutoTranslateDelayMs)
  → CancelAllOperations() / TranslateCommand
  → ExecuteAutomaticTranslationAsync()
  → BeginAutomaticOperation(): 입력 스냅샷과 작업 번호
  → ExecuteTranslateAsync(): 이력 캐시 조회
  → 미충족 서비스의 언어 결정과 번역
  → TranslationOperation.TranslateAsync(): 임시 결과와 안전한 중계
  → 최신 작업인지 검사 후 자동 복사·이력 저장

Settings.AutoTranslate의 초기값은 false, AutoTranslateDelayMs는 500입니다. 자동 번역을 켠 경우에만 입력 변화 뒤 기본 500ms를 기다립니다. DebounceExecutor는 세대 번호를 올려 이전 예약의 실행을 무효화하며, 대기 중인 Task.Delay 자체를 취소하는 구현은 아닙니다.

캐시가 없는 서비스들은 ExecuteTranslationForServicesAsync()에서 Task.WhenAll()로 기다립니다. 동시 실행 상한은 Math.Min(services.Count(), Environment.ProcessorCount * 10)이며 SemaphoreSlim으로 제한합니다. 이 값은 API 업체의 초당 요청 제한을 알아서 지켜 주는 속도 제한기는 아닙니다.

관련 검증 의도는 TranslationResultCoordinatorTests.cs의 CancelledOldExecutionCannotOverwriteNewCompletedExecution()과 NewerStreamingResultCannotBeOverwrittenByCancelledRequest()에서 확인할 수 있습니다. 테스트 이름을 먼저 읽으면 구현이 막으려는 실패를 빠르게 파악할 수 있습니다.

이미지 번역은 별도의 처리 파이프라인입니다

ImageTranslateWindowViewModel.ExecuteAsync()는 이미지 바이트와 폭·높이를 OCR에 전달합니다. 이후 Utilities.PrepareOcrResult(), ApplyLayoutAnalysis(), OcrWordBuilder를 거쳐 영역별 텍스트를 준비하고 Parallel.ForEachAsync()로 번역합니다.

번역이 끝나면 ImageTranslateRenderer.CreateTranslatedOverlay()가 원본 좌표계의 번역문 오버레이 문서를 만듭니다. 표시에는 벡터 오버레이를 쓰고, 이미지로 내보낼 때 비트맵을 만듭니다. 플러그인이 실패 결과나 빈 번역문을 반환하면 해당 블록은 OCR 원문을 유지하고 로그를 남깁니다. 플러그인 호출에서 발생한 예외는 바깥쪽 취소·오류 처리로 전달됩니다.

입력 → 변환 → 분석 → 번역 → 표시라는 흐름은 문서 처리나 이미지 분석 앱에도 옮겨 생각할 수 있습니다. 다만 이 경로는 메인 텍스트 번역의 조정기를 그대로 통과하지 않으므로, 두 기능의 동시성·오류 처리가 완전히 같다고 가정하지 마세요.

5디렉토리 구조 해부

어디서 시작해서 어디로 이동할지 정합니다

아래는 저장소 루트 기준의 구조입니다. 실제 솔루션 디렉토리의 이름이 src/이고, 그 안에 본체와 SDK, 플러그인 프로젝트가 있습니다. 트리는 학습에 필요한 항목만 추렸습니다.

STranslate/
├── README.md / README_ZH.md      사용 안내
├── CHANGELOG.md / LICENSE        변경 기록과 라이선스
├── PRIVACY.md                    개인정보 관련 안내
├── .github/workflows/dotnet.yml  Windows 릴리스 자동화
├── build.ps1                    버전 반영과 솔루션 빌드
├── post_build.ps1               배포·업데이트 패키지 생성
└── src/
    ├── STranslate.slnx          솔루션 구성
    ├── Directory.Build.props   공통 컴파일·출력 설정
    ├── Directory.Packages.props 패키지 버전 모음
    ├── STranslate/             앱 본체
    │   ├── App.xaml.cs         시작과 서비스 조립
    │   ├── Views/              창과 설정 페이지
    │   ├── ViewModels/         화면 상태와 명령
    │   ├── Controls/           입력·출력 등 재사용 UI
    │   ├── Core/               로딩·저장·통신·작업 조정
    │   ├── Services/           종류별 서비스 목록 관리
    │   ├── Helpers/            입력·언어·창 동작 보조
    │   ├── Converters/         바인딩 값 변환
    │   ├── Languages/          언어별 XAML 리소스
    │   └── Resources/          스타일·아이콘·보조 실행 파일
    ├── STranslate.Plugin/      인터페이스와 공용 모델
    │   ├── ITranslatePlugin.cs / IOcrPlugin.cs
    │   ├── IPluginContext.cs / Service.cs
    │   └── build/ / buildTransitive/  플러그인 패키징 대상
    ├── Plugins/                21개 플러그인 프로젝트
    │   ├── STranslate.Plugin.Translate.OpenAI/
    │   ├── STranslate.Plugin.Ocr.WeChatBuiltIn/
    │   └── ...
    └── Tests/STranslate.Tests/  요청·좌표·창·프로토콜 테스트

이름이 비슷한 세 폴더 구별하기

폴더책임수정할 기능을 찾는 기준
src/STranslate.Plugin/여러 구현이 공유하는 계약새 요청 필드나 플러그인 능력을 모든 구현에 알릴 때
src/Plugins/특정 업체·엔진의 구현API 경로, 인증, 응답 파싱, 해당 엔진 설정을 볼 때
src/STranslate/Services/사용자 서비스 목록과 활성화 상태서비스 추가·복제·순서·선택 동작을 볼 때

코드 규모: 파일의 물리적인 줄 수

범위파일 수줄 수
C# 전체34252,834
그중 앱 본체 C#19837,158
그중 SDK C#242,451
그중 플러그인 C#907,358
그중 테스트 C#295,844
XAML 전체26023,425
그중 Languages 제외 XAML6512,732
PowerShell2231

커밋 75f616a의 확장자별 텍스트 줄 수이며 빈 줄·주석·언어 리소스를 포함합니다. C# 세부 네 범위 외에 src/SolutionAssemblyInfo.cs 23줄이 있어 전체와 합계가 맞습니다. XAML에는 다국어 문자열도 많으므로 전부 화면 배치 코드라고 읽으면 규모를 과대평가하게 됩니다.

처음부터 5만 줄을 읽을 필요는 없습니다. 요청 모델 → 플러그인 하나 → 결과 조정 → ViewModel → XAML 순서로 한 문장의 여정을 좁혀 읽으세요. 테스트는 각 단계에서 기대한 동작을 확인하는 지도 역할을 합니다.

6학습 포인트 (기술별)

문법을 실제 문제와 연결하고, 작은 실습으로 확인합니다

① C#과 MVVM: 문자열 하나가 화면을 바꾸는 과정

src/STranslate.Plugin/ITranslatePlugin.cs의 TranslateResult부터 읽으세요. partial, 속성 생성기, 상속을 한 파일에서 만날 수 있습니다. record TranslateRequest는 요청 묶음을 표현하고, 관찰 가능한 결과 객체는 실행 중 변하는 상태를 표현합니다.

// TranslateResult의 실제 속성 선언 일부
[ObservableProperty] public partial bool IsSuccess { get; set; } = true;
[ObservableProperty] public partial string Text { get; set; } = string.Empty;
[ObservableProperty] public partial bool IsProcessing { get; set; } = false;

배울 것: 입력 데이터와 화면 상태의 차이, INotifyPropertyChanged가 필요한 이유, 생성된 명령과 속성을 추적하는 법입니다. 실습 아이디어: 결과 텍스트와 처리 중 표시를 종이에 그린 뒤, 어느 속성의 변경이 어느 표시를 바꾸는지 연결해 보세요.

② 비동기와 동시성: 기다리는 동안 앱이 해야 할 일

async/await는 비동기 작업을 이어 쓰는 문법이며, 붙였다고 언제나 별도 스레드에서 실행되는 것은 아닙니다. 이 앱은 네트워크를 기다리면서 여러 서비스를 진행하고, 필요한 UI 작업은 Dispatcher에 전달합니다.

배울 것: Task.WhenAll()로 여러 완료를 기다리는 법, SemaphoreSlim으로 진행 수를 제한하는 법, CancellationToken을 하위 호출까지 넘기는 법입니다. ConfigureAwait(false)는 대기 뒤 원래 컨텍스트로 돌아갈 필요가 없음을 표시하며, CPU 작업을 자동 병렬화하는 지시가 아닙니다.

비동기 UI의 정확성은 요청을 빨리 보내는 능력보다, 어떤 요청이 어떤 상태를 바꿀 수 있는지 정하는 데서 시작합니다. A가 늦고 B가 빠른 상황을 시간표로 그린 뒤 조정기의 작업 번호를 표시해 보세요.

헷갈리기 쉬운 세 가지
디바운스 · 취소 · 최신성 검사
디바운스는 연속 입력 중 요청을 언제 시작할지 정합니다. 취소는 시작한 작업에 중단을 요청합니다. 최신성 검사는 이미 도착한 결과를 지금 반영해도 되는지 판단합니다. 하나가 다른 둘을 대신하지는 못합니다.

③ HTTP와 스트리밍: 답 전체가 오기 전에 표시하기

src/STranslate/Core/HttpService.cs의 StreamPostAsyncEnumerable()은 ResponseHeadersRead로 응답 헤더를 받은 뒤 본문을 스트림으로 읽습니다. ReadLineAsync()로 비어 있지 않은 줄을 전달하고, OpenAI 플러그인이 그 줄의 의미를 해석합니다.

// OpenAI Main.ExecuteStreamingAsync()의 처리 흐름을 줄인 예시
await foreach (var line in Context.HttpService.StreamPostAsyncEnumerable(
    url, content, option, cancellationToken))
{
    var streamEvent = OpenAIProtocol.ParseStreamLine(apiMode, line);
    // 실제 구현은 오류·빈 델타 등을 검사한 뒤 텍스트를 누적합니다.
}

src/Plugins/STranslate.Plugin.Translate.OpenAI/OpenAIProtocol.cs는 Chat Completions의 choices[0].delta.content와 Responses의 response.output_text.delta를 공통 OpenAIStreamEvent로 바꿉니다. 이런 변환 계층을 두면 화면 쪽은 외부 응답 형식의 차이를 덜 알게 됩니다.

배울 것: IAsyncEnumerable<T>, 스트림의 수명, 델타 누적, 외부 형식의 정규화입니다. 실습 아이디어: 빈 줄, [DONE], 텍스트 델타, 오류 이벤트를 넣고 파서의 결과를 표로 적어 보세요. 서버 호출 없이 문자열만으로도 연습할 수 있습니다.

코드의 기본 모델 문자열은 gpt-4o, 기본 모드는 ChatCompletions입니다. 이는 해당 커밋의 초기 설정값이며, 현재 이용 가능한 모델이나 권장 모델 목록을 뜻하지 않습니다. 추가 JSON 매개변수는 내장 필드를 덮어쓰지 못하도록 검사합니다.

④ SQLite와 캐시: 저장하는 것과 다시 쓰는 것

src/STranslate/Core/SqlService.cs는 이력을 History 테이블에 저장합니다. SourceText·SourceLang·TargetLang으로 이전 항목을 찾고, RawData에 서비스별 결과를 직렬화합니다. EffectiveSourceLang·EffectiveTargetLang은 실제 처리에 사용한 언어를 따로 보관합니다.

Settings.HistoryLimit의 초기값은 1,000건입니다. ExecuteTranslateAsync()는 이력 저장이 켜져 있고 강제 재번역이 아닐 때 캐시를 먼저 확인한 뒤, 충족되지 않은 서비스만 요청합니다. 이력 기능과 번역 캐시가 같은 저장 데이터를 활용하는 셈입니다.

// SqlService.GetDataAsync(content, source, target)의 매개변수 방식
"SELECT * FROM History WHERE SourceText = @SourceText " +
"AND SourceLang = @SourceLang AND TargetLang = @TargetLang"

// 같은 클래스의 검색용 오버로드는 문자열을 직접 삽입합니다.
var query = $"SELECT * FROM History WHERE LOWER(SourceText) LIKE '%{content.ToLower()}%'";

두 구현의 차이는 좋은 학습 소재입니다. 검색용 오버로드는 따옴표가 든 입력을 SQL 문자열에 직접 넣습니다. Dapper를 쓴다는 사실만으로 모든 쿼리가 매개변수화되는 것은 아닙니다. 실습 아이디어: 검색어 O'Reilly를 매개변수로 전달하도록 고치고, 정상 검색을 검증하는 과제를 만들어 보세요.

캐시 무효화도 질문해 보세요. 모델이나 프롬프트를 바꿨을 때 예전 결과를 계속 써도 될까요? 상위 이력 조회 조건에는 모델·프롬프트 해시가 없습니다. HistoryModel.GetData()는 플러그인 ID와 서비스 ID로 세부 결과를 찾습니다. 이 재사용 조건과 강제 재번역 경로까지 함께 읽어야 합니다.

⑤ OCR와 좌표: 글자와 위치를 함께 모델링하기

IOcrPlugin.SupportBoxPoints()의 기본 반환값은 false입니다. 일반 OCR은 텍스트만 있어도 쓸 수 있지만, 이미지 위에 번역문을 놓으려면 위치 정보가 필요합니다. OcrService.IsImageTranslateOcrService()가 이 능력을 검사합니다.

배울 것: OcrResult의 평탄한 OcrContents와 구조화된 Regions → Paragraphs → Lines, 사각형 좌표, 읽기 순서, 화면 배율입니다. 이미지 픽셀과 WPF 논리 좌표를 같은 단위라고 가정하면 확대·축소 때 어긋날 수 있습니다.

실습 아이디어: 1,000×500 이미지의 한 영역을 표시 폭 500으로 줄일 때 좌표가 어떻게 바뀌는지 계산하세요. 그런 다음 src/STranslate/Core/BoxPointLayout.cs, src/STranslate/Core/ImageTranslateTextOverlayLayout.cs, src/STranslate/Controls/ImageTranslateOverlay.cs에서 사각형·글자 배치·표시의 책임을 구분합니다.

⑥ 설정·국제화·창 동작: 작아 보여도 제품을 좌우하는 코드

StorageBase<T>.Save()는 JSON을 .tmp에 쓴 뒤 File.Move() 또는 File.Replace()로 반영합니다. 설정 파일이 없거나 비어 있을 때, JSON 파싱 오류나 null 결과가 생길 때는 백업을 시도합니다. 파일 읽기 자체의 I/O 예외까지 복구하는 구현은 아닙니다. “저장 버튼 한 번”에도 중간 실패를 고려한 절차가 필요하다는 예제입니다.

Internationalization.InitializeLanguage()는 기본 영어 리소스와 선택 언어를 불러옵니다. src/STranslate/Languages/ko.xaml이 한국어 화면 리소스이고, BidiDirectionHelper.GetFlowDirection()은 명시 언어 → 감지 언어 → 텍스트의 첫 강한 방향 문자 순으로 글자 방향을 결정합니다.

최신 커밋과 연결되는 작은 사례도 있습니다. WindowShowAnimation.GetOffset()는 18 * Math.Pow(1 - progress, 3)으로 180ms 동안 아래쪽 오프셋을 0으로 줄입니다. 기존 계층형 창이 아닌 경우 알파 변화도 140ms 동안 적용하며, 시스템의 애니메이션 설정을 확인합니다. 보기 좋은 움직임뿐 아니라 바인딩 복구·입력 시 종료·이벤트 해제를 함께 살펴보세요.

코드 읽기 습관

파일마다 “정상 경로”, “실패 경로”, “정리 경로” 세 가지를 표시하세요. try/finally, using, Dispose()는 성공한 화면 뒤에 숨은 자원 수명을 보여 줍니다.

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

확인된 실행 대상과, 아직 수치로 말할 수 없는 요구량

이 앱을 공부하기 위해 고성능 GPU부터 준비할 필요는 없으며, 먼저 Windows와 선택한 플러그인의 실행 조건을 확인해야 합니다. 본체 소스에는 모델 훈련용 CUDA·PyTorch 환경이 없고, 여러 번역 구현은 외부 서비스에 요청을 보냅니다.

항목코드·설정에서 확인한 내용해석과 한계
운영체제WPF와 Windows 전용 타깃앱 실행에는 Windows가 필요. macOS·Linux에서는 소스 읽기와 문서 학습 가능
대상 아키텍처본체의 win-x64ARM64 네이티브 배포를 이 설정만으로 보장할 수 없음
Windows API 대상10.0.19041.0타깃 API 버전. 모든 기능의 공식 최소 지원 OS를 뜻하는 시험 결과는 아님
개발 도구CI가 .NET SDK 10.0.x 사용C# 미리 보기 기능과 WPF를 처리할 수 있는 개발 환경 필요
배포본 런타임소스는 .NET 10을 대상으로 함릴리스별 런타임 포함 여부·설치 조건은 배포 안내에서 별도 확인
CPU·RAM수치로 제시된 공식 최소 사양은 확인되지 않음기본 창과 대형 이미지 번역의 사용량이 같다고 가정하지 않기
GPU본체에 GPU 추론 필수 설정은 없음WPF 렌더링과 엔진별 의존성은 별도. GPU별 성능 비교 수치 없음
디스크앱·플러그인·설정·이력·로그를 저장공식 최소 여유 공간 수치는 확인되지 않음. 사용한 엔진과 이력에 따라 증가
네트워크·계정외부 번역·OCR·TTS·업데이트·백업 경로 존재선택한 서비스의 접속·인증 조건을 따름. 전체 앱의 오프라인 동작을 보장하지 않음

WPF가 Windows에서 실행된다는 설명은 Microsoft WPF 개요, 실제 타깃 값은 본체 프로젝트 설정을 기준으로 합니다. EnableWindowsTargeting=true를 다른 OS에서 실행 가능하다는 뜻으로 해석하지 마세요.

엔진을 선택할 때 나눠 확인할 조건

실행 조건을 보는 비유

앱 본체는 콘센트이고 플러그인은 연결하는 기기와 비슷합니다. 콘센트가 준비되어도 연결한 기기의 전압·부속품·통신 조건은 따로 맞춰야 합니다. “앱이 켜짐”, “OCR이 됨”, “원하는 언어로 번역됨”을 각각 확인하는 이유입니다.

이미지 처리에서 생각해 볼 자원 규모

4바이트 픽셀 형식을 가정하면 3,840×2,160 이미지 한 장의 픽셀 배열은 약 31.6MiB입니다. 이는 해상도와 픽셀 크기로 계산한 예시이며 앱의 실제 메모리 측정값은 아닙니다. 원본·변환·표시용 복사본이 생기면 사용량이 늘 수 있습니다.

따라서 큰 이미지를 다룰 때는 “GPU가 몇 GB인가” 하나보다 이미지 복사 횟수, 오버레이 방식, 동시 요청 수, 자원 해제를 먼저 읽는 것이 도움이 됩니다. 이 저장소의 벡터 오버레이와 using 경로를 연결해 보세요.

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

기능 추가보다 먼저, 기대한 동작을 명확하게 정합니다

아래 과제는 쉬운 순서대로 이어집니다. 네트워크 없는 모의 응답으로 핵심 원리를 먼저 확인할 수 있습니다. 원본 동작과 비교할 수 있도록 실습 변경은 독립된 연습 사본에서 진행하세요.

과제 1 · 입문

한 문장의 여행 지도 만들기

목표: 자동 번역을 켠 상태에서 입력부터 결과 표시까지 호출 경로를 설명합니다.

출발점: MainWindowViewModel.OnInputTextChanged(), DebounceExecutor.Execute(), ExecuteAutomaticTranslationAsync(), TranslationOperation.TranslateAsync()입니다.

할 일: 호출 화살표를 그리고, 각 단계 옆에 입력값·대기·취소·화면 변경 중 무엇을 담당하는지 적으세요. AutoTranslate=false일 때 어느 단계에서 끝나는지도 표시합니다.

완료 기준: “기본 500ms는 어디서 오는가?”, “강제 재번역은 왜 캐시를 건너뛰는가?”에 파일과 함수 이름으로 답할 수 있습니다.

# 저장소 루트에서 읽기 경로를 찾는 명령 예시
rg -n 'OnInputTextChanged|AutoTranslateDelayMs' src/STranslate
rg -n 'BeginAutomaticOperation|TryPublish' src/STranslate/Core
rg -n 'ExecuteTranslateAsync|PopulateResultsFromCache' src/STranslate/ViewModels
과제 2 · 입문

인터넷 없이 동작하는 가짜 번역 엔진 설계하기

목표: 엔진과 UI가 공통 인터페이스로 연결된다는 사실을 확인합니다.

출발점: TranslatePluginBase와 실제 플러그인의 Main.cs, plugin.json입니다. 번역문 대신 입력에 [연습]을 붙이는 구현을 구상합니다.

할 일: 아래 메서드와 함께 초기화·설정 UI·언어 매핑·자원 해제 계약을 채우세요. 실제 플러그인으로 가져오려면 DLL과 메타데이터, 프로젝트 참조도 필요합니다.

완료 기준: 요청 원문을 바꾸지 않고 결과를 생성하며, 토큰이 취소되면 성공 결과를 쓰지 않습니다. 외부 API 키나 네트워크 연결은 필요하지 않습니다.

// 연습용 TranslateAsync 본문: 완성된 플러그인 전체 코드는 아닙니다.
public override async Task TranslateAsync(
    TranslateRequest request,
    TranslateResult result,
    CancellationToken cancellationToken = default)
{
    await Task.Delay(300, cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    result.Success($"[연습] {request.Text}");
}
과제 3 · 중급

늦은 결과가 최신 결과를 덮지 못하는지 검증하기

목표: 비동기 작업의 완료 순서와 화면 표시 순서를 분리합니다.

출발점: TranslationResultCoordinatorTests.cs의 NewerStreamingResultCannotBeOverwrittenByCancelledRequest()입니다.

할 일: A 요청은 중간 결과를 내고 멈춰 두고, B 요청을 시작해 완료한 뒤 A가 다시 값을 쓰게 하세요. TaskCompletionSource를 사용하면 임의의 긴 대기 없이 순서를 정할 수 있습니다.

완료 기준: A의 늦은 텍스트·실패 표시·처리 중 표시가 모두 B의 상태를 덮지 않습니다. 취소 토큰 없이도 작업 번호 검사 자체가 지키는 범위를 설명할 수 있습니다.

시간  ───────────────────────────────────────────────→
A     시작 ── 중간 결과 ─────────────── 늦은 갱신 시도
B                 시작 ── 최종 결과
화면  A 중간값 ─── 새 요청 초기화 ── B 최종값 유지
조건  A 번호 ≠ 현재 채널 번호 → 게시 거부
과제 4 · 중급

검색어를 SQL 매개변수로 전달하기

목표: 데이터와 SQL 문장 구조를 분리합니다.

출발점: SqlService.GetDataAsync(string content, CancellationToken? token)의 이력 검색 오버로드입니다.

할 일: 직접 삽입하던 검색어를 @Pattern 매개변수로 전달하고 기존 취소 토큰 경로를 유지하세요. %와 _를 검색 문법으로 허용할지, 문자 그대로 찾을지도 결정합니다.

완료 기준: 일반 검색, 빈 검색어, O'Reilly, 한글, 와일드카드의 기대 결과를 설명하고 검증합니다. 매개변수화와 와일드카드 이스케이프가 별개임을 구분합니다.

// 학습용 변경 방향: 원문 소스의 현재 구현과 구분하세요.
const string query =
    "SELECT * FROM History WHERE LOWER(SourceText) LIKE @Pattern";
var command = new CommandDefinition(
    query,
    new { Pattern = $"%{content.ToLowerInvariant()}%" },
    cancellationToken: token ?? CancellationToken.None);
return await connection.QueryAsync<HistoryModel>(command);
과제 5 · 고급

OCR 좌표와 번역 오버레이의 정확성 지키기

목표: 문자열뿐 아니라 위치·읽기 순서·표시 단위를 보존합니다.

출발점: IOcrPlugin, OcrLayoutAnalyzer, ImageTranslateTextOverlayLayout, ImageTranslateOverlayTests.cs입니다.

할 일: 두 열 문서와 번호 목록을 가진 모의 OCR 결과를 만들고, 축소·확대 표시에서 번역 영역이 원문 영역을 따라가는지 점검하세요. 빈 좌표와 실패한 번역 블록도 넣습니다.

완료 기준: 읽기 순서가 뒤섞이지 않고, 실패한 블록의 원문이 유지되며, 표시 배율을 바꿔도 영역이 일치합니다. SupportBoxPoints() == false인 엔진을 이미지 번역용으로 선택할 수 없는 이유도 설명합니다.

좋은 실습의 완료 기준은 코드가 늘어났는지가 아니라, 이전에는 설명하지 못했던 실패 상황을 이제 재현하고 막을 수 있는지입니다. 과제 1~2를 마친 뒤 나머지 중 하나만 깊게 진행해도 충분한 학습 결과를 만들 수 있습니다.

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

한 주에 한 가지 질문에 답하는 여섯 주 과정

아래는 정해진 학습 시간을 보장하는 일정표가 아니라, 앞의 개념이 다음 개념의 발판이 되도록 만든 순서입니다. C#이 처음이라면 1주 차를 늘리고, 이미 웹 비동기 코드를 다뤄 봤다면 3주 차에서 차이점을 집중해서 읽으세요.

주차배울 내용과 질문읽을 소스남길 결과물
1주 차C# 클래스·인터페이스·record·제네릭. 요청과 결과는 왜 다른 모양일까?IPlugin.cs, ITranslatePlugin.cs, IOcrPlugin.cs요청·결과·플러그인의 관계도 한 장
2주 차WPF·MVVM·바인딩·명령. 상태 변화가 화면까지 어떻게 갈까?MainWindow.xaml, MainWindowViewModel.cs, OutputControl.cs결과 텍스트와 처리 중 표시의 연결표
3주 차Task·취소·스트리밍·임계 구역. 늦은 결과는 누가 거절할까?DebounceExecutor.cs, TranslationOperation.cs, TranslationResultCoordinator.cs두 요청의 시간표와 재현 시나리오
4주 차DI·플러그인 로딩·서비스 수명. 새 엔진을 어떻게 교체할까?App.xaml.cs, PluginAssemblyLoader.cs, ServiceManager.cs모의 번역 플러그인의 계약 점검표
5주 차SQLite·JSON·캐시. 무엇을 같은 요청으로 볼까?SqlService.cs, StorageBase.cs, PluginStorage.cs캐시 키와 설정 변경 시 무효화 정책 초안
6주 차OCR 좌표·국제화·배포. 내 환경 밖에서도 맞게 보일까?ImageTranslateWindowViewModel.cs, BidiDirectionHelper.cs, CI 파일두 열 이미지·오른쪽에서 왼쪽으로 쓰는 언어·배포 조건의 점검 기록

주마다 같은 방식으로 정리하기

  1. 입력 한 개를 정합니다. 문장, OCR 결과, 설정 파일처럼 시작 데이터를 구체적으로 고릅니다.
  2. 세 파일만 먼저 읽습니다. 계약·구현·호출자 순서로 연결하고, 모르는 함수가 나올 때만 범위를 넓힙니다.
  3. 실패 한 가지를 추가합니다. 빈 값, 네트워크 오류, 취소, 오래된 결과, 누락된 좌표 중 하나를 고릅니다.
  4. 기대 결과를 문장으로 씁니다. 그 문장을 기존 테스트와 대조하거나 실습의 완료 조건으로 사용합니다.

한 저장소를 깊게 읽는다는 것은 모든 파일을 읽는 일이 아니라, 하나의 동작을 계약·구현·실패·검증까지 연결해 설명하는 일입니다. 이것이 익숙해지면 다음 저장소에서도 기술 이름보다 책임과 데이터 흐름이 먼저 보입니다.

다음 공부 주제 선택하기

앱 개발에 관심이 있으면 WPF와 창 수명을, 서비스 연동에 관심이 있으면 HTTP 파서와 플러그인 계약을, 문서 처리에 관심이 있으면 OCR 레이아웃과 좌표를 더 파고드세요. 모두 같은 프로젝트의 서로 다른 입구입니다.

10핵심 키워드 사전

이름보다, 이 앱에서 하는 일을 기억하세요
용어쉬운 설명
WPFWindows 창과 컨트롤을 만드는 .NET 화면 기술. 이 앱의 메인 창·설정 창을 구성합니다.
XAML화면의 배치와 연결을 선언하는 마크업. C#이 처리할 화면 구조를 기술합니다.
MVVM화면, 화면 상태와 명령, 데이터를 나눠 관리하는 방식. 창 코드에 모든 기능이 몰리는 것을 줄입니다.
바인딩컨트롤의 값과 객체의 속성을 연결하는 장치. 창 위치나 결과 문자열에 쓰입니다.
ObservableObject속성 값이 바뀌었다는 알림을 보낼 수 있는 기반 클래스. 결과 표시를 갱신하는 연결점입니다.
소스 생성기컴파일 때 반복 코드를 만들어 주는 도구. 특성으로 표시한 속성과 명령을 생성합니다.
의존성 주입 · DI객체가 필요한 협력자를 직접 만들기보다 밖에서 받는 방식. 앱 시작 부분에서 조립 관계를 정합니다.
Singleton / Transient서비스 제공자가 같은 인스턴스를 공유할지, 요청마다 새로 만들지 정하는 수명 정책입니다.
플러그인 계약기능을 바꿔 끼우기 위해 서로 약속한 호출 형식. ITranslatePlugin이 대표적입니다.
PluginContext플러그인이 HTTP·로그·설정 저장 같은 앱 기능을 이용하는 창구입니다.
서비스 인스턴스플러그인에 사용자 설정과 고유 ID를 결합한 실행 단위. 같은 플러그인으로 여러 개를 만들 수 있습니다.
AssemblyLoadContext.NET DLL과 그 의존성을 로드하는 문맥. 자체적으로 위험한 코드를 격리하는 보안 경계는 아닙니다.
async / await작업 완료를 기다린 뒤 다음 처리를 이어 쓰는 방법. 그 자체가 별도 스레드를 뜻하지는 않습니다.
CancellationToken진행 중인 작업에 중단 요청을 전달하는 신호. 호출받은 쪽도 이를 확인해야 효과가 있습니다.
디바운스입력이 연달아 바뀔 때 잠시 기다린 뒤 마지막 입력에 대해 동작하는 방식입니다.
작업 스냅샷요청을 시작할 때의 문장·언어 등을 붙잡아 둔 값. 실행 도중 화면 입력이 바뀌어도 요청의 의미를 보존합니다.
최신성 검사결과의 작업 번호가 현재 채널의 번호와 같은지 확인하는 것. 오래된 결과의 덮어쓰기를 막습니다.
임계 구역 · lock공유 상태를 검사하고 바꾸는 동안 다른 작업이 끼어들지 못하게 보호하는 구간입니다.
SemaphoreSlim동시에 들어갈 수 있는 작업 수를 제한하는 문지기. 요청 개수를 시간당으로 제한하는 기능과는 다릅니다.
DispatcherWPF UI 스레드에 일을 전달하는 창구. 화면 객체를 적절한 실행 문맥에서 다루도록 돕습니다.
스트리밍 · 델타응답을 조금씩 받는 방식과 그때마다 도착하는 추가 텍스트. 플러그인이 델타를 누적해 표시합니다.
SQLite / Dapper파일에 저장하는 DB와 SQL 결과를 C# 객체에 연결하는 도구. 이 앱에서는 번역 이력을 관리합니다.
캐시 무효화이전 결과를 더 이상 재사용하지 않기로 정하는 것. 모델·프롬프트·언어 변경과 연결해 생각해야 합니다.
OCR 바운딩 박스인식한 글자가 이미지의 어디에 있는지 나타내는 영역. 번역문을 같은 위치에 놓는 데 필요합니다.
논리 좌표 / 픽셀화면 배율을 고려한 표시 단위와 이미지의 실제 점 단위. 좌표를 섞으면 위치가 어긋날 수 있습니다.
RTL오른쪽에서 왼쪽으로 읽는 글자 방향. 언어별 문자열뿐 아니라 입력·출력의 방향도 맞춰야 합니다.
원자적 파일 교체새 내용을 임시 파일에 준비한 뒤 원본을 교체하는 저장 방식. 쓰기 도중 손상 위험을 줄이려는 설계입니다.
Velopack / .spkg앱 배포·업데이트 도구와 플러그인 묶음 형식. 이 저장소의 .spkg는 ZIP으로 포장합니다.
커밋Git에서 특정 시점의 파일 상태를 식별하는 단위. 이 글의 코드 기준은 75f616a입니다.

11참고 링크

다시 확인할 수 있는 근거와 분석의 경계

공식 프로젝트와 사용 안내

소스 코드 다시 읽기

확인할 질문커밋 75f616a의 소스
앱은 어디서 조립되는가?src/STranslate/App.xaml.cs
입력에서 번역으로 어떻게 가는가?src/STranslate/ViewModels/MainWindowViewModel.cs
왜 오래된 응답이 거절되는가?src/STranslate/Core/TranslationResultCoordinator.cs · TranslationOperation.cs
플러그인은 어떤 계약을 지키는가?src/STranslate.Plugin/ITranslatePlugin.cs · IOcrPlugin.cs
로딩과 서비스 설정의 차이는?PluginManager.cs · ServiceManager.cs
외부 응답 형식을 어디서 맞추는가?src/Plugins/STranslate.Plugin.Translate.OpenAI/OpenAIProtocol.cs
이미지에서 번역문 위치까지의 흐름은?src/STranslate/ViewModels/ImageTranslateWindowViewModel.cs
캐시와 저장의 경계는?SqlService.cs · StorageBase.cs
기대하는 동작은 어떻게 표현했는가?src/Tests/STranslate.Tests/
의존성과 배포 값은 어디에 있는가?Directory.Packages.props · .github/workflows/dotnet.yml

관련 기술 공식 자료

프로젝트 현황 (2026-10-23 GitHub 기준)

항목확인 내용
프로젝트STranslate/STranslate — WPF 기반 번역·OCR 도구
선정 기준GitHub Trending C# Daily #6 · 2026-10-23
스타 / 포크8,362 / 438 · 2026-10-23 GitHub 기준
생성일2022-12-17
주 언어C#
최신 릴리스v2.0.10 · 2026-09-04
최근 push2026-09-24
분석 기준 커밋75f616a · 2026-09-24 · 창 표시 애니메이션의 튕김 제거
코드 규모C# 342개 파일·52,834줄 / XAML 260개 파일·23,425줄 / PowerShell 231줄
프로젝트 구성본체 1개 + SDK 1개 + 플러그인 21개 + 테스트 1개 = .csproj 24개
테스트 소스C# 29개 파일·5,844줄. 실행 결과나 테스트 케이스 총수와는 구별
의존성 기준.NET 10 타깃 / CommunityToolkit.Mvvm 8.4.2 / SDK 패키지 1.0.15
라이선스MIT · 2026-10-23 GitHub 기준

GitHub 수치는 2026-10-23 시점의 상태이고, 코드 수치와 함수 설명은 75f616a의 파일 기준입니다. 라이브 순위 페이지와 기본 브랜치가 나중에 달라져도 이 글의 기준 시점은 바뀌지 않습니다.

줄 수를 같은 기준으로 다시 계산하는 방법

저장소 루트에서 확장자를 골라 물리적인 줄을 셉니다. 아래 예시는 프로그램을 실행하지 않고 파일 바이트만 읽으며, Git 내부 파일을 제외합니다. 빈 줄과 주석도 포함하므로 실행 가능한 코드만 센 수치가 아닙니다.

python3 - <<'PY'
from pathlib import Path
for extension in ('.cs', '.xaml', '.ps1'):
    files = [p for p in Path('.').rglob('*' + extension)
             if p.is_file() and '.git' not in p.parts]
    lines = sum(len(p.read_bytes().splitlines()) for p in files)
    print(extension, len(files), lines)
PY
쓰기 전에 알아둘 한계
소스의 설계와 배포본의 실제 품질은 구별해서 판단해야 합니다

이 글의 코드 설명은 커밋 75f616a를 기준으로 합니다. 앱 실행 성능, OCR 정확도, 서비스 가용성, 배포본 설치·호환성, 테스트 통과 여부는 별도 검증이 필요합니다. 한 시점의 코드와 순위만으로 장기 유지보수 추세나 트렌딩의 직접 원인을 확정할 수도 없습니다.

최신 릴리스 날짜와 분석 커밋 날짜는 다릅니다. 분석 커밋에서 보이는 .NET 타깃·Responses 지원·버그 수정이 모두 v2.0.10 배포 파일에 그대로 들어 있다고 단정하지 마세요. 릴리스 태그의 소스와 별도로 대조해야 합니다.

외부 API를 쓰면 원문이나 이미지가 해당 서비스로 전송될 수 있습니다. StorageBase는 설정을 JSON으로 직렬화하며 이 저장 경로에서 암호화를 적용하지 않습니다. 또한 HttpService에는 요청·응답 본문을 Trace 수준으로 기록하는 코드가 있습니다. Settings.LogLevel의 기본값은 Information이고 초기화 때 적용되므로 본문이 항상 기록된다고 단정할 수는 없습니다. 상세 로그를 켜면 원문이 포함될 수 있으므로 API 키가 든 설정과 함께 공유 범위를 확인해야 합니다.

플러그인 DLL은 앱과 같은 프로세스에서 실행됩니다. API 비용·지원 언어·엔진 의존성도 플러그인마다 다릅니다. 이 글의 구조 설명만으로 모든 서비스의 현재 정책이나 안전성을 대신 판단할 수는 없습니다.