[WebTranslator 개발기 #05] 스팀 버튼 깨짐 방지와 인라인·블록 듀얼 렌더러
지난 4편에서는 스팀(Steam) 상점의 복잡한 레이아웃에서 발생한 링크 색상 무차별 전이 버그를 직속 부모 계산 스타일로 해결하고, 고아 텍스트 노드에 display: contents 가상 래퍼를 씌워 Flex/Grid 레이아웃을 0.1px도 틀어짐 없이 보존하는 데 성공했습니다.
하지만 또 다른 실전 렌더링 복병이 기다리고 있었습니다. 스팀 상점의 Add to Cart (장바구니에 추가)나 Buy Now (지금 구매) 같은 작은 녹색 액션 버튼을 번역했을 때, 버튼 내부에 줄바꿈을 동반한 거대한 블록 요소가 강제로 삽입되면서 날씬했던 버튼 높이가 세로로 3배 이상 거대하게 팽창(뚱뚱화)하여 인접 가격표와 겹치는 참사가 발생한 것입니다.
이번 글에서는 모든 번역문을 일괄 블록으로 처리하던 방식을 탈피하고, 25자 기준 및 시맨틱 태그를 결합한 지능형 듀얼 렌더러(Dual Renderer)와 비파괴적(Non-destructive) DOM 안전 삽입을 구축하여 완벽한 UI 균형을 달성한 과정을 정리해 보고자 합니다.
1. 작은 UI 버튼의 참사: 스팀 '장바구니 추가' 버튼의 거대화
웹페이지의 텍스트는 성격에 따라 차지하는 공간의 규칙(Formatting Context)이 완전히 다릅니다.
- 긴 본문 문단: 원문 아래에 독립된 줄로 배치되어야 가독성이 확보됩니다. (Block 성격)
- 버튼 및 메뉴: 정해진 좁은 영역 안에서 원문 옆에 나란히 배치되어야 버튼 크기가 유지됩니다. (Inline 성격)
기존 번역 로직은 모든 텍스트를 일괄적으로 <div class="wt-block"> 형태로 렌더링했습니다. 그 결과 스팀 장바구니 버튼 내부에서 강제 줄바꿈이 발생하며 다음과 같은 문제가 터졌습니다:
| 구분 | 정상 상태 (Original) | 블록 번역 적용 시 (Broken) |
|---|---|---|
| 버튼 내부 형태 | [ Add to Cart ] (높이 약 32px) |
[ Add to Cart (높이 약 90px) |
| 주변 레이아웃 | 옆의 할인율 배지 및 가격표와 완벽 수평 정렬 | 버튼이 거대해져 가격표를 가리거나 아래 줄로 밀려남 |
2. AI의 단순 태그 분기 실패와 자식 노드 증발 버그
AI 어시스턴트에게 버튼이나 짧은 태그는 인라인으로, 본문 문단은 블록으로 분기하도록 지시했습니다.
모든 요소를 일괄 블록으로 처리하지 말고, 버튼이나 짧은 태그는 인라인으로, 본문 문단은 블록으로 분기하여 렌더링하는 지능형 렌더러를 작성해라.
지시를 받은 AI는 아래와 같이 단순 태그 이름으로 분기하고 innerHTML/innerText를 덮어쓰는 코드를 작성해왔습니다.
// renderer.js: 태그 이름 단순 분기 및 innerText/innerHTML 덮어쓰기 (오류 코드)
function renderTranslatedNode(el, translatedText) {
// 1. 단순 태그 이름만으로 분기
if (el.tagName === "P" || el.tagName === "DIV") {
// innerHTML 직접 삽입으로 기존 자식 요소의 이벤트 소실
el.innerHTML += `<div class="wt-block">${translatedText}</div>`;
} else {
// innerText 조작으로 버튼 내부의 SVG 아이콘이나 <i> 태그 통째 증발!
el.innerText += ` (${translatedText})`;
}
}
발생한 심각한 부작용 분석
- 버튼 아이콘 및 이벤트 리스너 파괴: 스팀의 장바구니 버튼 안에는 카트 모양의 SVG 아이콘과 클릭 이벤트 핸들러가 바인딩되어 있습니다.
innerText나innerHTML로 문자열을 덮어쓰는 순간 브라우저가 기존 DOM 노드를 통째로 재생성하여 아이콘이 사라지고 버튼 클릭이 먹통이 되었습니다. - 짧은 DIV 태그의 팽창: 글자 수가 3~5글자에 불과한 짧은 제목이나 메뉴 배지가
<div>태그로 작성되어 있는 경우, 무조건 거대한 블록으로 처리되어 불필요한 공백이 흉하게 벌어졌습니다.
3. 해결책 1: 25자 기준 + 시맨틱 태그 결합 듀얼 렌더러
태그의 본질과 실제 텍스트 길이를 동시에 검사하는 지능형 인라인 판별 함수(isInlineElement)를 구축하도록 지시를 수정했습니다.
innerHTML/innerText로 기존 DOM을 덮어쓰지 마라. 독립된 span 요소를 생성해 삽입하고, 태그 종류(a, button, span, label)와 글자 수(25자 이하)를 동시에 검사하는 isInlineElement 함수를 만들어 25자 이하의 짧은 UI 요소는 인라인으로, 긴 문장은 블록으로 칼같이 분기해라.
| 판별 조건 | 적용 대상 예시 | 적용 클래스 및 스타일 |
|---|---|---|
| 시맨틱 인라인 태그 | <button>, <a>, <span>, <label> |
.wt-translation-inline (나란히 배치) |
| 25자 이하 짧은 텍스트 | <div>New</div>, <h4>Action</h4> |
.wt-translation-inline (여백 왜곡 방지) |
| 25자 초과 긴 문단 | 게임 상세 설명문, 사용자 리뷰 단락 | .wt-translation-block (하단 단락 배치) |
세부 구현 코드
// src/content/renderer.js: 지능형 듀얼 렌더러 판별 및 비파괴 삽입
export function isInlineElement(el, originalText) {
const tagName = el.tagName.toLowerCase();
// 1. 본질이 인라인 요소인 시맨틱 태그 목록
const inlineTags = ["a", "button", "span", "b", "strong", "em", "i", "label"];
if (inlineTags.includes(tagName)) return true;
// 2. 블록 태그라도 텍스트 길이가 25자 이하로 짧은 UI인 경우 인라인 처리
if (originalText.trim().length <= 25) {
const display = window.getComputedStyle(el).display;
if (display.includes("inline") || display.includes("flex")) {
return true;
}
}
return false;
}
export function renderTranslation(targetNode, translatedText, isInline) {
// 기존 DOM을 파괴하지 않고 독립된 번역 전용 span 노드 생성
const transSpan = document.createElement("span");
transSpan.className = isInline ? "wt-translation-inline" : "wt-translation-block";
transSpan.textContent = isInline ? ` (${translatedText})` : translatedText;
// 안전하게 자식 노드로 덧붙임 (이벤트 및 아이콘 100% 보존)
targetNode.appendChild(transSpan);
}
/* content.css: 인라인/블록 듀얼 렌더링 스타일 */
.wt-translation-inline {
display: inline !important;
margin-left: 6px;
font-size: 0.95em;
opacity: 0.9;
}
.wt-translation-block {
display: block !important;
margin-top: 6px;
line-height: 1.5;
}
4. 검증 결과 및 핵심 교훈
듀얼 렌더러를 적용한 결과, 스팀 상점의 모든 UI 요소가 이상적인 시각적 균형을 되찾았습니다.
- 버튼의 원형 보존:
Add to Cart (장바구니에 추가)형태로 텍스트가 나란히 붙어 버튼 높이가 32px 그대로 유지되었으며, 카트 SVG 아이콘과 클릭 이벤트도 온전히 작동했습니다. - 문단의 가독성 유지: 긴 게임 설명 문단은 원문 아래에 정갈한 블록으로 정렬되어 원문과 대조하며 읽기에 최적의 환경을 제공했습니다.
마무리하며
단축키(Alt+A)를 통한 페이지 전체 번역은 이제 완벽한 레이아웃 안정성을 갖추게 되었습니다. 하지만 웹 서핑 중 언제나 페이지 전체를 번역할 필요는 없었으며, 읽고 싶은 특정 문장이나 단어만 빠르게 확인하고 싶을 때를 위해 마우스 드래그 기반의 '선택 영역 번역(Selection Translation)' 기능을 새롭게 도입하기로 했습니다.
다음 글에서는 마우스 드래그를 감지하여 팝업을 띄우는 선택 영역 번역의 초기 설계와 함께, 개발 과정에서 마주친 "내용 없음" 오탐 버그 및 긴 문단 찌그러짐 문제를 해결한 과정을 다루어 보겠습니다.
대화 참여하기