DOM(Document Object Model)에 대하여

taeheeyoon·2022년 5월 25일
62

JavaScript

목록 보기
6/7

시작하며

안녕하세요.
최근 아래 사진과 같이 자바스크립트를 습득하지 않은 채 바로 React 같은 라이브러리나 프레임워크부터 배우는 분들이 적지 않다는 것을 알았습니다.

그래서 공부하시는 분들께 저의 지식이 조금이나마 도움이 돼면 좋을 것 같아 글을 쓰고자합니다.
먼저 이 글은 HTMLCSS에 대한 기초적인 지식이 있다는 가정하에 설명을 드립니다.

DOM은 프로그래머 관점에서 바라 본 HTML 입니다. DOM을 이해하고 조작할 수 있으면, HTML을 단순한 문서에서 웹 앱으로 업그레이드할 수 있습니다. DOM은 브라우저 환경에서 자바스크립트를 이용해 HTML을 조작할 수 있습니다. HTML 문서에 이미 작성되어 있는 엘리먼트에 접근하거나, 새로운 엘리먼트를 생성 또는 삭제할 수 있습니다.

DOM에 대하여


HTML과 DOM의 구조

DOM은 Document Object Model의 약자로, HTML 요소를 Object(JavaScript Object)처럼 조작(Manipulation)할 수 있는 Model입니다. 즉, 자바스크립트를 사용할 수 있으면, DOM으로 HTML을 조작할 수 있습니다.

그럼 HTML을 조작 한다는 건 무엇을 뜻할까요? 아시다시피 HTML로만 이루어진 페이지는 정적입니다. 하지만 DOM으로 HTML을 조작하면 HTML로 구성된 웹 페이지를 동적으로 움직이게 만들 수 있습니다.

Node와 Element는 무엇이 다른가

DOM 요소를 조작하다 보면 노드와 엘리먼트라는 말이 자주 등장하는데 시작하기 전에 앞서 둘의 차이점을 제대로 알고 넘어가면 좋을 것 같습니다.
많은 개발자들이 이를 혼동하고 헷갈려하고 있습니다.

Node는 DOM 계층구조에 속한 객체의 어떤 타입이든 가리킬 수 있는 이름입니다.
Node는 내장 DOM 엘리먼트(document, document.body)나 HTML의 특정 태그(input, p) 또는 텍스트 노드가 될 수도 있습니다. 간단히 얘기해서 Node는 아무 DOM 객체나 될 수 있다는 거죠.
Element는 Node의 하나의 타입인데요. 노드에는 정말 많은 타입이 있습니다... (텍스트 노드, 주석 노드, Document 노드 기타 등등) Element는 HTML tag로 바로 특정할 수 있거나, id나 class 같은 속성을 가진 것들입니다.
각 노드들은 .nodeType이라는 프로퍼티를 갖고 있는데, 이게 바로 그 노드가 어떤 타입인지 말해 주죠. ELEMENT_NODE 타입은 nodeType 프로퍼티 값이 1인 특정 타입입니다.

간단히 말해서 Node의 여러 타입 중 하나가 Element이며 Element는 Node보다 더 작은 개념이라고 생각하시면 됩니다.

HTML에 JavaScript 적용하기

그럼 HTML에서 자바스크립트는 어떻게 사용할 수 있을까요?
HTML에 JavaScript를 적용하기 위해서는 <script> 태그를 이용합니다. 아래의 경우 HTML 파일과 같은 디렉토리에 존재하는 myScriptFile.js을 불러옵니다.

<script src="myScriptFile.js"></script>

웹 브라우저가 작성된 코드를 해석하는 과정에서 <script> 요소를 만나면, 웹 브라우저는 HTML 해석을 잠시 멈춥니다. HTML 해석을 잠시 멈춘 웹 브라우저는 <script> 요소를 먼저 실행합니다.
웹 브라우저는 script 요소를 만나면 HTML 해석을 일시정지 합니다.

