글 목록

the node before which the new node is to be inserted is not a child of this node 에러를 해결해 보자

2025년 6월 2일 5분 읽기
디버깅

어느 날 외국인 사용자가 CS 채널에 문의를 남겼습니다. 특정 페이지에서만 항상 이런 오류가 발생한다는 거였습니다.

code
the node before which the new node is to be inserted is not a child of this node

재현이 안 되는 버그였다

처음엔 재현이 어려워서 정확한 원인을 파악하기가 쉽지 않았습니다. 같은 페이지에 몇 번을 들어가 봐도 제 화면에서는 아무 일도 일어나지 않았으니까요.

여러 조건으로 테스트한 끝에 재현 방법을 찾았습니다. 브라우저 자동 번역을 켜고 해당 페이지에 접근하면 오류가 발생했습니다.

현재 회사 플랫폼은 웹과 앱(웹뷰)로 구성돼 있고, 외국인 사용자는 모두 웹으로만 접근 가능합니다. 아직 다국어를 지원하지 않아서 외국인 사용자는 한글을 그대로 보거나 브라우저 자동 번역을 써야 합니다. 저에게는 없던 조건이 그들에게는 기본값이었던 겁니다.

컴포넌트를 하나씩 주석 처리하다 문제의 코드를 찾았다

해당 페이지의 컴포넌트를 하나씩 주석 처리하며 범위를 좁혔습니다. 오류를 일으키는 코드는 다음과 같았습니다.

html
<span>{productValue} 개</span>

JSX에서 {productValue} 개처럼 표현식과 정적 문자열을 나란히 쓰면, React는 이 둘을 각각 별도의 자식으로 처리합니다. 표현식 {productValue}는 동적으로 변할 수 있는 값이고, " 개"는 변하지 않는 리터럴이라서 React가 별도로 관리해야 할 두 개의 노드로 분리되는 겁니다. 실제 DOM에서는 이렇게 렌더링됩니다.

code
<span>
  #text: "3"
  #text: " 개"
</span>

브라우저 자동 번역 기능은 이 구조를 그대로 유지하지 않습니다. "3 개"를 "3 pcs"로 번역할 때 두 텍스트 노드를 하나로 합치거나 기존 노드를 제거하고 새로운 노드를 삽입하는 방식으로 DOM을 직접 수정합니다.

이때 React의 fiber 트리에 남아 있는 노드 참조와 실제 DOM이 어긋납니다. 이후 productValue가 바뀌면 React는 이전 참조를 기준으로 DOM을 수정하려 합니다. 하지만 해당 노드는 번역 과정에서 이미 분리되거나 교체된 상태입니다. 그래서 insertBefore 같은 DOM API가 "그 노드는 이 부모의 자식이 아니다"라는 DOMException을 던집니다.

템플릿 리터럴 하나로 끝났다

해결 방법은 간단합니다. 두 개로 나뉜 텍스트 노드를 React가 하나의 노드로 처리하게 만드는 겁니다.

html
<!-- AS-IS: React가 두 개의 텍스트 노드로 분리 -->
<span>{productValue} 개</span>

<!-- TO-BE: React가 하나의 텍스트 노드로 처리 -->
<span>{`${productValue} 개`}</span>

템플릿 리터럴로 감싸면 표현식과 정적 문자열이 평가 시점에 하나의 문자열로 합쳐집니다. React 입장에서는 단일 동적 표현식을 받는 것과 같으니까 DOM에 텍스트 노드를 하나만 생성하죠. 번역 기능도 이 텍스트를 하나의 덩어리로 인식해서 처리하고, React가 나중에 참조하는 노드도 변하지 않으니 충돌이 발생하지 않습니다.

비슷한 JSX도 함께 점검할 수 있다

비슷한 구조를 다른 곳에서도 쓰고 있다면 같은 방식으로 예방할 수 있습니다.

tsx
// 피해야 할 패턴
<div>{count}개</div>
<div>{price}원</div>
<div>{name}님</div>

