the node before which the new node is to be inserted is not a child of this node 에러를 해결해 보자
2025-06-02어느 날 외국 유저가 CS 채널에 문의를 남겼는데, 특정 페이지에서만 항상 이런 오류가 발생한다는 거였어요.
the node before which the new node is to be inserted is not a child of this node
문제 발견
처음엔 재현이 어려워서 정확한 원인을 파악하기가 쉽지 않았어요. 다양한 테스트 케이스를 시도해본 결과, 브라우저 자동 번역 기능이 활성화된 상태에서 해당 페이지로 접근했을 때 발생한다는 사실을 발견했어요.
현재 회사 플랫폼은 웹과 앱(웹뷰)로 구성돼 있고, 외국 유저는 모두 웹으로만 접근 가능해요. 아직 다국어를 지원하지 않아서 외국 유저는 한글을 그대로 보거나 브라우저 자동 번역을 써야 하거든요.
원인 분석
문제가 되는 페이지의 컴포넌트들을 하나하나 주석 처리하면서 분석한 결과, 문제의 코드를 찾았어요.
<span>{productValue} 개</span>
JSX에서 {productValue} 개처럼 표현식과 정적 문자열을 나란히 쓰면, React는 이 둘을 각각 별도의 자식으로 처리해요. 표현식 {productValue}는 동적으로 변할 수 있는 값이고, " 개"는 변하지 않는 리터럴이라서 React가 별도로 관리해야 할 두 개의 노드로 분리되는 거예요. 실제 DOM에서는 이렇게 렌더링돼요.
<span>
#text: "3"
#text: " 개"
</span>
브라우저 자동 번역 기능은 이 구조를 그대로 유지하지 않아요. "3 개"를 "3 pcs"로 번역할 때 두 텍스트 노드를 하나로 합치거나 기존 노드를 제거하고 새로운 노드를 삽입하는 방식으로 DOM을 직접 수정해요.
문제는 이 시점에서 React의 fiber tree와 실제 DOM이 어긋나기 시작한다는 거예요. React는 내부적으로 가상 DOM 트리(fiber tree)에 각 DOM 노드에 대한 참조를 유지하거든요. 번역 기능이 DOM을 변경하면 실제 DOM 구조가 달라지지만, React의 fiber tree는 여전히 번역 이전의 노드 참조를 들고 있어요. 이후 productValue가 바뀌거나 컴포넌트가 리렌더링되면, React는 fiber tree에 저장된 참조를 기준으로 DOM 조작을 시도하죠. 그런데 번역 과정에서 해당 노드가 이미 분리됐거나 다른 구조로 대체된 상태라, insertBefore 같은 DOM API가 "그 노드는 이 부모의 자식이 아니다"라는 DOMException을 던지게 되는 거예요.
해결 방법
해결 방법은 간단해요. 두 개로 나뉜 텍스트 노드를 React가 하나의 노드로 처리하게 만드는 거예요.
<!-- AS-IS: React가 두 개의 텍스트 노드로 분리 -->
<span>{productValue} 개</span>
<!-- TO-BE: React가 하나의 텍스트 노드로 처리 -->
<span>{`${productValue} 개`}</span>
템플릿 리터럴로 감싸면 표현식과 정적 문자열이 평가 시점에 하나의 문자열로 합쳐져요. React 입장에서는 단일 동적 표현식을 받는 것과 같으니까 DOM에 텍스트 노드를 하나만 생성하죠. 번역 기능도 이 텍스트를 하나의 덩어리로 인식해서 처리하고, React가 나중에 참조하는 노드도 변하지 않으니 충돌이 발생하지 않아요.
일반화된 해결 패턴
비슷한 구조를 다른 곳에서도 쓰고 있다면 같은 방식으로 예방할 수 있어요.
// 피해야 할 패턴
<div>{count}개</div>
<div>{price}원</div>
<div>{name}님</div>
// 권장 패턴
<div>{`${count}개`}</div>
<div>{`${price}원`}</div>
<div>{`${name}님`}</div>
사실 그동안 브라우저 자동 번역을 많이 썼지만, 이로 인해 React 내부 구조와 충돌이 생길 수 있다는 건 생각하지 못했어요. 다국어를 지원하지 않아서 외국 유저가 자동 번역에 의존하는 서비스라면, 번역 기능이 DOM을 직접 건드리는 방식으로 동작한다는 점을 염두에 두고 JSX를 작성하는 게 좋아요.
React fiber와 DOM 불일치가 오류로 이어지는 구조
이 오류가 왜 발생하는지 좀 더 깊이 이해하려면 React가 DOM을 관리하는 방식을 알아야 해요.
React는 컴포넌트 트리를 fiber 트리라는 내부 자료구조로 관리해요. 각 fiber 노드는 실제 DOM 노드에 대한 참조(stateNode)를 들고 있고, 리렌더링이 일어날 때 이 참조를 기준으로 DOM을 조작하죠.
fiber 트리 실제 DOM
───────────── ──────────────
<span> <span>
fiber(#text: "3") → #text: "3"
fiber(#text: " 개") → #text: " 개"
브라우저 번역 기능은 React 바깥에서 DOM을 직접 수정해요. "3 개"를 "3 pcs"로 번역할 때, 기존 두 텍스트 노드를 제거하고 새로운 단일 노드를 삽입하는 식이에요.
번역 후 실제 DOM
──────────────
<span>
#text: "3 pcs" ← 번역이 삽입한 새 노드
그런데 React의 fiber 트리는 여전히 이전 참조를 들고 있어요.
번역 후 fiber 트리 실제 DOM
───────────── ──────────────
<span> <span>
fiber(#text: "3") → (이미 DOM에서 제거됨)
fiber(#text: " 개") → (이미 DOM에서 제거됨)
이 상태에서 productValue가 바뀌어 리렌더링이 일어나면, React는 fiber 트리에 저장된 참조를 기준으로 DOM 조작을 시도해요. insertBefore를 호출할 때 "이 노드를 이 부모의 이 위치에 삽입해라"라고 하는데, 참조하는 노드가 이미 DOM에서 제거돼서 부모의 자식이 아닌 상태니까 오류가 나는 거죠.
비슷한 패턴 — 외부 DOM 조작이 React와 충돌하는 경우
같은 원리로 충돌이 발생할 수 있는 다른 상황들도 있어요.
jQuery 또는 바닐라 JS와 혼용
레거시 코드베이스에서 일부 기능은 jQuery로, 다른 부분은 React로 관리하는 경우예요. jQuery가 React가 관리하는 DOM 영역을 직접 수정하면 같은 문제가 생겨요.
// React가 관리하는 영역을 jQuery가 건드리면 위험
$('#react-root').find('.product-count').text('품절');
// React의 다음 리렌더링에서 fiber 참조와 DOM이 어긋남
서드파티 스크립트 (채팅봇, 분석 툴)
Intercom, Zendesk 같은 채팅 위젯이나 Google Optimize 같은 A/B 테스트 도구는 <body> 아래에 DOM 요소를 삽입하거나 기존 요소를 수정해요. React가 관리하는 영역과 겹치지 않으면 괜찮지만, 겹치면 예상치 못한 충돌이 생길 수 있어요.
이 패턴을 회피하는 방법
외부 DOM 조작이 불가피한 경우엔 React 컴포넌트 바깥에서 처리하거나, 해당 영역을 React가 관리하지 않는 독립된 DOM 노드로 분리하는 게 안전해요.
// React 컴포넌트 안에서 ref를 통해 명시적으로 관리 영역을 넘겨주는 방식
function ThirdPartyWidget() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (containerRef.current) {
// 서드파티 라이브러리가 이 컨테이너만 관리하도록
thirdPartyLib.init(containerRef.current);
return () => thirdPartyLib.destroy(containerRef.current);
}
}, []);
return <div ref={containerRef} />;
}
마치며
이번 버그가 흥미로웠던 건 원인이 "우리 코드"가 아니었다는 점이에요. JSX 문법, 브라우저 번역 기능, React 내부 구조 — 세 가지가 맞물려서 발생한 문제였거든요.
React가 DOM을 관리하는 방식(fiber 트리의 참조 기반 조작)을 이해하면, 비슷한 증상이 나타났을 때 "외부에서 DOM을 건드리는 무언가가 있지 않을까"라는 방향으로 빠르게 범위를 좁힐 수 있어요.