구글 번역기 이슈 해결 해보기

구글 번역기 이슈 해결 해보기

2024-01-183 min read

배경

사내 오류 지원 채널을 통해 “구글 번역기를 사용하면 서비스 이용 중 화면이 하얗게 사라지는(화이트아웃) 현상” 이 발생한다는 제보를 받았어요. 문제가 발생한 상황은 Chrome 브라우저에서 구글 번역기를만 사용하고 있는 유저가, 특정 조건에서 화면 전체가 화이트아웃 되는 현상이었어요.
처음엔 아무 단서가 없어 당황스러웠지만, 관련 리서치를 통해 재현 조건과 원인을 파악할 수 있었어요.

보안상의 이유로 영상은 샘플로 대체

재현조건

문제를 분석하며 발견한 재현 조건은 아래와 같아요:

  1. 부모-자식 관계의 컴포넌트 구조
  2. 2개 이상의 자식 노드
  3. Hello World처럼 텍스트만 존재하는 노드가 조건에 따라 제거되는 경우
const [someCondition, setSomeCondition] = useState(true); <div id="parent"> {someCondition && 'Hello World'} <text onClick={() => setSomeCondition(false)}>Hello</text> </div>

위와 같은 상황에서 클릭을 통해 someConditionfalse가 바뀐다면 어떻게 될까요?
아래 사진과 같이 에러(화이트아웃)가 나오게 돼요. white out

구글번역기 동작방식

저는 해당 현상이 리액트보단 구글 번역기의 번역 방식과 더 관련 있다고 판단했어요.

구글 번역기는 페이지 내 텍스트를 감지하고 번역하는 과정에서 다음과 같은 방식을 사용해요:

  • 태그로 감싼 텍스트는 해당 태그 내부의 텍스트만 치환
  • 태그로 감싸지 않은 순수 텍스트<font> 태그로 감싼 후 번역된 텍스트로 직접 치환
// AS IS <div> 'Welcome' <text>Something</text> </div> // TO BE <div> <font>'안녕하세요'</font> <text>무언가</text> </div>

이때 실제 번역 과정에서는 removeChild, insertBefore와 같은 DOM API를 활용하여 기존 텍스트를 제거하고 새 엘리먼트를 삽입하게 돼요.

const children = document.getElementById("parent").childNodes; for (const myEl of children) { if (myEl.nodeType === Node.TEXT_NODE) { const fontEl = document.createElement("font"); fontEl.textContent = myEl.data; myEl.parentElement.insertBefore(fontEl, myEl); myEl.parentElement.removeChild(myEl); } }

원인

해당 이슈에 대해서 조금 더 리서치를 진행해보았고 한 Github Issue를 찾았어요. 알고보니 되게 오래된 이슈였고, 논의가 최근까지도 활발하게 진행되고 있었어요. 하나하나 코멘트들을 읽어보니 원인에 대해서 알겠더라구요.

The problem is that Google Translate replaces text nodes with <font> tags containing translations while React keeps references to the text nodes that are no longer in the DOM tree.

구글 번역기는 번역된 텍스트를 <font> 태그로 감싼 후 대체하지만, React에선 더 이상 DOM tree에 없는 텍스트노드에 대한 참조를 계속 유지하고 있기때문이에요.
조건에 의해서 텍스트노드가 갑자기 사라지게될때 이미 DOM tree에서 없어진 텍스트 노드를 리액트에선 계속 참조하고 있기 때문에, 존재하지 않는 텍스트노드에 대해서 계속 메서드(removeChild)를 실행하려고 해서 에러가 발생하게 되는것이에요.

개인적인 생각으로는, React에서 reconcile 과정 이후 변경사항 반영 & 렌더링 해주는 과정에서, 외부요인에 의해 실제론 DOM이 바뀌었으나 리액트에서 참조하고있는 메모리상의 DOM(VDOM)에서는 바뀐것을 인지하지 못해서 에러가 나는것같다고 생각했어요.

근본원인: React vs Chromium

