
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
GoogleTranslate는 리액트 DOM의 텍스트 노드를 찾아서 다 <font> 태그로 감싸고
구조를 재구성한다. (원본 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>
이것은 전형적인 클라이언트 사이드 렌더링(CSR) 상황입니다.
페이지 전체를 다시 로드하지 않고 JavaScript를 통해 DOM의 일부만 변경됩니다.
새 네트워크 요청 없이 이미 로드된 페이지 내에서 컨텐츠만 교체합니다.
번역이 유지되는 이유:
Google 번역기는 페이지의 DOM을 직접 수정하는 방식으로 작동합니다.
탭 전환은 페이지를 새로 로드하지 않기 때문에 Google 번역기가 이미 변환한 DOM 구조가 유지됩니다.
새로운 컨텐츠가 로드될 때 번역기의 MutationObserver가 이를 감지하고 새 컨텐츠도 번역합니다.
에러가 없는 이유:
동일한 DOM 트리 내에서 조작이 이루어지므로 부모-자식 관계 불일치가 발생할 가능성이 낮습니다.
탭 전환은 일반적으로 display: none/block 같은 CSS 변경이나 간단한 DOM 조작만 수행합니다.
이것은 단일 페이지 애플리케이션(SPA)의 클라이언트 사이드 라우팅입니다.
전체 페이지를 다시 로드하지 않고 JavaScript로 페이지 컨텐츠를 교체합니다.
React, Vue 등의 라우터가 DOM을 동적으로 변경합니다.
에러가 발생하는 이유:
Google 번역기는 DOM에 자체 노드를 추가하고 원본 텍스트를 감싸는 구조를 만듭니다.
SPA 라우팅이 발생할 때, 라우터는 페이지 컨텐츠를 제거하고 새 컨텐츠로 교체합니다.
이때 번역기가 생성한 DOM 구조와 프레임워크의 가상 DOM 간에 불일치가 발생합니다.
프레임워크는 자신이 관리하는 가상 DOM 구조에 따라 요소를 제거하려 하지만, 실제 DOM은 번역기에 의해 변경되었습니다.
그 결과 removeChild 호출 시 부모-자식 관계가 일치하지 않아 에러가 발생합니다.
이것은 전통적인 멀티 페이지 애플리케이션(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가 사용 가능한지 확인합니다.
원본 removeChild 메서드를 변수에 저장합니다. 이는 나중에 사용하기 위함입니다.
JavaScript에서는 객체의 메서드를 변수에 할당할 수 있습니다.
function(child: T): T: TypeScript의 제네릭 문법으로, T는 Node 타입이나 그 하위 타입이어야 합니다.
이 함수는 T 타입의 매개변수를 받아서 같은 T 타입을 반환한다는 의미입니다.
원래 removeChild 메서드는 삭제한 노드를 반환하므로, 이 새 함수도 같은 동작을 유지합니다.
child.parentNode !== this: 삭제하려는 노드의 부모가 현재 노드(this)와 다른지 확인합니다.
DOM에서 .removeChild()는 직접적인 부모 노드에서만 호출해야 합니다.
Google 번역기는 DOM 구조를 변경하므로, 프레임워크가 예상하는 부모-자식 관계와 실제 DOM의 관계가 달라질 수 있습니다.
불일치가 발견되면 오류를 기록하고 노드를 그대로 반환합니다(제거하지 않음).
.call(this, child): 원본 removeChild 메서드를 현재 컨텍스트(this)에서 호출합니다.
this는 메서드가 호출된 객체(부모 노드)입니다.
as T: TypeScript에게 반환값이 입력과 동일한 타입(T)임을 알려줍니다.
apply() 대신 call()을 사용하는 이유는 명시적으로 인자를 전달할 수 있기 때문입니다.
insertBefore는 새 노드를 참조 노드 앞에 삽입하는 메서드입니다.
이 패치도 유사한 논리로 작동합니다: 참조 노드의 부모가 현재 노드와 다르면 작업을 중단합니다.
문제의 원인: 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 번역 사용 시 발생하는 오류를 방지함으로써 얻는 사용자 경험 개선 효과가 더 클 수 있습니다.