DOM은 HTML에서 파싱한 결과로 나온다고 했어요.
그렇다면 우리는 이걸 생각해봐야 돼요.
💡 그럼, HTML만 잘 나오면 되지, 왜 굳이 또 객체를 만들까?
이는 JavaScript
때문에 그렇습니다.
데이터를 동적으로 제어하여 화면에 출력해야 하는 경우에는, 정적인 HTML
언어로는 불가능했습니다.
그렇지만, javascript
가 HTML에 반영하기 위한 코드를 적용하기 위해서는, 납득이 되어야 하는 중간 다리가 필요했어요.
그것이 바로 DOM입니다. 즉, 우리가 자바스크립트로 뭔가를 렌더링하게 된 이유이기도 하죠.
우리, HTML을 파싱하는 과정에서 DOM Tree가 만들어진다는 것은 이전 파트에서 알게 됐어요.
이때, HTML 요소는 노드 객체로 변환하게 되는데, 다음과 같이 변환됩니다.
<div class="text">HELLO!</div>
div
class="text"
HELLO!
DOM
의 핵심은 트리 자료구조라는 겁니다.
따라서, 계층적으로 노드를 관리한다는 것을 기억해주어야 해요.
이런 특성이 나중에 이벤트를 설명할 때 영향을 주기도 합니다.
책에서는 최상위 노드, 부모 노드, 자식 노드... 이런 말들 많이 쓰기는 하는데, 그냥 직감적으로 와닿잖아요? 그렇게 이해하는 게 편하니, 넘어가도록 하죠.
✅ 필요하지 않다는 것이 아닙니다. 그냥 마주보면 본능적으로 이해하게 되는 용어라 넘어가는 거에요!
우리, 전역 객체 기억나시나요?
전역 객체는 직접 드러나지는 않아도, 암묵적으로 전체 자바스크립트 코드에 엄청난 영향을 주죠. 전역이라는 건 그래서 중요한 겁니다.
그렇다면 이러한 DOM
의 최상위에는 무엇이 있을까요?
바로 Document
객체가 존재합니다.
Object
-EventTarget
- Node
에 걸친 다음 상속으로, 바로 Document
라는 객체가 존재하게 되는 것이죠.
이 친구는, 전역에 유일하게 존재하는데요. 이유는 window
객체가 생성되면, 생성 과정에서 document
라는 프로퍼티에 반영하기 때문입니다.
console.log(window.document) // #document
여기서 흥미로운 점은
Element
와Document
가 결국Node
라는 객체를 상속받는다는 것입니다.
따라서 두 친구 모두,Node
객체의 특성을 지니고 있기에,querySelector
같은 것이 가능한 거였군요!
주목할 것은, 노드 객체의 특성이겠어요.
Object
: JavaScript
의 객체 특성을 갖습니다.EventTarget
: 이벤트를 트리거할 수 있습니다.Node
: 트리 자료구조에 속합니다.Element
: 웹 문서로써의 요소 기능을 합니다(XML, SVG, HTML)HTMLElement
: HTML 요소를 갖고 있습니다.자, 이제 자바스크립트를 통해 DOM을 갖고 놀아볼까요?
다음과 같은 방법으로 요소를 취득할 수 있어요!
// 다음 두 문은 동일하게 id가 'id'인 노드를 찾아냅니다.
const elem1 = document.getElementById('id');
const elem2 = document.querySelector('#id');
// 다음 두 문은 동일하게 class에 'class'가 포함된 노드를 찾아냅니다.
const elem1 = document.getElementsByClassName('class'); // HTMLCollection[포함된 노드 개수]
const elem2 = document.querySelector('.class'); // 계층적으로 탐색하여 일치하는 노드 하나 할당
// 이런 식으로 하위 노드에 대한 탐색 조건을 추가할 수 있습니다.
const elem2 = document.querySelector('#root > .class');
// 다음 두 문은 동일하게 태그가 'div'인 노드를 찾아냅니다.
const elem1 = document.getElementsByTagName('DIV'); // HTMLCollection[포함된 노드 개수]
const elem2 = document.querySelector('div');
// getElementsByTagName, querySelector(All)에서는 와일드카드 문자 '*'을 허용합니다.
document.body.querySelector('*') // 루트 객체
document.body.getElementsByTagName('*') // HTMLCollection(Node 개수)
우리, DOM에 서식하는 친구들 중 유사배열객체가 있다고 했죠?
이 친구들이 그렇습니다. 이터레이션 프로토콜로 이루어져 있으며, 순회가 가능한 유사배열객체죠!
특이한 것은, DOM API를 통해 가져온 결과물로, 실시간으로 노드 객체의 상태를 반영해주고 있습니다.
이러한 특성 때문에, 이 친구들을 살아 있는 객체라고 부르기도 한다고 하네요!
다만 NodeList
의 경우에는 조건부로 살아 있는 상태이지, 실제로는 과거의 상태를 유지하는 Non-live
한 특성을 갖고 있습니다.
이 친구들을 다룰 때에는 각별한 유의가 필요해요.
만약 다음과 같이 document node
가 구성되었다고 가정합시다.
<div class = "red"></div>
<div class = "red"></div>
<div class = "red"></div>
const $elems = document.getElementsByClassName('red');
for (let i = 0; i < $elems.length; i += 1) {
const now = $elems[i];
$elems[i].classList.remove('red');
}
결과는 이렇습니다.
<div class></div>
<div class = "red"></div>
<div class></div>
이유가 무엇일까요?
HTMLCollection
은 정~말 완전히 살아있는 객체에요. 변경 시 그 변경 사항이 즉시 변경이 된답니다. 따라서, i = 0
일 때, 이미 red
가 삭제되어, HTMLCollection[0]
에 위치한 노드가 조건에 맞지 않으니, 바로 제거해버린 거에요.
해결 방법은 아~주 많은데, NodeList
랑 비교해주기 위해 다음 해결방법을 제시합니다.
const $elems = document.querySelectorAll('.red');
for (let i = 0; i < $elems.length; i += 1) {
const now = $elems[i];
$elems[i].classList.remove('red');
}
어때요, 모든 결과가 잘 수행되었나요?
<div class></div>
<div class></div>
<div class></div>
이유는, NodeList
의 경우 실시간으로 반영하기는 하지만, 때에 따라 이처럼 실시간으로 반영해주지 않는 경우가 있는데요.
querySelectorAll
메서드는 이러한 NodeList
를 반환하는 친구라, 클래스 삭제 시에도 삭제된 노드를 기억하고, 나머지를 수행하게 해준 것이에요!
저는 이런 특성 때문에, querySelector
을 이용하는 편입니다.
음... 자식 노드 탐색, 형제 노드 탐색, 부모 노드 탐색 모두 다 나와 있네요.
하지만, 이는 실제로 응용할 때 한 번씩 보시길 추천드려요.
생각보다 안 쓰는 것들도 많고, 괜히 이걸 하나하나 외웠다가는 과부하가 걸릴 것 같습니다.
다만, 생각보다 잘 쓰는 것들을 추천드릴게요.
이 키워드들만 외우면, 나중에 정말 유용할 거에요!
Node.prototype.childNodes
: 자식 노드들 NodeList에 담아 반환Node.prototype.parentNode
: 부모 노드 탐색Node.prototype.previousElementSibling(nextElementSibling)
: 형제 HTML 요소 탐색nodeValue
는 나와 있지만, 저의 경우 잘 안 씁니다.
아무래도 텍스트 노드만 가능하다는 측면이, 생각보다 아쉽기 때문입니다.
textContent
의 경우, 전체를 바꾼다는 점이 약간 불안정해보이지만, document.createElement
가 빈번한 상황에서는 해당 값을 변수로 캐싱하고 바로 textContent
를 작성하는 경우가 많아서 크게 걱정하지는 않는 편입니다.
const div = document.createElement('div');
div.textContent = '...';
const div1 = document.createElement('div');
div.appendChild(div1); // 텍스트가 `div1`에 반영되지 않음.
그렇지만 또 innerText
라는 것도 있죠. 이것까지 생각하면 이제 이중택일인데요.
저는 그래도 textContent
를 선호합니다. 이유는 성능이 더 좋기 때문입니다.
정말 많이 쓰는 프로퍼티입니다. 이 역시 접근자 프로퍼티로 구성되어 있는데요.
HTML
형식에 맞는 문자열을 입력하면, 이를 호출한 노드 객체의 하위에 반영해줍니다!
이때,
innerHTML
은 하위 노드의 마크업까지 모~두 리셋해버릴 거에요.
만약 순전히 추가만 한다면xxx.innerHTML += '추가할 마크업 코드'
을 입력해주면 된답니다! 그러면 기존 자식 노드의 끝에 추가돼요!
여기서도 잘 나와 있네요.
결국 크로스 사이트 스크립팅으로 인해 사용에 유의해야 하는데요.
일반적인 해결 방법으로는 HTML sanitization
이 있어요.
이는 호출을 유도하는 태그들을 살균함으로써 위험한 코드 실행을 방지하는 거에요!
대개는 DOMPurify
나 sanitize-html
등의 라이브러리를 사용하는 편입니다!
innerHTML
에는 단점 중에 하나가, 어느 순서에 넣을 것인지를 정하지 못한다는 거에요.
순전히 자식 노드에 해당하는 HTML 마크업을 변경하는 용도로 사용하는 용도입니다.
이런 단점을, 이 친구가 해결해주어요! 크게 4가지 옵션이 있는데요.
각 옵션에 따라 추가될 위치는 밑의 코드에 주석으로 달아놨어요!
<!-- beforebegin -->
<div class="parent">
<!-- afterbegin -->
<div class="child">CHILD</div>
<!-- beforeend -->
</div>
<!-- afterend -->
const $parent = document.querySelector('.parent');
$parent.insertAdjacentHTML('beforebegin', '<div>NEW CHILD</div>');
$parent.insertAdjacentHTML('afterbegin', '<div>NEW CHILD</div>');
$parent.insertAdjacentHTML('beforeend', '<div>NEW CHILD</div>');
$parent.insertAdjacentHTML('afterend', '<div>NEW CHILD</div>');
말 그대로 요소를 생성하기 위한 메서드에요.
저는 innerHTML
보다는 이 친구를 많이 애용하는 편이랍니다!
참고로,
innerHTML
이 더 편한 점도 많아요. 마크업 구조를 볼 수 있기 때문에 좀 더 직관적인 특성이 있고, 코드가 짧다는 장점이 존재합니다.<template>
과 사용할 때 재사용성 역시 높은 편이구요.다만 제가 이를 좋아하는 이유는, 선언 후 다시 해당 변수로 요소를 캐싱하여 재사용하는 점이 꽤나 효율적이더라구요.
documentFragment
와 함께 조합하면 충분히 리페인트와 리플로우 신경을 덜 쓰게 되기도 하구요. 그저 취향과 전략에 따른 선택이라는 점, 유념하시길 바라요!
이 친구는 인수로 전달받은 노드의 맨 마지막 자식 요소로 추가해줘요!
const parentNode = document.querySelector('.parent');
// 이 코드는 밑의 코드와 동일한 역할을 수행합니다.
parentNode.innerHTML += '<div></div>'
const parentsLastChild = document.createElement('div');
parentNode.appendChild(parentsLastChild);
이 친구는 insertAdjacentHTML
과 비슷한 친구라 생각하면 돼요!
단지, Node
로 두 번째 인자에 전달하여, 그 친구의 앞에 추가합니다.
const parentNode = document.querySelector('.parent');
const parentsLastChild = document.createElement('div');
parentNode.appendChild(parentsLastChild);
const newBeforeChild = document.createElement('div');
parentNode.appendChild(newBeforeChild, parentsLastChild);
노드를 복사해줄 수 있어요.
정~말 가끔 가다 쓰기는 하는데, 이런 복사 용도의 메서드도 있다!라는 것만 생각하면 될 것 같아요.
사실 통째로 지우는 게 innerHTML
로 하는 게 더 편리해서 많이 잘 안 쓰기는 합니다.
다만, 원하는 요소만 콕 찝어서 지울 때 사용하는 편입니다!
parentNode.removeChild(childNode)
한 노드는 크게 3가지의 노드로 나뉘었는데, 그 중 하나가 바로 어트리뷰트 노드였죠? 그 친구입니다.
HTML에서 어트리뷰트
의 키는 대개 정해져 있는 편이에요.
당장 많이 쓰는 것만 따져봐도, id, class, style, tabindex, data
등이 그렇죠.
이러한 어트리뷰트는 NamedNodeMap
객체에 담기게 됩니다.
이는 Element.prototype.attributes
로 알 수 있죠.
document.body.attributes // NamedNodeMap
hasAttribute
를 통해 엘리먼트가 해당 어트리뷰트를 갖고 있는지 알 수 있습니다.
또한, getAttribute
메서드를 통해 엘리먼트의 어트리뷰트 키가 어떤 값을 갖고 있는지를 알 수 있죠.
setAttribute
를 통해 엘리먼트의 어트리뷰트를 조작할 수 있어요.
결과적으로, 초기 상태 값을 변경함으로써 요소를 다시 정의해주는 것입니다.
$urlLink = document.querySelector('a');
$urlLink.setAttribute('href', 'https://velog.io');
둘은 항상 같지 않습니다. 물론 className
과 id
등의 어트리뷰트는 즉각 반응하기도 합니다. 그렇지만, inputElement.value
와 같은 프로퍼티를 동적으로 변경하면 HTML 어트리뷰트
에서는 반응하지 않는데요.
하지만 둘의 쓰임새는 다릅니다.
정말 많이 쓰는 친구죠.
데이터 속성을 정의해줄 수 있는데요. 이를 통해 해당 요소가 특정 값을 가질 수 있습니다. 이때 저장되는 값은, string
으로 변환된다는 것에 유념합시다.
const $elem = document.querySeletor('div');
$elem.dataset.nowArr = [1,2,3];
console.log($elem.dataset.nowArr); // 1,2,3
<!-- 어트리뷰트의 이름의 경우, 케밥케이스로 변환됩니다. -- >
<div data-now-arr="1,2,3"></div>
classList
로 편하게 모든 것을 다룰 수 있습니다.
className
은 이름을 지정하기 편하기는 하지만, 복수의 클래스 이름을 지정할 때는 매우 불편하여,classList
를 잘 기억하면 됩니다!
$elem.classList.add('name') // 추가
$elem.classList.contains('name') // true; 탐색
$elem.classList.replace('name', 'id') // name이라는 클래스 이름을 id로 변경
$elem.classList.toggle('name') // class="id name"; id라는 클래스 이름 존재 시 제거, 아니면 추가
$elem.classList.remove('id') // class="name"; 클래스 이름 제거
이건 이번에 저도 새롭게 알게 되었고, 덕분에 언러닝하게 되었어요 🎉
저는 항상 착각하고 있었던 게, css
는 style
프로퍼티로 가져온다고 생각했어요.
그런데 항상 style
로 가져올 경우, css
에서 정의한 스타일은 가져오지 않더라구요!
이를 통해 잘못된 가설을 세웠었습니다. 바로 우리가 개발자 도구로 보는 DOM은 DOM Tree
라고 말이죠!
그런데 생각해보니... 개발자 도구에는
Style
을 볼 수가 있죠? 따라서 우리가 보는 건 모든 레이아웃 결과를 반영한 결과입니다. 즉 제 가설이 틀린 것이죠.
따라서 스타일을 가져올 수 있어요.
어떻게 말이죠? 우리, 브라우저에서는 HTML
보다 상위에 있는 친구가 있잖아요!
실제로 HTML은 Element
객체이죠. 이보다 상위에 있는 객체에는, 바로 전역 객체, Window
가 있습니다.
window.getComputedStyle(document.body); // CSSDeclaration {...}
window.getComputedStyle(document.body).width // '811px'
// 길이도 가져올 수 있어요.
window.getComputedStyle(document.body).length // 343
이런 식으로 가져올 수 있어요!
DOM은 2018년 4월부터 완전히 표준을 제공하게 되었어요.
아무래도 DOM
이 달라지면, 또 벤더마다 코드를 따로 짜야 하니... 매우 골치 아픈 상황이 발생하기 때문이죠.
현재는 구글, 애플, MS, 모질라가 주도하는 WHATWG
을 표준으로 하기로 했으며, 현재까지 4개의 DOM 레벨이 존재한답니다 🎉
와...
간단히 쓰려고 엄청 간결하게 썼다 자부하는데, 이정도의 양이 나왔네요.
확실히 DOM... 파면 팔 수록 매우 깊다는 것을 다시금 체감하네요.
그렇지만, 정말 얻은 게 많았어요.
특히 CSS
의 결과 값을 DOM
에서 가져올 수 있다는 것을 통해, 모든 렌더링 과정의 결과물을 DOM
에서 업데이트한다는 것까지 이해할 수 있었어요.
덕분에, HTML Attribute와 DOM Property가 다르다는 것 역시 쉽게 납득할 수 있었답니다.
역시 공부라는 것은 힘들 수록 얻는 게 많은 것 같아요.☺️ 이상! 🌈