논의가 진행중이던 Github Issue를 조금 더 살펴보니, React 개발자들은 해당 이슈에 대해서 구글 번역기의 동작이 문제인것이라고 판단하고 chromium측에 다시 이슈레이징을 한 상태였더라구요.

그러나 chromium 개발자들은 장문으로 반박하였어요.

chromium 1
chromium 2

요약:
React와 같은 Virtual DOM을 사용하는 라이브러리들은 DOM 노드 참조에 의존하기 때문에 구글번역기 같은 외부에서 DOM을 조작하는 라이브러리와 공존하게 되는경우 에러가 날 수 밖에 없다.
대부분의 VDOM 라이브러리는 자기자신 이외에는 DOM을 수정하는 매개체가 없다는 가정 하에 작성되는 경우가 많다. 외부에서 트리거된 DOM변경작업 이후에도 DOM을 최신 상태로 유지하도록 사전에 모니터링하지 않으면 이 문제는 더욱 악화될것이다.
문제는 자바스크립트 라이브러리가 DOM이 본질적으로 설계상 변경 가능하다는 사실을 고려하지 않고, VDOM 라이브러리들이 DOM에 대한 완전하고 독점적인 제어권을 가지고 있다고 가정할 때 발생한다.
이것은 구글번역기만의 문제가 아닌 매우 일반적인 상황에서도 나타날 수 있으며, 구글 번역기는 한 브라우저의 DOM 수정 기능 중 하나일 뿐 근본적인 문제의 증상을 더욱 명확하게 만들고 있는꼴이다.

chromium측에서는 위와같은 입장을 밝히면서 수정에 대해선 회의적인 입장이였어요. 저도 해당 글을 읽어보면서 공감이 잘 되었기때문에 근본적인 해결방법은 없겠다고 생각했어요.

해결방법

프로덕션 환경에서 실사용자에게 영향을 주는 문제이기에, 임시적이더라도 대응이 필요했어요.
저는 아래 두 가지 방법을 생각했어요:

1. Eslint 커스텀 룰을 통한 사전 차단 태그로 감싸지지 않은 순수 텍스트가 존재하지 않도록 강제하는 방법이에요.

  • 관련 오픈소스: eslint-plugin-sayari
  • 효과적이고 앞으로 발생할 문제를 사전차단 가능하다고 생각했지만 적용 시 1300곳 이상 수정이 필요해 현실적으로 어려웠어요 🥲

eslint error

2. Monkey Patching을 통한 방어코드 적용 문제가 발생하는 removeChild, insertBefore 메서드 자체를 Monkey Patch 방식으로 오버라이드 해서, 안전하게 동작하도록 만드는 방법이에요.

export const monkeyPatchGoogleTranslatorMethods = () => { if (typeof Node === 'function' && Node.prototype) { const { removeChild: originalRemoveChild } = Node.prototype; Node.prototype.removeChild = function (child) { if (child.parentNode !== this) { if (console) { logError(child, this) } return child; } return originalRemoveChild.apply(this, arguments); }; const { insertBefore: originalInsertBefore } = Node.prototype; Node.prototype.insertBefore = function (newNode, referenceNode) { if (referenceNode && referenceNode.parentNode !== this) { if (console) { logError(referenceNode, this) } return newNode; } return originalInsertBefore.apply(this, arguments); }; } };

다만 Monkey Patching 특성상 서비스 환경에선 위험성이 존재했기 때문에 서비스 환경이 아닌 개발 환경에서만 활성화하여 점진적으로 문제를 개선할 수 있도록 했어요.

마무리

이번 이슈는 단순 버그 수정보다는 얻은게 많았어요. 정확한 재현 조건과 외부 라이브러리 간 충돌 구조를 분석하고, React와 Chromium 개발자들의 기술적 논의와 관점 차이를 직접 확인해보며 많은 것을 배울 수 있었어요. 근본적인 해결은 어려웠지만, 현실적인 대안을 마련하고 서비스 품질을 지킬 수 있었던 점에서 좋은 경험이였던것 같아요 ☺️

참고

Profile Image

Clzzi

Frontend 개발자 손민재입니다. 방문해 주셔서 감사합니다!