<script> 태그를 추가하는 두 가지 대표적인 사례가 존재합니다. 하나는 <head> 태그에 추가하는 방법, 다른 하나는 </body> 가 끝나기 전에 추가하는 방법입니다. 두 사례를 비교해보고 차이점은 무엇인지 알아봅시다.

1. <head> 안쪽에 삽입하는 경우

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <!-- script 요소 삽입 위치 -->
    <script src="myScriptFile.js"></script>
  </head>
  <body>
    <div id="msg">Hello JavaScript!</div>
  </body>
</html>

2. <body> 태그가 끝나기 전에 삽입하는 경우

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="msg">Hello JavaScript!</div>
    <!-- script 요소 삽입 위치 -->
    <script src="myScriptFile.js"></script>
  </body>
</html>

두 방식의 공통점과 차이점

두 방식 모두 myScriptFile.js 내의 첫 번째 console.log를 성공적으로 출력하지만,
아래에 위치한 console.log의 경우 제대로 출력하지 못하는 예시가 있습니다.

console.log('welcome JavaScript');

let msgElement = document.querySelector('#msg');
console.log(msgElement);

각 방법의 콘솔 출력 방법이 어떻게 다른지 눈치채셨나요?
아마 1번 방법대로 head 안쪽에 sctipt 요소를 삽입하게되면 null이 출력될텐데요.
그 이유는 자바스크립트 코드가 html를 파싱하여 dom 노드를 구축하기 전에 id값이 msg인 엘리먼트를 찾고 있어서입니다.
이 부분이 아직 이해가 안간다면 제가 이전에 작성한 글 웹 브라우저의 동작원리를 알아보자에서 확인하실 수 있습니다.
이러한 이유 때문에 DOM 조작을 하기위해서는 body 태그 하단에 스크립트 코드를 위치시키는 것이 좋습니다.

HTML이 JavaScript에서는 어떻게 표현될까?

혹시 다음 HTML 파일을 보고 아래 질문에 답하실 수 있으신가요?

  • body 엘리먼트의 자식 엘리먼트(element)는 총 몇 개인가요?
  • id의 이름이 news-contentsdiv 엘리먼트의 부모 엘리먼트는 무엇인가요?
  • id의 이름이 navdiv엘리먼트 를 포함해서, 모든 자식 엘리먼트의 class 이름을 console.log를 사용하여 확인하는 방법을 수도코드(pseudocode)로 작성하세요.
<html>
  <body>
    <div id="nav">
      <div class="logo"></div>
      <div class="menu-wrapper">
        <div class="menu"></div>
        <div class="menu"></div>
        <div class="menu"></div>
        <div class="profile-photo"></div>
      </div>
    </div>
    <div id="news-contents">
      <div class="news-content-wrapper">
        <div class="news-picture"></div>
        <div class="news-title"></div>
        <div class="news-description"></div>
      </div>
    </div>
    <div id="footer"></div>
  </body>
</html>

body 엘리먼트의 자식 엘리먼트(element)는 총 몇 개인가요?

body 엘리먼트의 자식 엘리먼트는 총 3개입니다. id가 nav, news-contents, footer 인 3가지 엘리먼트입니다.

그림으로 표현하면, 쉽게 이해할 수 있습니다. 그러나 컴퓨터에게는 이 사실을 어떻게 전달할까요? 자바스크립트에서 DOM은 document 객체에 구현되어 있습니다. 브라우저에서 작동되는 자바스크립트 코드에서는, 어디에서나 document 객체를 조회할 수 있습니다. 한번, body를 찾아보겠습니다.

DOM 구조를 조회할 때에는 console.dir 이 유용합니다. console.dirconsole.log 와 달리 DOM을 객체의 모습으로 출력합니다. 앞으로 console.logconsole.dir 의 차이를 구분해서 사용할 일이 계속 있습니다. 🙂

