← 기술 블로그 목록으로 돌아가기
#974 • Text Algorithms & UI Viewport Synchronization

교안과 소스코드의 1줄 차이를 0.1초 만에 규명: ZeliDiff의 비동기 Myers Diff 엔진과 양방향 가상화 스크롤 동기화 아키텍처

"중간고사 시험 문제의 최종 수정본과 초안, 어디가 바뀌었는지 눈으로 대조하다가 오탈자를 놓치셨나요?" 수만 줄의 시험 문제·수업 교안·실습 소스코드 비교 시 UI 디스패처 프리징을 원천 차단하는 백그라운드 태스크 Myers Diff 알고리즘, 연속 입력 폭주를 방어하는 _isComparing & _pendingCompare 펜딩 루프, 좌우 높이 불일치를 극복하는 Imaginary 유령 행 가상화 정렬, 원클릭 단방향 원자적 델타 머지 백서.

"초·중·고 시험 출제 기간, 교사들은 국어 지문, 수학 서술형 배점표, 영어 보기 지문의 최종본과 이전 검토본을 일일이 육안으로 대조합니다. '이 문항의 3번 보기를 수정한 것이 맞나?', '어느 교시 파일이 최종 결재본인가?'를 확인하기 위해 무료 인터넷 텍스트 비교 사이트에 시험 문제를 복사해 붙여넣는 순간, 보안 시험지 데이터는 외부 상용 서버로 유출됩니다. ZeliDiff는 폐쇄망 내부에서 수만 줄의 문서를 0.1초 만에 Myers Diff 알고리즘으로 분석하고, 1글자의 차이까지 색상으로 완벽히 분리해 냅니다."

1. 시험 출제와 교안 개정의 악몽: "1글자 차이로 문항 오류가 갈리는 교단 현장"

학교 현장의 정기 고사 출제나 소프트웨어 실습 수업에서는 두 텍스트의 미세한 차이를 비교(Diff)해야 하는 상황이 일상적으로 발생합니다:

2. O(ND) 복잡도의 Myers Diff 엔진과 비동기 백그라운드 태스크 격리

두 텍스트 사이의 차이점을 찾는 고전 알고리즘인 LCS(Longest Common Subsequence)는 $O(M \times N)$의 연산량을 가집니다. 수천 줄의 문서에서는 연산량이 기하급수적으로 폭증하여 UI 스레드를 수 초간 얼려버립니다.

ZeliDesk의 ZeliDiff는 Git의 표준 알고리즘인 Myers Diff 알고리즘 (DiffPlex 엔진)을 채택하여, 차이점(Edit Distance $D$)에 비례하는 $O(N \cdot D)$의 압도적인 속도로 양방향 텍스트를 대조합니다:

[ZeliDiff의 비동기 Myers Diff 및 Side-by-Side 뷰포트 파이프라인]
graph LR subgraph InputState["텍스트 입력 레이어"] TextA["원본 문서 (TxtLeft)
시험 초안 / 기존 코드"] TextB["비교 문서 (TxtRight)
최종 수정본 / 학생 과제"] end subgraph BgWorker["비동기 백그라운드 태스크 (Task.Run)"] direction TB Differ["Myers Differ Engine
O(N·D) 최소 편집 거리"] Builder["SideBySideDiffBuilder
공백/대소문자 정규화 필터"] LineAlign["ChangeType 분류
Equal / Added / Removed
Imaginary(유령 행 삽입)"] Differ --> Builder --> LineAlign end subgraph UIThread["WinUI 3 가상화 렌더링"] ListL["ListViewLeft
(단방향 복사 툴팁 바인딩)"] ListR["ListViewRight
(원클릭 SyncLine 패치)"] end InputState --> BgWorker LineAlign --> UIThread

특히 UI 디스패처 스레드가 블로킹되는 것을 원천 차단하기 위해, 전체 텍스트 토큰화 및 모델 빌드 과정을 Task.Run 백그라운드 스레드로 완전 격리했습니다:

C# (ZeliDiffWindow.xaml.cs - 비동기 Myers Diff 파이프라인)
// 무거운 diff 계산은 백그라운드 스레드에서 전담하여 UI 프리징 0% 달성
var (leftLines, rightLines, added, removed, lineCount) = await Task.Run(() =>
{
    var differ = new DiffPlex.Differ();
    var diffBuilder = new DiffPlex.DiffBuilder.SideBySideDiffBuilder(differ);
    
    // 사용자가 선택한 공백 무시(ignoreWhitespace) 및 대소문자 무시(ignoreCase) 옵션 적용
    var result = diffBuilder.BuildDiffModel(leftText, rightText, 
        ignoreWhitespace: ignoreWhiteSpace, ignoreCase: ignoreCase);

    var left = new List<DiffLine>(result.OldText.Lines.Count);
    var right = new List<DiffLine>(result.NewText.Lines.Count);

    for (int i = 0; i < result.OldText.Lines.Count; i++)
    {
        var lPiece = result.OldText.Lines[i];
        var rPiece = result.NewText.Lines[i];
        // ... DiffLine 모델 생성 및 행 상태(Equal/Removed/Added/Empty) 매핑 ...
    }
    return (left, right, addedCount, removedCount, result.OldText.Lines.Count);
});

3. 실시간 타이핑 중 스레드 재진입 방지: _isComparing & _pendingCompare 펜딩 루프

사용자가 텍스트를 빠르게 편집하거나 붙여넣기를 연타할 때, 비동기 메서드가 무분별하게 병렬 호출되면 스레드 경합(Race Condition)과 메모리 낭비가 발생합니다.

ZeliDiff는 불필요한 뮤텍스 락이나 세마포어 없이, 가벼운 두 개의 불리언 플래그를 활용한 펜딩 루프(Pending Loop) 아키텍처를 구현했습니다:

C# (DoCompareAsync - 재진입 방지 및 최신 상태 보장 루프)
private bool _isComparing;
private bool _pendingCompare; // 진행 중 들어온 최신 요청 보류

private async Task DoCompareAsync()
{
    // 이미 계산 중이면 다음 턴에 실행하도록 플래그만 세우고 즉시 반환
    if (_isComparing) { _pendingCompare = true; return; }
    _isComparing = true;
    try
    {
        do
        {
            _pendingCompare = false;
            
            // UI 스레드에서 최신 텍스트 캡처 (Task.Run 내부에서는 UI 요소 접근 불가)
            var leftText = TxtLeft.Text ?? "";
            var rightText = TxtRight.Text ?? "";

            // 비동기 계산 수행 및 UI 컬렉션 갱신
            // ... (await Task.Run) ...

        } while (_pendingCompare); // 진행 중 들어온 최신 요청이 있다면 완료 후 즉시 1회 더 실행
    }
    finally
    {
        _isComparing = false;
    }
}

이 패턴은 사용자가 타이핑하는 동안 이전 계산을 안전하게 완료하면서도, 타이핑이 끝나는 즉시 가장 최신의 텍스트 상태를 정확히 반영하여 UI와 데이터가 어긋나는 현상을 완벽히 방어합니다.

4. 높이 불일치를 극복하는 ChangeType.Imaginary 유령 행(Phantom Line) 가상화 정렬

텍스트를 좌우(Side-by-Side)로 비교할 때 가장 까다로운 점은 비대칭 변경(Asymmetric Chunk)입니다.

"문서 B에만 새로운 문단 5줄이 추가되었다면 어떻게 표시해야 할까요?
문서 A의 다음 줄이 그대로 올라와 붙어버리면, 좌우의 서로 다른 내용이 같은 높이에 나란히 놓여 비교가 불가능해집니다."

ZeliDiff는 이를 ChangeType.Imaginary라는 유령 행(Phantom Line)으로 해결합니다. 한쪽에만 존재하는 추가/삭제 블록이 있을 때, 반대편 패널에 내용이 비어 있는 Status = "Empty" 가상 행을 삽입하여 좌우의 행 번호와 수직 높이를 1:1로 물리적 일치시킵니다:

C# (ApplyBrushes - 유령 행 및 시각적 브러시 동기화)
private void ApplyBrushes(DiffLine dl, bool isLeft)
{
    switch (dl.Status)
    {
        case "Equal": // 변경 없는 행: 투명 배경, 기본 텍스트
            dl.BackgroundBrush = _brushEqualBg; dl.ForegroundBrush = _brushEqualFg;
            break;
        case "Removed": // 삭제된 행: 소프트 레드 배경 (#ef5350)
            dl.BackgroundBrush = _brushRemovedBg; dl.ForegroundBrush = _brushRemovedFg;
            break;
        case "Added": // 추가된 행: 소프트 에메랄드 그린 배경 (#66bb6a)
            dl.BackgroundBrush = _brushAddedBg; dl.ForegroundBrush = _brushAddedFg;
            break;
        case "Empty": // 유령 행: 반대편의 변경 블록 높이와 정확히 맞물리도록 대칭 배경 처리
            dl.BackgroundBrush = isLeft ? _brushAddedBg : _brushRemovedBg;
            dl.ForegroundBrush = _brushEqualFg;
            break;
    }
}

5. 단 1회의 클릭으로 줄 단위 원자적 머지: SyncLine과 상태 전이(State Transition)

차이점을 발견한 후 교사가 일일이 복사해서 붙여넣을 필요가 없습니다. ZeliDiff는 단 한 번의 마우스 클릭으로 반대편 패널에 변경 사항을 즉각 이식하는 SyncLine 엔진을 내장하고 있습니다:

C# (SyncLine - 원클릭 단방향 동기화)
private async Task SyncLine(int sourceIndex, bool leftToRight)
{
    // 1. 클릭된 행의 텍스트 추출
    string textToCopy = leftToRight ? _diffLeft[sourceIndex].Text : _diffRight[sourceIndex].Text;

    // 2. 대상 컬렉션의 대응 위치에 텍스트 주입
    if (leftToRight) _diffRight[sourceIndex].Text = textToCopy;
    else _diffLeft[sourceIndex].Text = textToCopy;

    // 3. 유령 행(Empty)을 제외한 실제 유효 텍스트만 필터링하여 원본 텍스트박스 복원
    var newLeft = _diffLeft.Where(d => d.Status != "Empty" || (!leftToRight && sourceIndex == d.SourceIndex))
                           .Select(d => d.Text).ToList();
    var newRight = _diffRight.Where(d => d.Status != "Empty" || (leftToRight && sourceIndex == d.SourceIndex))
                             .Select(d => d.Text).ToList();

    TxtLeft.Text = string.Join("\r\n", newLeft);
    TxtRight.Text = string.Join("\r\n", newRight);

    // 4. 즉시 재비교하여 동기화 완료 상태("Equal")로 뷰포트 갱신
    await DoCompareAsync();
}

빨간색(삭제)이나 초록색(추가)으로 강조된 행을 클릭하기만 하면, 반대편으로 텍스트가 즉시 병합되며 색상이 회색("Equal")으로 바뀌어 병합 완료를 시각적으로 즉시 인지할 수 있습니다.

6. 기존 웹 Diff 사이트 vs WinMerge vs ZeliDesk ZeliDiff 심층 비교

비교 항목 온라인 텍스트 비교 웹사이트 WinMerge (설치형 전통 도구) ZeliDesk ZeliDiff (엔지니어링 백서)
폐쇄망(Air-Gap) 보안 불가 (클라우드 서버에 텍스트 유출) 가능 100% 온디바이스 완전 격리 (네트워크 0바이트)
UI 모던 디자인 광고 배너 난립, 느린 반응 속도 구형 Win32 MFC 디자인, 복잡한 메뉴 WinUI 3 Fluent 디자인 & 다크 오로라 테마
줄 단위 원클릭 병합 지원 안 함 (단순 눈 대조 전용) 화살표 버튼을 정확히 클릭해야 함 행 자체를 클릭하는 직관적 SyncLine 머지
대용량 비동기 렌더링 브라우저 탭 멈춤 현상 발생 C++ 고속 구동 Task.Run 백그라운드 격리 & 펜딩 루프 방어
공백·대소문자 필터 유료 옵션이거나 부분 지원 세부 설정 필요 체크박스 원클릭 즉시 실시간 재정규화

7. 결론: "외부 유출 없는 100% 온디바이스에서 실현되는 무결점 문서 거버넌스"

단 한 줄의 문장, 단 하나의 숫자가 만드는 차이가 학생들의 성적을 바꾸고 행정의 신뢰를 좌우합니다.

ZeliDesk의 ZeliDiff는 중요한 공문서와 교육 자료를 외부 서버에 맡기는 타협을 용납하지 않습니다. 검증된 Myers Diff 알고리즘과 비동기 펜딩 루프, 그리고 직관적인 원클릭 델타 머지를 통해 교사와 실무자가 가장 안전하고 신속하게 문서의 무결성을 수호할 수 있도록 뒷받침합니다.

← 이전 기술 백서 #975 폐쇄망 학교와 군부대를 위한 오프라인 QR 코드 거버넌스: ZeliQR의 QRCoder 인프로세스 렌더링과 Wi-Fi/vCard 무선 자동 연결 아키텍처 다음 기술 백서 → #973 컴퓨터실 40대 PC를 교무실에서 1초 만에 동시 부팅: ZeliWake의 매직 패킷(Magic Packet) 브로드캐스트와 로컬 절전 거버넌스 아키텍처
⚖️ ZeliDesk ZeliDiff 비동기 텍스트 비교기 체험하기
시험 문제, 수업 교안, 프로그래밍 과제의 미세한 1글자 차이까지 0.1초 만에 안전하게 규명하고 원클릭으로 동기화하세요.
72종 유틸리티 전체 둘러보기 →