Google Translate, removeChild, Node 에러

강연주·2025년 2월 26일

🪪 한 달 인턴

목록 보기
7/8

https://velog.io/@ansrjsdn/Failed-to-execute-removeChild-on-Node-The-node-to-be-removed-is-not-a-child-of-this-node.-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0

Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.


에러 원인

GoogleTranslate는 리액트 DOM의 텍스트 노드를 찾아서 다 <font> 태그로 감싸고
구조를 재구성한다. (원본 DOM 구조를 크게 변경)

  • font 태그로 교체가 누락되는 경우 발생.
  • font 태그로 감싸지며 텍스트 노드의 부모자식 구조 재조정.
  • 리액트 가상 DOM과 실제 DOM 간의 매핑이 깨짐.

➡️ 리액트가 참조할 노드가 존재하지 않아 에러 발생

(더 보기 : https://github.com/solidjs/solid/issues/1451)


해결

참고 : Google Translate's usage of font for text replacement breaks React
출처 : React Google Translate 오류

1. Google Translate 막아버리기

2. 모든 TextNode 들을 Tag들로 감싸기
TextNode들을 Tag들로 감싸면, Google Translate을 통해 그 내용들이 <font> 태그로 대체되더라도 React에서 참조하는 노드가 그대로 DOM Tree에 남는다. 해당 내용을 모두 찾아서 바꾸기 어려울 것 같아서 제외.

3. i18n 등을 사용해 번역을 직접 제공하기
들여야하는 노력이 크다.

4. Workaround(Monkey Patch) 사용하기
문제를 근본적으로 해결하는 건 아니고, 오류를 없애고 페이지가 다운되는 것을 막는다.
대신 사이트의 성능에 문제가 생길 수 있다.

출처 : reddit-remove child error

5. 에러바운더리로 컴포넌트 감싸기

class BrowserTranslateErrorCatcher extends Component {
  state = { hasError: false } // example state
  componentDidCatch(error) {
    if (error.message.includes("Failed to execute') {
      // You could put some error logging here, or set a state, whatever you like.
      this.setState({ hasError: true });
    } else {
      throw error;
    }
  }
  render() {
    return this.props.children;
  }
}

사용 예시

<BrowserTranslateErrorCatcher>
  <SomeComponentThatHasTheProblem />
</BrowserTranslateErrorCatcher>

  1. UI 내 탭 이동 - 번역 유지, 에러 없음
    동작 방식:

이것은 전형적인 클라이언트 사이드 렌더링(CSR) 상황입니다.
페이지 전체를 다시 로드하지 않고 JavaScript를 통해 DOM의 일부만 변경됩니다.
새 네트워크 요청 없이 이미 로드된 페이지 내에서 컨텐츠만 교체합니다.

번역이 유지되는 이유:

Google 번역기는 페이지의 DOM을 직접 수정하는 방식으로 작동합니다.
탭 전환은 페이지를 새로 로드하지 않기 때문에 Google 번역기가 이미 변환한 DOM 구조가 유지됩니다.
새로운 컨텐츠가 로드될 때 번역기의 MutationObserver가 이를 감지하고 새 컨텐츠도 번역합니다.

에러가 없는 이유:

동일한 DOM 트리 내에서 조작이 이루어지므로 부모-자식 관계 불일치가 발생할 가능성이 낮습니다.
탭 전환은 일반적으로 display: none/block 같은 CSS 변경이나 간단한 DOM 조작만 수행합니다.

  1. 페이지 이동 - removeChild 에러 발생
    동작 방식:

이것은 단일 페이지 애플리케이션(SPA)의 클라이언트 사이드 라우팅입니다.
전체 페이지를 다시 로드하지 않고 JavaScript로 페이지 컨텐츠를 교체합니다.
React, Vue 등의 라우터가 DOM을 동적으로 변경합니다.

에러가 발생하는 이유:

Google 번역기는 DOM에 자체 노드를 추가하고 원본 텍스트를 감싸는 구조를 만듭니다.
SPA 라우팅이 발생할 때, 라우터는 페이지 컨텐츠를 제거하고 새 컨텐츠로 교체합니다.
이때 번역기가 생성한 DOM 구조와 프레임워크의 가상 DOM 간에 불일치가 발생합니다.
프레임워크는 자신이 관리하는 가상 DOM 구조에 따라 요소를 제거하려 하지만, 실제 DOM은 번역기에 의해 변경되었습니다.
그 결과 removeChild 호출 시 부모-자식 관계가 일치하지 않아 에러가 발생합니다.

  1. 새 창으로 페이지 이동 - 번역 풀림, 에러 없음
    동작 방식:

이것은 전통적인 멀티 페이지 애플리케이션(MPA) 또는 서버 사이드 렌더링(SSR) 방식입니다.
브라우저가 완전히 새로운 페이지를 요청하고 로드합니다.
이전 페이지의 DOM과 JavaScript 상태는 완전히 제거됩니다.

번역이 풀리는 이유:

새 창이나 새 페이지 로드는 완전히 새로운 DOM을 생성합니다.
Google 번역기의 변경사항은 이전 페이지에만 적용되었으므로 새 페이지에는 영향을 주지 않습니다.
사용자가 새 페이지에서 다시 번역을 시작해야 합니다.

에러가 없는 이유:

완전히 새로운 페이지이므로 번역기가 수정한 DOM과 새 페이지의 DOM 사이에 충돌이 없습니다.
이전 페이지의 모든 상태와 DOM이 파괴되고 완전히 새로 시작합니다.

해결 방법
앞서 설명된 코드 패치는 특히 두 번째 상황(SPA 라우팅)에서 발생하는 문제를 해결하는 데 도움이 됩니다. 이 코드는 부모-자식 관계가 일치하지 않을 때 오류를 방지하고 작업을 안전하게 처리합니다.
이 코드를 클라이언트 사이드 코드에 추가하여 번역기와 SPA 라우팅 간의 충돌을 완화할 수 있습니다.


이 코드는 반드시 루트 레이아웃에만 적용할 필요는 없습니다. 중요한 점은 다음과 같습니다:

클라이언트 환경에서 실행되어야 함
페이지 로드 초기에 실행되어야 함
모든 라우트/페이지에 적용되어야 함

적용할 수 있는 파일 옵션:
Next.js 프로젝트의 경우:

pages/_app.js 또는 pages/_app.tsx (Pages Router)
app/layout.js 또는 app/layout.tsx (App Router, 'use client' 지시어 필요)
전역적으로 로드되는 클라이언트 컴포넌트

React 프로젝트의 경우:

index.js 또는 메인 앱 진입점
App.js 또는 루트 컴포넌트

일반 웹 프로젝트의 경우:

메인 JavaScript 파일
모든 페이지에 포함되는 공통 스크립트

중요한 것은 사용자가 어떤 페이지에 접근하든 이 코드가 실행되어야 한다는 점입니다. 따라서 앱이 처음 로드될 때 항상 실행되는 파일에 배치하는 것이 좋습니다.
코드 적용 시 다음과 같이 클라이언트 사이드에서만 실행되도록 해야 합니다:
javascriptCopy// Next.js App Router의 경우
'use client';

// 일반적인 모든 경우
if (typeof window !== 'undefined') {
// DOM 패치 코드
if (typeof Node === 'function' && Node.prototype) {
// removeChild와 insertBefore 패치 코드
}
}
또는 useEffect 내에서 실행하는 방법도 있습니다:
javascriptCopyimport { useEffect } from 'react';

function MyApp({ Component, pageProps }) {
useEffect(() => {
if (typeof Node === 'function' && Node.prototype) {
// removeChild와 insertBefore 패치 코드
}
}, []);

return <Component {...pageProps} />;
}
당신의 프로젝트 구조에 따라 가장 적합한 위치를 선택하면 됩니다. 핵심은 클라이언트 사이드의 초기 로드 시점에 코드가 실행되어 모든 페이지 네비게이션에 영향을 미치도록 하는 것입니다.RetryY// 🐡 번역 테스트 출발페이지import KollegeMain from "@/components/kollege/main/KollegeMain";export default function PageKollegeMain() {
return (

<main className="bg-background-layer-1">
  <KollegeMain />
</main>

);
}위 코드가 번역을 해서 다른 페이지로 이동하기 전의 출발 페이지인데, 여기서 적용해보면 돼? 적용된 코드 전체를 보여줘Edit네, 이 페이지에서도 적용할 수 있습니다. 다만 이 컴포넌트는 아마도 라우트 컴포넌트로 보이는데, 이 경우 Google 번역 DOM 패치가 모든 페이지에 적용되길 원한다면 이 페이지보다는 더 상위 레벨에 적용하는 것이 좋습니다.
하지만 테스트용으로 이 페이지에 적용하려면 다음과 같이 코드를 수정하면 됩니다:
jsxCopy// 🐡 번역 테스트 출발페이지
'use client'; // Next.js 13+ App Router를 사용하는 경우 필요

import { useEffect } from 'react';
import KollegeMain from "@/components/kollege/main/KollegeMain";

export default function PageKollegeMain() {
useEffect(() => {
if (typeof window !== 'undefined') {
if (typeof Node === 'function' && Node.prototype) {
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function(child) {
// 여기에서 this는 textNode의 parentNode이다.
if (child.parentNode !== this) {
// child의 parentNode와 this가 다르면, child를 return한다.
// (child가 textNode면, 그 parentNode가 null로 찾을 수 없기 때문에 이 분기로 들어온다.)
if (console) {
console.error('Cannot remove a child from a different parent', child, this);
}
return child;
}
// child가 textNode가 아니면, this가 가르키는 context를 변경해서 바로 removeChild를 실행한다.
return originalRemoveChild.apply(this, arguments);
}

    const originalInsertBefore = Node.prototype.insertBefore;
    Node.prototype.insertBefore = function(newNode, referenceNode) {
      if (referenceNode && referenceNode.parentNode !== this) {
        // referenceNode가 존재하고, referenceNode의 parentNode가 this가 다르면, 새로운 Node를 반환한다.
        // (child가 textNode면, 그 parentNode가 null로 찾을 수 없기 때문에 이 분기로 들어올 것이라고 생각한다.)
        if (console) {
          console.error('Cannot insert before a reference node from a different parent', referenceNode, this);
        }
        return newNode;
      }
      // child가 textNode가 아니면, this가 가르키는 context를 변경해서 바로 insertBefore를 실행한다.
      return originalInsertBefore.apply(this, arguments);
    }
  }
}

}, []);

return (

<main className="bg-background-layer-1">
  <KollegeMain />
</main>

);
}
이 코드는:

'use client' 지시어로 클라이언트 컴포넌트임을 선언합니다 (Next.js App Router 사용 시)
React의 useEffect를 import하고 사용합니다
컴포넌트가 마운트될 때 한 번만 실행되는 useEffect 내에서 DOM 패치 코드를 실행합니다
브라우저 환경인지 확인 후 (typeof window !== 'undefined') 패치를 적용합니다

이렇게 하면 이 페이지가 로드될 때 Google 번역 관련 DOM 패치가 적용됩니다. 하지만 다른 페이지에서 직접 접근할 경우에는 이 패치가 적용되지 않을 수 있으므로, 테스트 후에는 _app.js나 layout.js 같은 전역 파일로 이동하는 것이 좋습니다.RetryYNode.prototype.removeChild = function(child) {
오류 : '(child: T) => Node' 형식은 '(child: T) => T' 형식에 할당할 수 없습니다. 'Node' 형식은 'T' 형식에 할당할 수 없습니다. 'Node'은(는) 'T' 형식의 제약 조건에 할당할 수 있지만, 'T'은(는) 'Node' 제약 조건의 다른 하위 형식으로 인스턴스화할 수 있습니다.ts(2322)
(method) Node.removeChild(child: T): T
MDN Reference(this, arguments) 오류 : 'IArguments' 형식의 인수는 '[child: Node]' 형식의 매개 변수에 할당될 수 없습니다.ts(2345)
(local var) arguments: IArguments
문제 보기 (⌥F8)
빠른 수정을 사용할 수 없음Node.prototype.insertBefore = function(newNode, referenceNode) { 오류 : '(newNode: T, referenceNode: Node | null) => Node' 형식은 '(node: T, child: Node | null) => T' 형식에 할당할 수 없습니다. 'Node' 형식은 'T' 형식에 할당할 수 없습니다. 'Node'은(는) 'T' 형식의 제약 조건에 할당할 수 있지만, 'T'은(는) 'Node' 제약 조건의 다른 하위 형식으로 인스턴스화할 수 있습니다.ts(2322)
(method) Node.insertBefore(node: T, child: Node | null): T
MDN Reference
문제 보기 (⌥F8)
빠른 수정을 사용할 수 없음return originalInsertBefore.apply(this, arguments); 오류 : 'IArguments' 형식의 인수는 '[node: Node, child: Node | null]' 형식의 매개 변수에 할당될 수 없습니다.ts(2345)
(local var) arguments: IArguments
문제 보기 (⌥F8)
빠른 수정을 사용할 수 없음EditTypeScript에서 발생하는 타입 오류를 수정해야 하네요. 이 문제는 TypeScript의 엄격한 타입 체크 때문에 발생합니다. Node 메서드의 제네릭 타입 시그니처와 arguments 객체 사용에 관련된 문제입니다.
수정된 코드를 제공해드리겠습니다:
tsxCopy// 🐡 번역 테스트 출발페이지
'use client'; // Next.js 13+ App Router를 사용하는 경우 필요

import { useEffect } from 'react';
import KollegeMain from "@/components/kollege/main/KollegeMain";

export default function PageKollegeMain() {
useEffect(() => {
if (typeof window !== 'undefined') {
if (typeof Node === 'function' && Node.prototype) {
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function(child: T): T {
if (child.parentNode !== this) {
if (console) {
console.error('Cannot remove a child from a different parent', child, this);
}
return child;
}
// TypeScript에서는 apply 사용 시 타입 단언이 필요합니다
return originalRemoveChild.call(this, child) as T;
};

    const originalInsertBefore = Node.prototype.insertBefore;
    Node.prototype.insertBefore = function<T extends Node>(newNode: T, referenceNode: Node | null): T {
      if (referenceNode && referenceNode.parentNode !== this) {
        if (console) {
          console.error('Cannot insert before a reference node from a different parent', referenceNode, this);
        }
        return newNode;
      }
      // TypeScript에서는 apply 사용 시 타입 단언이 필요합니다
      return originalInsertBefore.call(this, newNode, referenceNode) as T;
    };
  }
}

}, []);

return (

<main className="bg-background-layer-1">
  <KollegeMain />
</main>

);
}
수정사항 설명:

제네릭 타입 명시적 선언:

function(child: T): T - 입력과 출력 타입이 동일한 제네릭 타입을 명시
function(newNode: T, referenceNode: Node | null): T - 같은 방식으로 insertBefore도 수정

arguments 객체 대신 명명된 매개변수 사용:

apply(this, arguments) 대신 call(this, child) 또는 call(this, newNode, referenceNode) 사용

반환 타입 단언 추가:

as T를 사용하여 원본 메서드의 반환값이 예상된 제네릭 타입과 일치하도록 단언

이렇게 수정하면 TypeScript의 타입 검사를 통과하면서도 원래 코드의 기능을 유지할 수 있습니다.RetryY이 코드를 적용하니까 페이지 이동을 해도 번역이 풀리지 않고 오류가 나지 않아서 좋은데 이해가 안 됐어.
혹시 패치 코드의 문법(아주 기초적인 개념부터 동작원리까지)을 하나하나 설명해줄 수 있어?Edit물론이죠, 이 패치 코드의 문법과 동작 원리를 기초부터 자세히 설명해드리겠습니다.
1. 프로토타입 이해하기
typescriptCopyif (typeof Node === 'function' && Node.prototype) {
// 코드
}

typeof Node === 'function': JavaScript에서 클래스와 생성자는 함수로 취급됩니다. 이 코드는 Node가 함수인지 확인합니다.
Node.prototype: 모든 JavaScript 객체는 프로토타입을 가집니다. Node.prototype은 모든 DOM 노드 객체가 상속받는 프로토타입 객체입니다.
이 조건문은 브라우저 환경이고 DOM API가 사용 가능한지 확인합니다.

  1. 메서드 오버라이딩
    typescriptCopyconst originalRemoveChild = Node.prototype.removeChild;

원본 removeChild 메서드를 변수에 저장합니다. 이는 나중에 사용하기 위함입니다.
JavaScript에서는 객체의 메서드를 변수에 할당할 수 있습니다.

  1. 제네릭 타입과 함수 재정의
    typescriptCopyNode.prototype.removeChild = function(child: T): T {
    // 코드
    };

function(child: T): T: TypeScript의 제네릭 문법으로, T는 Node 타입이나 그 하위 타입이어야 합니다.
이 함수는 T 타입의 매개변수를 받아서 같은 T 타입을 반환한다는 의미입니다.
원래 removeChild 메서드는 삭제한 노드를 반환하므로, 이 새 함수도 같은 동작을 유지합니다.

  1. 부모-자식 관계 검증
    typescriptCopyif (child.parentNode !== this) {
    if (console) {
    console.error('Cannot remove a child from a different parent', child, this);
    }
    return child;
    }

child.parentNode !== this: 삭제하려는 노드의 부모가 현재 노드(this)와 다른지 확인합니다.
DOM에서 .removeChild()는 직접적인 부모 노드에서만 호출해야 합니다.
Google 번역기는 DOM 구조를 변경하므로, 프레임워크가 예상하는 부모-자식 관계와 실제 DOM의 관계가 달라질 수 있습니다.
불일치가 발견되면 오류를 기록하고 노드를 그대로 반환합니다(제거하지 않음).

  1. 원본 메서드 호출
    typescriptCopyreturn originalRemoveChild.call(this, child) as T;

.call(this, child): 원본 removeChild 메서드를 현재 컨텍스트(this)에서 호출합니다.
this는 메서드가 호출된 객체(부모 노드)입니다.
as T: TypeScript에게 반환값이 입력과 동일한 타입(T)임을 알려줍니다.
apply() 대신 call()을 사용하는 이유는 명시적으로 인자를 전달할 수 있기 때문입니다.

  1. insertBefore 메서드도 유사하게 패치
    typescriptCopyNode.prototype.insertBefore = function(newNode: T, referenceNode: Node | null): T {
    if (referenceNode && referenceNode.parentNode !== this) {
    // 코드
    }
    return originalInsertBefore.call(this, newNode, referenceNode) as T;
    };

insertBefore는 새 노드를 참조 노드 앞에 삽입하는 메서드입니다.
이 패치도 유사한 논리로 작동합니다: 참조 노드의 부모가 현재 노드와 다르면 작업을 중단합니다.

  1. 전체 패치의 동작 원리

문제의 원인: Google 번역기는 DOM을 수정하여 원본 텍스트 노드를 다른 구조로 감쌉니다. 이로 인해 프레임워크(React 등)의 가상 DOM과 실제 DOM 사이에 불일치가 발생합니다.
해결 방법:

DOM 조작 메서드를 가로채서 부모-자식 관계가 예상과 다를 때 오류 대신 안전하게 처리합니다.
잘못된 조작을 시도할 경우 오류를 콘솔에 기록하고 작업을 중단합니다.
정상적인 경우 원래 기능을 그대로 수행합니다.

효과:

페이지 탐색 시 "Cannot remove a child from a different parent" 오류가 발생하지만 앱이 중단되지 않습니다.
번역된 상태가 유지되어 사용자 경험이 향상됩니다.

이 패치는 브라우저의 기본 DOM API를 변경하는 "몽키 패치(Monkey Patch)" 기법의 예입니다. 원래의 기능을 유지하면서 특정 상황에서 발생하는 오류를 방지하는 방어적 프로그래밍의 한 형태라고 볼 수 있습니다.RetryY그럼 웹사이트의 속도가 느려진다거나 혹은 다른 단점이 있을까?Edit이러한 DOM 패치 코드를 적용하면 다음과 같은 잠재적인 단점이나 고려사항이 있을 수 있습니다:
성능 영향

미미한 오버헤드: 모든 removeChild와 insertBefore 호출에 추가 로직이 실행되므로 약간의 성능 오버헤드가 발생합니다. 하지만 현대 브라우저에서는 이 정도 수준의 오버헤드는 일반적으로 눈에 띄지 않습니다.
에러 로깅: 문제가 발생할 때마다 console.error가 호출되는데, 많은 에러가 발생하면 콘솔 출력이 과도해져 브라우저 개발자 도구의 성능에 영향을 줄 수 있습니다.

호환성 문제

다른 라이브러리와의 충돌: 다른 JavaScript 라이브러리나 프레임워크가 동일한 DOM 메서드를 수정하는 경우 충돌이 발생할 수 있습니다.
브라우저 업데이트: 브라우저가 업데이트되어 DOM API가 변경되면 이 패치가 더 이상 작동하지 않을 수 있습니다.

디버깅 복잡성

디버깅 어려움: 기본 DOM 메서드를 변경하면 디버깅이 더 복잡해질 수 있습니다. 예상치 못한 동작이 발생했을 때 원인을 파악하기 어려울 수 있습니다.
스택 트레이스 변경: 오류가 발생했을 때 스택 트레이스가 덜 명확해질 수 있습니다.

안정성 문제

부작용 위험: 모든 DOM 조작이 이 패치를 통과하게 되므로, 의도하지 않은 부작용이 발생할 가능성이 있습니다.
보안 고려사항: 코어 브라우저 기능을 수정하는 것은 항상 일정한 보안 위험을 내포합니다.

권장 접근법
그럼에도 불구하고, 이러한 패치는 Google 번역 문제를 해결하는 현실적인 방법입니다. 단점을 최소화하기 위해:

조건부 적용: 번역 기능이 감지될 때만 패치를 적용하는 방식으로 변경할 수 있습니다.
필요한 페이지에만 적용: 전체 사이트가 아닌 필요한 페이지에만 패치를 적용할 수 있습니다.
모니터링: 패치 적용 후 웹사이트 성능과 오류를 모니터링하여 문제가 발생하면 빠르게 대응합니다.
향후 업데이트 고려: 브라우저나 Google 번역 기능이 업데이트되면 패치도 업데이트해야 할 수 있습니다.

실제로는 이 패치가 웹사이트 속도에 미치는 영향은 매우 미미하며, Google 번역 사용 시 발생하는 오류를 방지함으로써 얻는 사용자 경험 개선 효과가 더 클 수 있습니다.


https://velog.io/@ansrjsdn/Failed-to-execute-removeChild-on-Node-The-node-to-be-removed-is-not-a-child-of-this-node.-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0


🙈 TIL - 몽키 패치

profile
아무튼, 개발자

0개의 댓글