[WebTranslator 개발기 #03] 사용자 사전 적용 순서 버그 해결과 v0.1 스냅샷
지난 2편에서는 AbortController와 3단계 상태 머신을 구축하여 단축키(Alt+A) 연타 시 발생하는 비동기 레이스 컨디션을 방어하고, 100KB 한도의 sync 스토리지와 대용량 local 스토리지를 분리 설계했습니다.
기본 인프라를 안정화한 뒤 실전 웹페이지 번역을 검증하던 중, 또 다른 난관에 직면했습니다. 개발 문서나 게임 패치 노트에 등장하는 고유명사를 올바르게 번역하기 위해 구현한 '사용자 정의 단어 사전(Custom Dictionary)' 기능이 완전히 엉뚱하게 동작하거나 문맥을 파괴하는 치명적인 파이프라인 결함이 발견된 것입니다.
이번 글에서는 사전 단어가 오역되던 전처리(Pre-processing) 방식의 한계를 분석하고 사후 덧씌우기(Post-processing) 파이프라인으로 전면 재설계한 과정과, DOM 자가 오염을 방어하며 프로젝트 v0.1 스냅샷을 완성한 기록을 정리해 보고자 합니다.
1. 사용자 정의 단어 사전의 필요성과 요구사항
기술 문서와 게임 커뮤니티에는 일반적인 번역 엔진이 문맥에 맞지 않게 직역해 버리는 전문 용어와 고유명사가 다수 존재합니다.
| 원문 단어 | 기계 번역 엔진의 일반 번역 | 원하는 사용자 지정 번역 |
|---|---|---|
| Mod (게임 모드) | 수정, 유행, 패션 (Fashion) | 모드 |
| Issue (깃허브 이슈) | 발행, 쟁점 | 이슈 |
| Loadout (장비 세팅) | 적재, 탑재 | 로드아웃 |
따라서 사용자가 옵션 페이지에서 원하는 단어 쌍(Original $\rightarrow$ Translated)을 등록해 두면, 번역 결과에 해당 단어가 100% 강제 적용되어야 했습니다.
2. AI의 파이프라인 역전과 문맥 파괴 오류
AI 어시스턴트에게 사용자 사전을 번역 파이프라인에 통합하도록 지시했습니다.
사용자가 입력한 커스텀 단어 사전이 번역 결과물에 강제 적용되도록 로직을 수정하고, 현재까지의 개발 상태를 점검해라.
지시를 받은 AI는 아래와 같이 번역 API를 호출하기 전에 원문 영어 문장 내 단어를 한글로 먼저 바꿔치기(전처리)하는 코드를 작성해왔습니다.
// content.js: 원문 선치환(Pre-replacement) 방식 (오류 코드)
function prepareTextForTranslation(rawText, userDict) {
let preprocessed = rawText;
// 1. 번역 API로 원문을 보내기 전에 사전을 먼저 한글로 치환!
for (const item of userDict) {
// 예: "This is a great Mod." -> "This is a great 모드."
preprocessed = preprocessed.replaceAll(item.original, item.translated);
}
// 2. 이미 한글이 섞인 문장을 번역 API로 발송
return requestTranslation(preprocessed);
}
왜 전처리 선치환은 실패하는가?
AI가 작성한 전처리 방식은 겉보기에는 직관적이지만, 실제 번역 엔진(Google Translate, Gemini LLM)과 결합했을 때 대참사를 유발했습니다.
- 문맥 붕괴 및 역번역 발생: 번역 엔진에
This is a great 모드.와 같은 한영 혼용 문장이 전달되면, 번역 엔진은 이미 치환된 한글모드를 문맥에 맞지 않는 외래어로 판단하여 다시fashion이나mode로 역번역해 버립니다. - 조사 및 어순 파괴: 앞뒤 단어의 품사 관계가 깨지면서
이것은 훌륭한 패션입니다처럼 문장 전체의 의미가 왜곡되는 현상이 발생했습니다.
3. 해결책: 사후 덧씌우기(Post-replacement) 파이프라인 전환
파이프라인의 순서를 완전히 뒤집어, 원문 전체를 온전히 보내 자연스러운 문맥 번역을 수신한 뒤, 도착한 결과물에 사용자의 사전을 사후 덧씌우는(Post-processing) 아키텍처로 전환했습니다.
원문을 건드려서 번역기로 보내지 마라. 번역 API에는 깨끗한 원문 전체를 온전히 보내서 고품질 문맥 번역을 받아오고, 도착한 최종 번역 결과물에 사용자의 사전을 정규식 단어 경계로 덧씌우는 방식으로 순서를 완전히 바꿔라.
| 단계 | 처리 주체 | 데이터 상태 | 핵심 역할 |
|---|---|---|---|
| 1. 원문 추출 | Content Script | Pure English Text |
사전 치환 없이 순수 원문 텍스트 수집 |
| 2. 문맥 번역 | Translation API | Raw Translated Text |
자연스러운 문장 구조 및 조사 생성 |
| 3. 사후 덧씌우기 | Filter Module | Final Customized Text |
정규식을 통해 지정된 고유명사 강제 치환 |
사후 치환 함수 구현
// src/content/filter.js: 정규식 기반 사후 사용자 사전 덧씌우기
export function applyLocalDictionary(translatedText, userDict) {
if (!userDict || !Array.isArray(userDict) || userDict.length === 0) {
return translatedText;
}
let result = translatedText;
for (const item of userDict) {
if (!item.original || !item.translated) continue;
// 특수문자 이스케이프 처리
const escaped = item.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// 대소문자 무시(gi) 정규식 생성 후 사후 치환
const regex = new RegExp(escaped, "gi");
result = result.replace(regex, item.translated);
}
return result;
}
4. DOM 자가 오염 방지 (`isOurElement` 가드)
사전 적용 로직을 테스트하던 중, 단축키를 눌렀을 때 이미 삽입된 번역문 컨테이너 아래에 또 다른 번역문이 중첩 삽입되는 DOM 자가 오염 버그가 발견되었습니다.
텍스트 노드 수집기(collectTextNodes)가 확장 프로그램이 자체적으로 렌더링한 .wt-translation 요소 내부의 텍스트까지 번역 대상 원문으로 착각하고 긁어간 것이 원인이었습니다.
// src/content/dom.js: 자가 요소 배제 가드 로직
export function isOurElement(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
// 확장 프로그램이 생성한 클래스나 태그는 텍스트 탐색에서 원천 배제
return (
node.classList.contains("wt-translation") ||
node.classList.contains("wt-container") ||
node.tagName === "SCRIPT" ||
node.tagName === "STYLE"
);
}
5. v0.1 스냅샷 마일스톤 완성
이로써 프로젝트의 기초 뼈대를 이루는 핵심 4대 기능이 완성되었으며, 이를 v0.1 스냅샷으로 아카이빙했습니다.
- Chrome Manifest V3 보안 통신: Background Service Worker 중계를 통한 완벽한 CSP 우회
- 단축키 비동기 상태 머신:
AbortController기반 실시간 요청 물리 취소 (Race Condition 방어) - 스토리지 이원화: 설정값(Sync 100KB)과 대용량 캐시(Local 10MB)의 물리적 격리
- 사후 사전 파이프라인: 원문 문맥을 훼손하지 않는 사후 덧씌우기 및 DOM 자가 오염 가드
마무리하며
기초 아키텍처가 탄탄하게 완성된 v0.1을 바탕으로 스팀(Steam) 상점 페이지를 본격적으로 테스트하기 시작했습니다. 그러나 실제 커뮤니티 페이지에서는 <a> 링크 태그 내부의 파란색 글자 색상이 페이지 전체의 다른 번역문으로 잘못 상속되어 글자가 증발하거나, 링크 클릭 이벤트가 통째로 먹통이 되는 새로운 DOM 렌더링 결함이 발생했습니다.
다음 글에서는 Steam의 복잡한 DOM 트리를 분석하고, display: contents 가상 래퍼를 도입하여 링크 색상 전이와 고아 텍스트 노드 문제를 해결한 과정을 다루어 보겠습니다.
대화 참여하기