console.dir 을 이용해 document.body를 조회해보니, 너무나 많은 속성이 나타납니다. 앞서 학습한 내용을 상기해보면, HTML 엘리먼트에 지정할 수 있었던 다양한 속성이 이미 객채 내에 존재한다고 생각하면 됩니다.

문제에 대한 답을 찾기 위해, 찾고자 하는 자식 엘리먼트를 찾아보세요. 이번에는 힌트가 있습니다. document.body 객체의 키 중에서 children을 찾아볼까요?

console.dir(document.body) 를 통해 출력된 객체에서, children 속성을 찾을 수 있습니다. children 속성에 nav, news-contents, footer 가 자식으로 있는 것을 확인할 수 있습니다. 물론 document.body.children 으로 바로 조회할 수도 있습니다.

id의 이름이 news-contents 인 div 엘리먼트의 부모 엘리먼트는 무엇인가요?


id가 news-contents 인 div 엘리먼트는 body 엘리먼트의 자식 엘리먼트입니다. 반대로 body 엘리먼트는 id가 news-contents div 엘리먼트의 부모 엘리먼트입니다. 이 관계를 자바스크립트에서 확인합니다. id가 news-contents 인 엘리먼트를 조회하려면, document.body.children 의 첫 번째 엘리먼트를 조회합니다.

document.body 의 children을 조회할 때마다, 매번 document.body 로부터 찾아가는 일은 정말 번거롭습니다. 따로 변수 선언을 해서 이 정보를 저장해두면, 주소를 참조하기때문에 언제든지 접근할 수 있습니다. 변수 newsContents 를 따로 선언하여 id가 news-contents 인 엘리먼트를 할당합니다.

자바스크립트로 newsContents의 부모 엘리먼트를 가리키는 속성은 무엇일까요?
Node.parentElement는 부모의 엘리먼트를 반환합니다. 만약 부모 엘리먼트가 없거나 부모가 DOM 엘리먼트가 아니라면 null을 반환합니다. 따라서 parentElement의 반환값은 언제나 DOM 엘리먼트이거나 null입니다.

여기서 주의할 점은 document.documentElement의 부모는 document인데, DOM 트리에는 document 엘리먼트라는 건 없기 때문에 null를 반환합니다.
대신 document는 그냥 DOM 객체(document node)로서 존재하게됩니다. 이것은 HTML 문서 최상위 요소인 html에 적용해도 같은 결과가 나옵니다.

아래 표에서 nodeType별로 type-value의 값을 확인해보면 HTML 엘리먼트는 ELEMENT_NODE에 해당하기때문에 1을 반환했고 HTML의 부모는 DOCUMENT_TYPE_NODE기 때문에 Node.parentNode에서는 9를 반환하고 Node.parentElement에서는 엘리먼트가 아니기 때문에 null을 반환한 것을 확인할 수 있습니다.

id의 이름이 nav인 div 엘리먼트를 포함해서, 모든 자식 엘리먼트의 class 이름을 console.log를 사용하여 확인하는 방법을 수도코드(pseudocode)로 작성하세요.

DOM 구조도를 살펴보면, 다음과 같은 조직도가 떠오르지 않나요?

DOM 구조에서도 회사의 조직도와 유사한 모습을 발견할 수 있습니다. body가 가장 상위에 있고, 아래에 여러 구성요소가 부모-자식 관계를 가지고 있습니다. 이 관계를 그려보면 아래와 비슷한 구조가 만들어집니다.

이런 자료 구조를 컴퓨터 공학에서는 트리 구조라고 합니다. 트리 구조의 가장 큰 특징은 부모가 자식을 여러 개 가지고, 부모가 하나인 구조가 반복되는 점입니다. 즉, 부모가 가진 하나 또는 여러 개의 자식 엘리먼트를 조회하는 코드를 작성한다면, 여러 번 반복해서 실행하는 코드가 필요합니다.