// 권장 패턴
<div>{`${count}개`}</div>
<div>{`${price}원`}</div>
<div>{`${name}님`}</div>

사실 그동안 브라우저 자동 번역을 많이 썼지만, 이로 인해 React 내부 구조와 충돌이 생길 수 있다는 건 생각하지 못했습니다. 다국어를 지원하지 않아서 외국인 사용자가 자동 번역에 의존하는 서비스라면, 번역 기능이 DOM을 직접 건드리는 방식으로 동작한다는 점을 염두에 두고 JSX를 작성하는 게 좋습니다.


React는 자기가 기억하는 노드를 찾아간다

이 오류가 왜 발생하는지 좀 더 깊이 이해하려면 React가 DOM을 관리하는 방식을 알아야 합니다.

React는 컴포넌트 트리를 fiber 트리라는 내부 자료구조로 관리합니다. 각 fiber 노드는 실제 DOM 노드에 대한 참조(stateNode)를 들고 있고, 리렌더링이 일어날 때 이 참조를 기준으로 DOM을 조작하죠.

code
fiber 트리                   실제 DOM
─────────────                ──────────────
<span>                       <span>
  fiber(#text: "3")    →       #text: "3"
  fiber(#text: " 개")  →       #text: " 개"

브라우저 번역 기능은 React 바깥에서 DOM을 직접 수정합니다. "3 개"를 "3 pcs"로 번역할 때, 기존 두 텍스트 노드를 제거하고 새로운 단일 노드를 삽입하는 식입니다.

code
번역 후 실제 DOM
──────────────
<span>
  #text: "3 pcs"   ← 번역이 삽입한 새 노드

그런데 React의 fiber 트리는 여전히 이전 참조를 들고 있습니다.

code
번역 후 fiber 트리           실제 DOM
─────────────                ──────────────
<span>                       <span>
  fiber(#text: "3")  → (이미 DOM에서 제거됨)
  fiber(#text: " 개") → (이미 DOM에서 제거됨)

이 상태에서 productValue가 바뀌어 리렌더링이 일어나면, React는 fiber 트리에 저장된 참조를 기준으로 DOM 조작을 시도합니다. insertBefore를 호출할 때 "이 노드를 이 부모의 이 위치에 삽입해라"라고 하는데, 참조하는 노드가 이미 DOM에서 제거돼서 부모의 자식이 아닌 상태니까 오류가 나는 거죠.

바깥에서 DOM을 건드리는 건 전부 같은 위험을 안는다

같은 원리로 충돌이 발생할 수 있는 다른 상황들도 있습니다.

jQuery가 React의 영역에 손을 댈 때

레거시 코드베이스에서 일부 기능은 jQuery로, 다른 부분은 React로 관리하는 경우입니다. jQuery가 React가 관리하는 DOM 영역을 직접 수정하면 같은 문제가 생깁니다.

javascript
// React가 관리하는 영역을 jQuery가 건드리면 위험
$('#react-root').find('.product-count').text('품절');
// React의 다음 리렌더링에서 fiber 참조와 DOM이 어긋남

채팅 위젯과 분석 툴도 결국 같은 일을 한다

Intercom, Zendesk 같은 채팅 위젯이나 Google Optimize 같은 A/B 테스트 도구는 <body> 아래에 DOM 요소를 삽입하거나 기존 요소를 수정합니다. React가 관리하는 영역과 겹치지 않으면 괜찮지만, 겹치면 예상치 못한 충돌이 생길 수 있습니다.

관리 영역을 명확히 갈라주면 된다

외부 DOM 조작이 불가피한 경우엔 React 컴포넌트 바깥에서 처리하거나, 해당 영역을 React가 관리하지 않는 독립된 DOM 노드로 분리하는 게 안전합니다.

tsx
// 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을 건드리는 무언가가 있지 않을까" 하는 방향으로 말이죠.

재현이 안 되는 버그를 만난다면, 내 환경과 사용자 환경이 어떻게 다른지부터 확인해보면 좋겠습니다. 저는 자동 번역 사용 여부가 그런 차이를 만들 줄은 생각하지 못했습니다.