[WebTranslator 개발기 #06] 선택 영역 번역 도입과 "내용 없음" 오탐 버그 해결
지난 5편에서는 스팀(Steam)의 작은 액션 버튼 내부에서 번역문이 거대하게 팽창하던 렌더링 붕괴를 해결하기 위해 25자 기준 인라인/블록 듀얼 렌더러와 비파괴적(Non-destructive) DOM 삽입 구조를 완성했습니다.
단축키(Alt+A)를 통한 페이지 전체 번역이 완성도 높은 안정성을 갖추게 되자, 자연스럽게 다음 사용자 경험(UX)에 대한 요구가 발생했습니다. 웹 서핑 중 항상 페이지 전체를 번역할 필요는 없으며, 긴 기술 문서의 특정 문단이나 생소한 단어 하나만 마우스로 드래그하여 가볍게 확인하고 싶을 때가 많기 때문입니다.
이에 마우스 드래그 기반의 '선택 영역 번역(Selection Translation)' 기능을 새롭게 설계하여 도입했습니다. 그러나 실전 테스트 중 멀쩡한 영문 문장을 선택했음에도 "번역할 내용이 없습니다"라는 엉뚱한 경고창(alert)이 뜨며 번역이 거부되거나 긴 문단이 한 줄로 찌그러지는 치명적인 파서 결함이 발생했습니다.
이번 글에서는 선택 영역 번역 기능의 초기 설계와 함께, 부모 DOM 오염으로 인해 발생하던 오탐(False Alarm)을 순수 문자열 판정 및 50자 기준 문단 팝업 분기로 해결한 과정을 정리해 보고자 합니다.
1. 선택 영역 번역 기능의 도입 배경과 초기 설계
전체 번역과 선택 영역 번역은 사용자 인터랙션과 기대 동작이 완전히 다릅니다.
| 구분 | 페이지 전체 번역 (`Alt+A`) | 선택 영역 번역 (마우스 드래그) |
|---|---|---|
| 트리거 방식 | 단축키 또는 확장 프로그램 아이콘 클릭 | 마우스 드래그 완료 시 (`mouseup` 이벤트) |
| 번역 범위 | 화면 내 모든 텍스트 노드 순회 수집 | 사용자가 선택한 특정 Selection Range 영역만 |
| 표출 형태 | 원문 인라인/블록 직속 삽입 | 선택 좌표 근처의 플로팅 툴팁 / 카드 팝업 |
초기 설계에서는 웹 표준 window.getSelection() API를 활용하여 마우스 드래그가 끝나는 시점에 텍스트를 추출하고 백그라운드로 전달하도록 구성했습니다.
2. 실전 테스트에서 발생한 2대 결함
기능을 구현한 뒤 깃허브 이슈 페이지와 스팀 커뮤니티에서 검증하던 중 2가지 심각한 문제가 드러났습니다.
- 억울한 "내용 없음" 경고창 오탐: 분명히 영문 본문을 정상적으로 드래그했음에도 브라우저 기본 경고창으로
"번역이 이미 되어있거나 번역할 내용이 없습니다."가 발생하며 번역이 씹히는 현상. - 긴 문단의 인라인 찌그러짐: 300자가 넘는 긴 설명 문단을 드래그했을 때 줄바꿈 없는 한 줄의 인라인 툴팁으로 처리되어 팝업이 브라우저 오른쪽 화면 밖으로 탈출하는 현상.
3. AI의 잘못된 부모 DOM 검사와 원인 분석
AI 어시스턴트에게 선택 영역 텍스트 추출 로직을 지시했을 때 작성해 온 코드를 분석했습니다.
// selection.js: 과도한 부모 DOM 검사 및 alert() 남발 (오류 코드)
function handleSelectionTranslate() {
const selection = window.getSelection();
const text = selection.toString().trim();
// 결함 원인: 상위 부모 컨테이너에 번역 태그가 있으면 무조건 번역 완료로 오판!
if (selection.anchorNode.parentElement.querySelector(".wt-translation")) {
alert("번역이 이미 되어있거나 번역할 내용이 없습니다."); // 작업 흐름을 끊는 alert
return;
}
sendTranslateRequest(text, "inline"); // 문단 길이를 무시하고 인라인 고정
}
오탐이 발생한 기술적 원인
AI는 중복 번역을 방어하겠다는 의도로 selection.anchorNode.parentElement.querySelector(".wt-translation") 검사를 넣었습니다.
그러나 사용자가 긴 웹페이지에서 상단 문단을 한 번이라도 전체 번역한 상태에서, 아래쪽의 새로운 영문 문단을 드래그하면 상위 공통 컨테이너(예: <article>, <main>) 내부에 이미 .wt-translation이 존재하게 됩니다. 이로 인해 파서는 방금 드래그한 영문 문장까지 "이미 번역된 요소"로 잘못 판정하여 번역을 전면 차단한 것입니다.
alert() 창은 사용자의 마우스 포커스를 강제로 빼앗아 극심한 불쾌감을 유발합니다.
4. 해결책: 순수 문자열 판정 + 50자 기준 문단 팝업 분기
부모 DOM 탐색을 원천 배제하고 오직 선택된 순수 문자열의 유효성만 검사하도록 로직을 수정했습니다.
부모 요소에 번역 태그가 있다고 번역을 거절하지 마라. 사용자가 현재 드래그한 window.getSelection().toString().trim()의 순수 문자열만 보고 번역을 수행해라. 화면을 가로막는 alert()를 전면 제거하고, 50자를 초과하는 긴 문단은 전용 카드 팝업으로 분기해라.
| 선택 텍스트 길이 | 분기 모드 | 렌더링 UI 형태 | 적용 스타일 |
|---|---|---|---|
| 1자 ~ 50자 | 단어/구문 모드 | 미니 플로팅 툴팁 | .wt-selection-tooltip (원문 상단 밀착) |
| 50자 초과 | 긴 문단 모드 | 독립 문단 팝업 카드 | .wt-selection-card (너비 420px, 줄바꿈/스크롤 지원) |
| 0자 (공백) | 무효 선택 | 무반응 (Silent Return) | 불필요한 알림창 없이 조용히 종료 |
정제된 선택 영역 핸들러 구현
// src/content/selection_handler.js: 순수 텍스트 검증 및 50자 분기
export function handleSelectionTranslation() {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return;
// 1. 부모 DOM을 보지 않고 순수 선택 문자열만 추출
const rawText = selection.toString().trim();
if (rawText.length === 0) return; // 공백 드래그 시 조용히 무시
const isLongParagraph = rawText.length > 50;
// 2. 백그라운드로 번역 요청 발송
chrome.runtime.sendMessage({
action: "translateSelection",
payload: {
text: rawText,
isLongParagraph: isLongParagraph
}
}, (response) => {
if (response && response.success) {
// 3. Range 좌표 기반 지능형 팝업/툴팁 표출
renderSelectionPopup(selection.getRangeAt(0), response.data, isLongParagraph);
}
});
}
/* selection.css: 문단형 팝업 카드 스타일 */
.wt-selection-card {
position: absolute;
max-width: 440px;
min-width: 280px;
padding: 12px 16px;
background: var(--bgContent, #ffffff);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
line-height: 1.6;
word-break: break-word;
z-index: 999999;
}
5. 검증 결과 및 Part 2 (DOM & 렌더링) 총결산
선택 영역 파서를 정비한 뒤 다양한 웹사이트에서 드래그 번역을 검증했습니다.
- 오탐 및 경고창 제로: 페이지 전체 번역이 이미 적용된 복합 웹문서에서도 새로운 단락을 드래그하면 100% 즉시 번역 팝업이 활성화되었습니다.
- 문단 가독성 확보: 300자 이상의 긴 게임 스토리 설명문도 440px 너비의 단락 팝업 카드 안에서 완벽하게 줄바꿈되어 편안하게 읽을 수 있었습니다.
이로써 Part 2 (DOM 조작 & 렌더링 엔진)의 3대 핵심 난제(가상 래퍼를 통한 레이아웃 보존, 25자 듀얼 렌더러, 정밀 선택 영역 파서)가 모두 성공적으로 마무리되었습니다.
마무리하며
DOM 파싱과 UI 렌더링이 안정화되자, 기본 제공되는 구글 번역 엔진 외에 고품질 문맥 이해가 가능한 최신 AI 번역 엔진(Google Gemini, OpenAI GPT, Anthropic Claude, 로컬 프라이버시를 위한 Ollama)을 유연하게 교체하며 사용하고 싶다는 요구가 대두되었습니다.
다음 글부터 시작되는 Part 3 (멀티 LLM 엔진 연동 & 어댑터 패턴)에서는 단일 API 구조를 탈피하여 다양한 AI 번역 엔진을 손쉽게 플러그인할 수 있는 엔진 어댑터 패턴(Adapter Pattern) 아키텍처 설계 과정을 다루어 보겠습니다.
대화 참여하기