아래는 엘리먼트를 조회하는 코드를 작성하기 위한 수도코드입니다.

function consoleLogAllElement(element){
// nav의 class 이름을 console.log 합니다.
// nav의 자식 엘리먼트가 있는지 검색합니다. (logo, menu-wrapper)
  //logo의 class 이름을 console.log 합니다.
  //logo의 자식 엘리먼트가 있는지 검색합니다. (없음)
  //menu-wrapper의 class 이름을 console.log 합니다.
  //menu-wrapper의 자식 엘리먼트가 있는지 검색합니다. (menu, menu, menu, profile-photo)
    //첫 번째 menu의 class 이름을 console.log 합니다.
    //첫 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
    //두 번째 menu의 class 이름을 console.log 합니다.
    //두 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
    //세 번째 menu의 class 이름을 console.log 합니다.
    //세 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
    //profile-photo의 class 이름을 console.log 합니다.
    //profile-photo의 자식 엘리먼트가 있는지 검색합니다 (없음)
 //자식 엘리먼트를 모두 탐색했음으로, 함수 실행이 종료됩니다.
//자식 엘리먼트를 모두 탐색했음으로, 함수 실행이 종료됩니다.
}

DOM으로 HTML 조작하기

document 객체에는 많은 속성과 메소드가 존재합니다. 모든 속성과 메소드를 외워야 할까요? 지금 당장 전부 알아야 할 필요는 없습니다. 지금 집중할 부분은 CRUD(Create, Read, Update and Delete) 입니다. 앞으로 여러분이 앞으로 어떤 종류의 개발이나 컴퓨터 언어를 배우시더라도 가장 먼저 CRUD에 집중해야 합니다. CRUD를 먼저 이해하는 것이 새로운 언어를 가장 빠르게 학습하는 방법입니다.

document 객체를 통해서 HTML 엘리먼트를 만들고(CREATE), 조회하고(READ), 갱신하고(UPDATE), 삭제하는(DELETE) 하는 방법을 학습합니다. DOM에는 HTML에 적용(APPEND)하는 메소드가 따로 있으니 주의해주세요!

CREATE - createElement

document 객체의 createElement 메소드를 이용하여 div element를 만듭니다.

const newDiv = document.createElement('div')

해당 코드를 입력하면 아무런 변화도 일어나지 않습니다. 왜 그럴까요? newDiv라는 엘리먼트는 현재 공중부양 중입니다. 다음 그림을 보며 설명합니다. DOM의 구조를 나타내는 트리 구조를 하나 그립니다. 아무것도 연결이 되어있지 않은 하나의 노드를 그립니다.

위 그림처럼 공중에 떠있는 엘리먼트를 확인하기 위해서는 APPEND 해야합니다. APPEND 를 이용해 실제 웹 페이지 상에도 보이는 것을 확인할 수 있습니다. 이어지는 APPEND에서, newDiv를 트리 구조에 연결합니다.

APPEND - append, appendChild

CREATE에서 만든 newDiv 라는 변수는 아직 "공중부양"을 하고 있습니다.
이번에는 append 라는 메소드를 사용해서, 변수 newDivcontainer 에 넣어보겠습니다.

document.body.container.append(newDiv)

크롬의 개발자도구 Elements 탭에서 내용을 확인하면 새로운 div 엘리먼트가 추가된 것을 확인 할 수 있습니다.

같은 엘리먼트를 appendChild 하면, 기존 엘리먼트를 복사할까?

아래와 같은 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로 복제된다.
  • div B로 이동한다.

개발자 433명이 이 설문에 답했는데요. 결과는 꽤나 흥미로웠습니다. 많은 개발자가 span이 복제될 것이라고 생각했습니다. 정답은 span은 복제되지 않고 새로운 상위 div B로 이동합니다.

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

위 HTML 문서는 다음과 같은 트리로 구성됩니다.

아래 트리 그림처럼 div A에 있는 span을 div B로 옮기고 싶습니다.

