번역

왜 appendChild는 DOM 노드를 이 부모에서 저 부모로 이동시키는 걸까?

미래에서 온 개발자 2022. 11. 16. 18:54

들어가기에 앞서

이 포스팅은 inDepthDev의 아티클 Here is why appendChild moves a DOM node between parents를 번역한 것입니다. 번역에 오류가 있는 경우 댓글을 통해 알려주시면 신속하게 수정하도록 하겠습니다. 

 


나는 웹의 기본 원리를 아는 것이 무엇보다 중요하다고 믿는 사람이다. 그래서 종종 웹 개발 아키텍처나 웹 플랫폼 API에 관한 흥미로운 질문들을 하곤 한다. 이러한 질문을 던짐으로써 피면접자가 자신의 일에 대해 얼마나 열정을 가지고 있는 개발자인지, 지식에 대한 열망을 가지고 얼만큼 멀리 가봤는지를 이해하는 데 도움을 받는다. 

 

지난주 트위터에 다음과 같은 질문을 올렸다.  

 

아래와 같은 HTML 코드와 

<div class="a">
    <span></span>
</div>
<div class="b"></div>

 

appendChild 메소드를 사용하는 다음과 같은 자바스크립트 코드를 줬을 때

const span = document.querySelector(‘span’); 
const divB = document.querySelector(‘.b’); 
divB.appendChild(span);

 

자식 요소인 span은 : 

  • 계속 div A 아래에 있는다
  • div B로 복제(clone) 된다
  • div B로 이동한다

 

그랬더니 아래와 같은 꽤나 재밌는 결과를 볼 수 있었다. 

 

 

솔직히 내가 이제까지 이 질문을 던졌을 때 대부분의 개발자가 <span>이 복제될 거라고 생각한다는 걸 알아서 그렇게까지 놀라운 결과는 아니었다. 나는 면접 때 자주 이 질문을 하는데, 그러면 대개 두 번째 선택지를 고른다. 이 질문을 던지면 그 뒤에 숨겨진 논리를 볼 수 있다. 속기 딱 좋은, 까다로운 질문이다.

 

정답은 'span이 새로운 부모 요소인 div B로 이동한다'이다. 복제되는 것이 아니라 이동한다. 여러분은 그저 appendChild 메소드에 대한 MDN 페이지에 가서 공식 문서를 읽어보기만 해도 이를 알 수 있다. MDN에서는 다음과 같이 언급되어 있다. 

 

Node.appendChild() 메소드는 한 노드를 특정 부모 노드의 자식 노드 리스트 중 마지막 자식으로 붙입니다. 만약 주어진 노드가 이미 문서에 존재하는 노드를 참조하고 있다면 appendChild() 메소드는 노드를 현재 위치에서 새로운 위치로 이동시킵니다. (문서에 존재하는 노드를 다른 곳으로 붙이기 전에 부모 노드로부터 지워버릴 필요는 없습니다.)

 

여기까지 확인했으면 그만 멈출 수도 있겠지. 하지만 나는 뭐든지 더 깊이 파보는 걸 좋아하기 때문에 이 예제를 가지고 상세한 설명을 해 나가며 여러분에게 DOM의 개념에 대해 알려주고자 한다. 

 

 

DOM 노드

DOM 노드부터 시작해 보자. 여러분이 웹사이트를 둘러본다고 할 때 어떤 일들이 벌어질까? 브라우저는 요청을 보내고 응답을 받는데, 이 때 항상 HTML이 포함되어 있다. HTML은 그저 문서에 불과할 뿐인데, 우리는 어떻게 JavaScript에서 HTML 문서를 가지고 작업할 수 있을까? 자, 브라우저의 렌더링 엔진이 HTML을 파싱(parse)하고, HTML 태그에 대응하는 JavaScript 객체를 만든다. 바로 여기에서 DOM이라는 약자로 잘 알려진 'Document Object Model'이라는 이름이 유래한 것이다. HTML 태그가 하나 만들어지면 JavaScript 객체의 인스턴스도 하나가 생성된다. 

 

문제의 HTML을 다시 살펴 보자. 

 

<div class="a">
    <span></span>
</div>
<div class="b"></div>

 

브라우저는 HTMLDivElement 인스턴스 두 개와 HTMLSpanElement 인스턴스 한 개를 생성할 것이다. 여기에 각 div 요소에 해당하는 클래스명을 추가한다. 만약 여러분이 브라우저가 하는 작업을 따라한다면 다음과 같은 코드를 작성해 볼 수 있을 것이다. 

 

// <div class="a">
const divA = document.createElement('div');
divA.classList.add('a');

// <div class="b"></div>
const divB = document.createElement('div');
divB.classList.add('b');

// <span></span>
const span = document.createElement('span');

// a few checks
divA.className; // "a"
divB.className; // "b"
divA instanceof HTMLDivElement // true
divB instanceof HTMLDivElement // true
span instanceof HTMLSpanElement // true

 

 

