본문 바로가기
메이킹 로그와 트러블슈팅

[텍스트 차이점 비교기 메이킹 로그 #02] GitHub 스타일의 Diff Checker 만들기: 추가(+), 삭제(-) 색상 하이라이팅과 나란히 보기(Side-by-Side) UI/UX 구현 로직

by 코드메이트 2026. 4. 25.

안녕하세요! 코드브릭스(Code-Bricks)를 만들어가고 있는 프론트엔드 개발자입니다.

지난 [메이킹 로그 1편]에서는 텍스트 차이점 분석기(Diff Checker)의 핵심 두뇌인 'LCS 알고리즘'과 클라이언트 사이드 아키텍처의 기획 배경에 대해 깊이 있게 다뤄보았는데요. 다들 재미있게 읽어보셨나요?

오늘은 그 두 번째 시간으로, 무거운 알고리즘의 연산 결과를 사용자의 눈에 어떻게 예쁘고 직관적으로 보여줄 것인가에 대한 'UI/UX 설계와 프론트엔드 렌더링 로직'을 파헤쳐보려고 합니다. (※ 현재 열심히 마무리 작업 중이며, 실제 동작하는 차이점 분석기 사이트 링크는 추후 본문 하단에 연결해 둘 예정이니 많은 기대 부탁드립니다!)

개발자들이 매일 보는 GitHub 스타일의 깔끔한 추가(+), 삭제(-) 색상 하이라이팅과 나란히 보기(Side-by-Side) 레이아웃을 프론트엔드 코드로 어떻게 구현했는지, 지금부터 그 비밀을 하나씩 공개하겠습니다!

 

1. GitHub 스타일 UI/UX: 왜 '나란히 보기(Side-by-Side)'인가?

텍스트의 차이점을 보여주는 UI(User Interface) 방식은 크게 두 가지로 나뉩니다. 하나는 원본과 수정본을 한 줄씩 번갈아 가며 보여주는 인라인(Inline) 방식이고, 다른 하나는 화면을 좌우로 정확히 반으로 갈라 왼쪽에는 원본을, 오른쪽에는 수정본을 배치하는 나란히 보기(Side-by-Side) 방식입니다.

인라인 방식은 모바일처럼 화면이 좁은 곳에서는 유리하지만, 긴 계약서나 수백 줄의 코드를 읽을 때는 문맥의 흐름이 끊겨 가독성이 크게 떨어집니다. 반면, Side-by-Side 방식은 사용자가 왼쪽과 오른쪽을 번갈아 보며 "아, 이 문단이 통째로 날아가고 새로운 문단으로 대체되었구나"를 한눈에 직관적으로 파악할 수 있죠.

그래서 코드브릭스의 Diff Checker는 데스크톱 환경의 생산성을 극대화하기 위해 과감하게 Side-by-Side 레이아웃을 메인 UI로 채택했습니다.

 

차이점 분석기 Side-by-Side 나란히 보기 UI GitHub 스타일 코드 리뷰 텍스트 비교

 

2. 프론트엔드의 숨은 난제: 양쪽 스크롤 동기화(Sync) 로직

Side-by-Side 레이아웃을 짤 때 프론트엔드 개발자를 가장 괴롭히는(?) 녀석이 바로 '스크롤 동기화'입니다. 왼쪽 창(원본)의 스크롤을 휠로 주르륵 내렸는데 오른쪽 창(수정본)은 가만히 멈춰있다면? 사용자는 길을 잃고 엄청난 피로감을 느끼게 됩니다. 양쪽 창이 마치 하나의 창처럼 동시에 부드럽게 스크롤되도록 만들어야 하죠.

로직은 생각보다 간단합니다. 왼쪽 컨테이너에 scroll 이벤트 리스너를 달아서, 왼쪽이 스크롤될 때마다 그 위치(scrollTop)를 읽어와 오른쪽 컨테이너의 스크롤 위치에 강제로 대입해 주는 것입니다. 반대의 경우도 마찬가지고요.

// 스크롤 동기화를 위한 Vanilla JS 핵심 로직 구현 예시
const leftPanel = document.getElementById('left-panel');
const rightPanel = document.getElementById('right-panel');

let isSyncingLeft = false;
let isSyncingRight = false;

leftPanel.addEventListener('scroll', function(e) {
    if (!isSyncingLeft) {
        isSyncingRight = true; // 오른쪽 스크롤 이벤트가 연쇄적으로 발생하는 것을 방지
        rightPanel.scrollTop = this.scrollTop;
        rightPanel.scrollLeft = this.scrollLeft;
    }
    isSyncingLeft = false;
});

rightPanel.addEventListener('scroll', function(e) {
    if (!isSyncingRight) {
        isSyncingLeft = true;
        leftPanel.scrollTop = this.scrollTop;
        leftPanel.scrollLeft = this.scrollLeft;
    }
    isSyncingRight = false;
});

여기서 주의할 엣지 케이스는 '무한 루프(Infinite Loop)' 방지입니다. 왼쪽 스크롤이 오른쪽을 움직이고, 움직여진 오른쪽이 다시 왼쪽 스크롤 이벤트를 트리거하는 핑퐁 현상이 발생할 수 있습니다. 이를 막기 위해 위 코드처럼 플래그(Flag) 변수(isSyncingLeft, isSyncingRight)를 활용하여 이벤트의 연쇄 폭발을 막아주는 것이 핵심 포인트입니다.

 

3. 추가(+)와 삭제(-) 색상 하이라이팅: DOM 조작 로직

이제 가장 중요한, 텍스트에 색깔 옷을 입히는 과정을 살펴볼까요? 지난 편에서 다뤘던 LCS 알고리즘(또는 npm의 diff 라이브러리 활용)의 연산이 끝나면, 결과물은 보통 다음과 같은 형태의 배열 데이터(상태)로 반환됩니다.

  • 0 (Unchanged): 변경되지 않은 줄
  • -1 (Removed): 원본에서 삭제된 줄
  • 1 (Added): 수정본에 추가된 줄

이 상태(State) 데이터를 바탕으로 화면에 HTML 태그(DOM)를 동적으로 그려내야 합니다. GitHub의 글로벌 표준 UI를 차용하여, 삭제된 줄은 붉은색 배경에 '-' 기호를, 추가된 줄은 녹색 배경에 '+' 기호를 붙여주도록 컴포넌트를 설계했습니다.

빈 줄을 맞추는 것도 매우 중요합니다. 왼쪽에 삭제된 줄이 있다면, 오른쪽 컨테이너의 동일한 라인에는 내용이 없는 '빈 블록(Placeholder)'을 그려주어야 양쪽 문맥의 높낮이가 틀어지지 않고 정확하게 일치하게 됩니다.

 

 

스크롤 동기화 자바스크립트 이벤트 리스너 프론트엔드 DOM 조작 무한루프 방지

 

 

 

 

 

 

 

4. 상태(State)를 HTML로! 동적 렌더링 코드 구현

실제 프론트엔드 프레임워크(React 기반)에서 이 데이터를 받아 동적으로 리스트를 렌더링하는 로직을 살짝 공개해 드립니다.

// React를 활용한 Diff 결과물 렌더링 컴포넌트 예시
const DiffViewer = ({ diffResults }) => {
  return (
    <div className="diff-container side-by-side">
      {/* 왼쪽: 원본 텍스트 패널 */}
      <div className="panel left-panel" id="left-panel">
        {diffResults.map((line, index) => {
          if (line.type === 'added') {
            // 왼쪽에 추가된 부분이 있다면 오른쪽과 높이를 맞추기 위해 빈 줄 렌더링
            return <div key={index} className="line empty-placeholder"></div>;
          }
          return (
            <div key={index} className={`line ${line.type === 'removed' ? 'line-removed' : ''}`}>
              <span className="line-number">{line.oldLineNumber}</span>
              <span className="line-sign">{line.type === 'removed' ? '-' : ' '}</span>
              <span className="line-content">{line.value}</span>
            </div>
          );
        })}
      </div>

      {/* 오른쪽: 수정본 텍스트 패널 */}
      <div className="panel right-panel" id="right-panel">
        {diffResults.map((line, index) => {
          if (line.type === 'removed') {
            // 오른쪽에 삭제된 부분이 있다면 빈 줄 렌더링
            return <div key={index} className="line empty-placeholder"></div>;
          }
          return (
            <div key={index} className={`line ${line.type === 'added' ? 'line-added' : ''}`}>
              <span className="line-number">{line.newLineNumber}</span>
              <span className="line-sign">{line.type === 'added' ? '+' : ' '}</span>
              <span className="line-content">{line.value}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
};

CSS에서는 .line-removed 클래스에 background-color: #ffebe9; color: #cf222e; (연한 붉은색)를 주고, .line-added 클래스에 background-color: #e6ffec; color: #1a7f37; (연한 녹색)를 적용하여 시각적인 대비를 극대화해 주었습니다.

이렇게 데이터의 상태(Type)에 따라 클래스명만 조건부로 렌더링해 주면, 아무리 복잡하게 꼬인 텍스트라도 단 1초 만에 깔끔한 색상으로 차이점을 뱉어내는 훌륭한 UI가 완성됩니다.

 

5. 다음 편 예고: 브라우저 프리징과의 사투

여기까지 구현하고 테스트를 해보며 저는 "오! 완벽해!"라고 쾌재를 불렀습니다. 하지만 기쁨도 잠시, 1만 줄이 넘어가는 방대한 로그 파일을 복사해서 붙여넣는 순간, 화면이 하얗게 멈춰버리는(Freezing) 끔찍한 현상을 마주하게 됩니다.

자바스크립트의 싱글 스레드(Single Thread) 한계로 인해 무거운 연산이 렌더링을 꽉 막아버린 것이죠. 과연 저는 이 대용량 텍스트 렌더링의 늪에서 어떻게 빠져나왔을까요?

다음 포스팅인 [트러블슈팅 1편: 대용량 텍스트 연산 브라우저 멈춤 현상(Freezing) 해결기]에서 Web Worker와 가상 스크롤(Virtual Scroll)이라는 강력한 무기를 들고 다시 찾아오겠습니다.

차이점 분석기 완성본 사이트 링크도 조만간 이곳에 업데이트될 예정이니, 제 블로그 즐겨찾기 해두시고 꼭 다시 방문해 주세요! 오늘도 버그 없는 행복한 코딩 하시길 응원합니다.