appendChild 메소드 사용하게 되면 span 엘리먼트가 div A에서 div B로 이동합니다.

divB.appendChild(span);

노드가 복제되지 않은 이유는 무엇일까요?

그러면 많은 사람들의 예상과 달리 노드가 복제되지 않은 이유는 무엇일까요?
appendChild를 통해 노드를 복제하면 직접 참조가 없는 노드가 생성됩니다.

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

위의 코드에서 노드가 복제되면 span 엘리먼트는 복제된 인스턴스나 원래 인스턴스를 가리킬 것이므로 DOM 노드 인스턴스에 대한 참조를 잃게 됩니다.

  • appendChild한 엘리먼트에 매우 깊게 중첩된 하위 트리가 있는 경우 수행할 작업이 명확하지 않습니다. 하위 트리까지 전부 복제를 해야될까요? 객체 깊은 복사는 매우 비싸고(메모리관점에서) 매우 복잡합니다.

  • 노드를 복제하면 ID가 중복될 수 있습니다.

노드가 두 부모의 참조하는 자식이 되지 않는 이유는 무엇일까요?

기본적으로 node는 트리 구조를 가지고 있습니다.
루트 노드를 제외한 트리의 모든 노드는 상위 노드에 대해 단 하나의 부모를 가지고 있습니다.
즉, 노드를 다른 부모로 이동하려면 노드가 두 부모를 가질 수 없기 때문에 먼저 이전 부모에서 삭제해야 합니다.

READ - querySelector, querySelectorAll

자바스크립트에서 원시 자료형인 변수의 값을 조회하기 위해서는, 변수의 이름으로 직접 조회할 수 있습니다. 참조 자료형인 배열은 index를, 객체는 key를 이용해 값을 조회할 수 있습니다. 그러나 DOM은 조금 특별한 방법을 사용해야 합니다. DOM으로 HTML 엘리먼트의 정보를 조회하기 위해서는 querySelector의 첫 번째 인자로 셀렉터(Selector)를 전달하여 확인할 수 있습니다. 셀렉터로는 HTML 태그("div"), id("#only-one"), class(.group) 세 가지가 가장 많이 사용됩니다.

querySelector

querySelector 에 '.group' 을 첫 번째 인자로 넣으면, 클래스 이름이 group 인 HTML 엘리먼트 중 첫 번째 엘리먼트를 조회할 수 있습니다.

const oneGroup = document.querySelector('.group')

만약 HTML 문서에는 클래스 이름이 group 인 엘리먼트가 여러 개 있으면, 변수 oneGroup 에 할당된 엘리먼트는 단 하나입니다. 여러 개의 엘리먼트를 한 번에 가져오기 위해서는, querySelectorAll 을 사용합니다. 이렇게 조회한 HTML 엘리먼트들은 배열처럼 for문을 사용하실 수 있습니다. 주의하세요! 앞서 조회한 HTML 엘리먼트들은 배열이 아닙니다! 이런 '배열 아닌 배열'을 유사 배열, 배열형 객체 등 다양한 이름으로 부릅니다. 정식 명칭은 Array-like Object 입니다. Array-like Object 같이 개념을 설명하는 용어는 영어로도 명확하게 기억해두는 게 좋습니다.

const Allgroup = document.querySelectorAll('.group')

querySelector와 querySelectorAll 만 알아도 대부분의 엘리먼트를 조회할 수 있습니다.
하지만 레거시 코드를 살펴볼때 아래와 같이 get으로 시작하는 DOM 조회 메소드를 볼 수도 있습니다.

const getContainer = document.getElementById('container')
const queryContainer = document.querySelector('#container')
console.log(getContainer === queryContainer) // true

JavaScript에서 NodeList(유사배열)를 배열로 변환하는 방법