노드 트리 (Node tree)

자, 이제 우리에게는 JavaScript 객체가 있다. 이 객체들은 메모리에 위치하고, 이러한 객체들을 노드(node)라고 부른다. 중요한 점은 이 노드들이 따로따로 있는 게 아니라 특별한 자료 구조(data structure)로 구성되어 있다는 사실인데, 이 자료 구조가 바로 트리(tree)이다. 

 

우리는 이러한 사실을 어떻게 알 수 있을까? 음, 개발자가 사용하는 대부분의 것들은 specification에 정의되어 있다. JavaScript는 EcmaScript spec에 정의되어 있고, 웹 플랫폼은 WHATWG에서 정의하고 있다. 

 

해당 스펙에 가서 읽어 보면 우리가 찾는 내용이 다음과 같이 명시되어 있다. 

 

(간단히 '노드'라고 부르는) 문서, DocumentType, DocumentFragment, 요소, 텍스트, ProcessingInstruction, 주석 객체는 노드 트리라는 이름의 트리에 들어가 한 무리가 된다. 

 

divspan은 둘 다 요소 노드(element node)라는 점을 기억해 두자. 이러한 계층 구조를 도식화해서 표현해 보면 다음과 같다. 

 

이제 spec에서 트리를 뭐라고 정의하는지 살펴 보자. 

 

트리란 유한한 계층적 자료 구조이다. [...] 트리에 들어가 한 무리가 되는 객체는 하나의 부모를 가지며, 이 때 부모는 null이거나 또다른 객체이고, 여러 자식을 가지고 있다. [...] 

 

웹에서는 문서 트리(document tree)라고 부르는 노드 트리의 특정한 유형이 쓰이는데,

 

문서 트리란 노드 트리의 루트(root)가 문서인 노드 트리이다. 

 

이제 우리는 Document Object Model(DOM)에서 문서(document)가 어떻게 유래하게 되었는지를 알게 되었다. 

 

 

문서 트리 만들기

우리는 앞서 DOM 노드를 각각 따로따로 만들어 두었다. 우리는 이제 이 노드들이 트리에 들어가게 할 수 있다. div 요소 두 개를 document에 추가하고, span 요소를 DivA에 추가하면 된다.

 

document.appendChild(divA);
document.appendChild(divB);
divA.appendChild(span);

 

위의 코드를 트리 구조로 그려보면 다음과 같다. 

 

 

내가 처음 했던 질문에서 하고자 했던 것은 노드를 다른 부모 요소 노드로 이동하게 하는 것이었다. 

 

 

다음과 같이 appendChild 메소드를 쓰면 된다.

 

divB.appendChild(span);

 

 

왜 노드가 복제되지 않는 걸까? 

노드가 복제될 거라고 생각할 법도 하다. 그러나 이러한 시나리오대로 진행될 수 없는 몇 가지 이유가 있다. 

 

  • appendChild 메소드를 통해 노드가 복제되는 경우, 노드에 대한 직접 참조 없이 노드가 생성된다. 
const span = document.querySelector(‘span’); 
const divB = document.querySelector(‘.b’);
divB.appendChild(span);

 

위의 코드에서 노드가 복제된다면 변수 span은 복제된 인스턴스와 원본 인스턴스 둘 다를 가리키게 되기 때문에 원본 DOM 노드 인스턴스와 새로운 DOM 노드 인스턴스에 대한 참조를 모두 잃는다. 

 

  • 이동될 요소에 자식 트리가 아주 많이 중첩되어 있는 경우 수행할 작업이 명확하지가 않다. 중첩된 자식들도 복제되어야 할까? 객체의 깊은 복제는 매우 많은 비용이 들고 굉장히 복잡한 작업이며, 특히 원형 참조(circular reference)가 관련된 경우에는 더욱 그렇다. 

  • 노드를 복제하면 ID가 이중으로 생기는 문제가 발생할 수 있다. 

 

 

왜 노드는 두 부모의 자식이 되지 않을까? 

자, 트리의 정의로 다시 돌아가서 읽어보면 해답을 찾을 수 있다. 

 

 [...] 트리에 들어가 한 무리가 되는 객체는 하나의 부모를 가지며, 이 때 부모는 null이거나 또다른 객체이고, 여러 자식을 가지고 있다. [...] 

 

따라서 루트 노트를 제외하고 트리에 있는 모든 노드는 부모라고 불리는 상위 노드에 대해 정확히 단 하나의 연결을 갖는다. 많은 부모를 갖는 게 아니라 0 또는 1인 것이다. 

 

이는 곧 우리가 노드를 다른 부모로 이동시키고 싶다면 먼저 기존 부모에게서 해당 노드를 삭제해야 한다는 뜻이다. 노드는 두 개의 부모를 가질 수 없기 때문이다!