HTML 엘리먼트는 앞서 말했 듯 유사배열 입니다. 배열과 비슷하지만 map(), .filter(), slice(), 와 같은 일반적인 배열 메서드를 사용할 수 없습니다. 유사 배열을 배열로 변환하는 3가지의 방법을 소개해드리겠습니다.

Array.from() 방법

최신 JavaScript(ES6)에서 유사배열을 배열로 변환하는 가장 간단하고 쉬운 방법은 Array.from() 메서드를 사용하는 것입니다.

// create a `NodeList` object
const divs = document.querySelectorAll('div');

// convert `NodeList` to an array
const divsArr = Array.from(divs);

스프레드 연산자

또 다른 방법은 스프레드 연산자를 사용하는 것입니다.
마찬가지로 최신 JavaScript(ES6)의 스프레드 연산자는 배열로 변환하는 간결하고 멋진 방법입니다.

// create a `NodeList` object
const divs = document.querySelectorAll('div');

// convert `NodeList` to an array
const divsArr = [...divs];

Array.prototype.slice() 방법

마지막으로 배열로 변환하는 마지막 방법은 Array.prototype.slice() 메서드를 사용하는 것입니다.

// create a `NodeList` object
const divs = document.querySelectorAll('div');

// convert `NodeList` to an array
const divsArr = Array.prototype.slice.call(divs);

위 방법의 간결한 형식을 사용할 수도 있습니다.

const divsArr = [].slice.call(divs);

위의 모든 방법은 JavaScript 배열을 제공하며 forEach()를 사용하여 반복하고 원하는 모든 배열 메소드를 사용하여 다른 작업을 수행할 수 있습니다.

UPDATE - textContent, classList.add

앞에서 CREATE, APPEND, READ를 통해 새로운 DOM 객체를 만들고, 기존의 DOM 객체에 붙이고, DOM 객체를 선택해서 조회하는 방법을 학습했습니다. 이번 콘텐츠 UPDATE에서는 기존에 생성한 빈 div 태그를 업데이트하여, 보다 다양한 작업을 할 수 있습니다. textContent 를 사용해서, 비어있는 div 엘리먼트에 문자열을 입력합니다.

console.log(oneDiv) // <div></div>
oneDiv.textContent = 'dev';
console.log(oneDiv) // <div>dev</div>

앞서 생성한 div 엘리먼트를 container에 append 했을 때, CSS 스타일링이 적용되지 않았습니다. group 클래스의 CSS가 정의되어 있다고 가정하고 CSS 스타일링이 적용될 수 있도록, div 엘리먼트에 class를 추가합니다.

oneDiv.classList.add('group')
console.log(oneDiv) // <div class="group">dev</div>

생성한 엘리먼트에 텍스트를 채웠고, 클래스를 추가하여 스타일링을 적용했습니다. 이번에는 append를 이용해 container의 자식 요소로 추가합니다.

const container = document.querySelector('#container')
container.append(oneDiv)

새롭게 추가한 엘리먼트는 클래스 group 의 스타일이 적용된 상태로 출력됩니다.
classList 외에도 setAttrubute라는 메소드를 사용하면 더 많은 attrubute를 추가할 수 있습니다.

DELETE - remove, removeChild

CRUD의 Delete, 삭제하는 법을 학습합니다. 삭제하는 방법에도 여러 가지가 있습니다. 먼저 삭제하려는 엘리먼트의 위치를 알고 있는 경우에 사용하는 방법입니다. 앞서 생성하고 추가한 newDiv 를 삭제합니다. remove 메소드를 사용하세요.

const container = document.querySelector('#container')
const newDiv = document.createElement('div')
container.append(newDiv)
newDiv.remove() // 이렇게 append 했던 엘리먼트를 삭제할 수 있습니다.

만약 이렇게 하나하나 엘리먼트를 삭제해주는 것이 아닌 여러 개의 자식 엘리먼트를 지우려면, 어떻게 해야 할까요? innerHTML 을 이용하면, 아주 간단하게 모든 자식 엘리먼트를 지울 수 있습니다. 컨테이너의 모든 자식 엘리먼트를 지우려면, 다음과 같이 입력합니다.

//id가 container인 엘리먼트 아래의 모든 엘리먼트를 지웁니다.
document.querySelector('#container').innerHTML = '';

innerHTML 을 이용하는 방법은 분명 간편하고 편리한 방식이지만, innerHTML은 보안에서 몇 가지 문제를 가지고 있습니다. 이 방법을 대신할 다른 메소드를 사용합니다. removeChild 는 자식 엘리먼트를 지정해서 삭제하는 메소드입니다. 모든 자식 엘리먼트를 삭제하기 위해, 반복문(while, for, etc.)을 활용할 수 있습니다. 다음의 코드는 자식 엘리먼트가 남아있지 않을 때까지, 첫 번째 자식 엘리먼트를 삭제하는 코드입니다.

//container의 첫 번째 자식 엘리먼트가 존재하면, 첫 번째 자식 엘리먼트를 삭제합니다.
const container = document.querySelector('#container');
while (container.firstChild) {
  container.removeChild(container.firstChild);
}

removeChildwhile 을 이용해 자식 엘리먼트를 삭제하면, container의 하위 엘리먼트가 전부 사라집니다. 이걸 원치 않으면 반복문이 끝난 뒤에 새롭게 추가할 수도 있습니다. 또는 자식 엘리먼트를 하나만 남기게 할 수도 있습니다.

// container의 자식 엘리먼트가 1개만 남을 때까지, 마지막 자식 엘리먼트를 삭제합니다.
const container = document.querySelector('#container');
while (container.children.length > 1) {
  container.removeChild(container.lastChild);
}

또는 직접 클래스 이름이 Group인 엘리먼트만 찾아서 지우는 방법도 있습니다.

//클래스 이름이 group인 엘리먼트만 찾아서 삭제합니다.
const AllGroup = document.querySelectorAll('.group')
AllGroup.forEach(function(group){
    group.remove();
})
// or
for (let group of AllGroup){
    group.remove()
}

노드는 실제로 메모리 상에서 삭제될까?

엘리먼트를 삭제하는 것이 정말 메모리상에서 바로 삭제된다고 있기 때문에 혼동될 수 있습니다.
하지만 실제로는 바로 삭제되지 않습니다.

삭제의 개념은 기본적으로 자식와 부모 사이의 관계를 끊는 것을 의미합니다. 그냥 분리될 뿐 입니다.

이벤트 객체

웹 사이트를 서핑하다 보면, 이미지나 카드를 클릭하거나 드래그하는 일이 있습니다. 이렇게 클릭이나 드래그하는 일을 이벤트라고 합니다.
자바스크립트로 이벤트 핸들러 생성하여를 이벤트 핸들러를 적절한 엘리먼트에 적용하여, 사용자가 엘리먼트에 특정 이벤트를 발생시켰을 때 이벤트 핸들러가 동작하도록 합니다.

다음 그림은 POS기를 만드는 과정 중 일부입니다. 화면에 아메리카노와 카페라떼를 선택하도록 하는 두 개의 버튼이 있습니다. 이 두 개의 버튼을 클릭할 때, 각각 아메리카노를 클릭하셨습니다. 또는 카페라떼를 클릭하셨습니다. 라고 출력하는 단순한 프로그램입니다.

위 예제에서 JavaScript 코드는 다음과 같이 시작합니다.

let menus = document.querySelectorAll("button"); //모든 버튼을 가져옵니다.

let btnAmericano = menus[0];
let btnCaffelatte = menus[1];

btnAmericano.onclick = handleClick;
btnCaffelatte.onclick = handleClick; // 이상으로 for 문으로 충분히 구현할 수 있는 내용입니다.

function handleClick() {
  let currentMenu = event.target.textContent;
  console.log(currentMenu + "를 클릭하셨습니다.");
}

두 버튼의 이벤트 핸들러가 동일한 함수에 의해 동작한다는 점을 주목하세요. 어떤 메뉴가 추가되더라도, 하나의 함수를 추가하면 됩니다. 이렇게 함수를 작성하면 함수를 여러 번 재사용할 수 있습니다.

그렇다면 위의 코드에서 현재 메뉴의 이름을 가져오는 방법은 무엇일까요?
사용자가 버튼을 클릭하면, 그 버튼의 textContent(또는 innerHTML)을 이용해 메뉴의 이름을 가져올 수 있습니다. 사용자가 누른 버튼에 따라 출력되는 이름이 달라지므로, 클릭된 이벤트 객체에서 메뉴의 이름을 가져옵니다. 다시 말해, 이벤트 객체는 사용자 입력(onclick, onkeyup, onscroll 등)을 트리거로 발생한 이벤트 정보를 담은 객체입니다.

addEventListener() vs onclick()

위에서 사용해본 onclick외에도 자바스크립트에서 클릭 이벤트를 할당할 때 한 가지 방식이 더 있습니다.
클릭 이벤트를 할당할 때 대표적인 것이 addEventListener(), onclick() 이렇게 두 가지가 있습니다.

둘의 가장 큰차이점은 onclick은 하나의 이벤트만, addEventListener는 여러 개의 이벤트를 할당할 수 있는 점 입니다.

즉, 다시 말해서 onclick에는 하나의 콜백만 지정가능하고.
addEventListener를 사용하면 여러 개의 이벤트 리스너를 추가할 수 있습니다.

따라서 만약 onclick 이벤트 핸들러를 두 번 이상 사용한다면, 기존 이벤트 핸들러를 덮어쓰기 때문에 가장 아래에 추가한 핸들러만 제대로 작동하게 됩니다. (맨 아래에 설정된 onclick event만 실행됩니다)

addEventListener는 기존 이벤트 핸들러를 덮어 쓰지 않고 얼마든지 계속해서 핸들러를 추가해도 모든 핸들러가 정상적으로 작동하게됩니다.

onclick()이 있는 이유?

onclick은 초기 DOM Level-0에서 제공하던 기능이고, 그 이후 버전인 Level-2에서 추가된 것이 addEventListener입니다.

addEventListener이벤트 캡쳐링, 버블링 같은 이벤트 방식을 설정할 수 있기 때문에 이벤트 제어에 있어서 더욱 유용하지만, addEventListener는 IE8 이하에서는 작동을 하지 않습니다. 그래서 구형 브라우저 지원이 필요하면 onclick을 사용하거나 다른 방법을 찾아야 합니다.

마치며

DOM에 대해 알아보았습니다. 추가로 DOM과 관련해서 알아두면 좋은 지식도 몇가지 추가해서 글을 적게 되었습니다만 알아두시면 자바스크립트가 어떤 방식으로 HTML을 조작하고, 동작하는지에 대해 이해도가 올라갈 것이라고 생각합니다. 앞으로도 공부하기 좋은 주제로 찾아 뵙겠습니다. 긴 글 읽어주셔서 감사드립니다.

Reference

What is the difference between 'remove' and 'removeChild' method in JavaScript?
Difference between Node object and Element object?
Here is why appendChild moves a DOM node between parents
MDN Introduction to the DOM
MDN Node.parentElement
MDN Node.appendChild()
MDN Array-like Object
MDN Element.innerHTML

profile
생각하는 대로 살지 않으면, 사는 대로 생각하게 된다.

6개의 댓글

comment-user-thumbnail
2022년 6월 2일

좋은 글 잘 배우고 갑니다!!

1개의 답글
comment-user-thumbnail
2022년 6월 5일

👍👍👍

1개의 답글
comment-user-thumbnail
2022년 7월 4일
답글 달기
comment-user-thumbnail
2022년 11월 14일

공유 감사합니다 :) 잘 읽었습니다.

답글 달기