DOM에 대해 배우기 전에 왜 DOM이 필요한지 브라우저 렌더링에 대해 간단하게 살펴보며 알아보도록 하겠습니다.
대부분의 프로그래밍 언어는 운영체제나 가상 머신 위에서 실행되지만 웹 애플리케이션은 브라우저 위에서 HTML, CSS와 함께 실행
됩니다. 브라우저는 HTML, CSS, Javascript 문서들을 파싱해서 브라우저에 렌더링 하게 됩니다.
1. 브라우저는 필요한 리소스를 서버에 요청하여 받습니다.
2. 브라우저의 렌더링 엔진은 응답받은 HTML과 CSS를 파싱해서 DOM과 CSSOM을 생성, 결합하여 렌더 트리를 생성합니다.
3. 브라우저의 자바스크립트 엔진은 응답받은 자바스크립트를 파싱하여 AST(Abstract Syntax Tree)를 생성하고 바이트코드로 변환하여 실행합니다. 이때 자바스크립트는 DOM API를 통해 DOM과 CSSOM을 변경할 수 있습니다. 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합됩니다.
4. 렌더 트리를 기반으로 HTML요소의 레이아웃을 계산하고 브라우저 화면에 HTML요소를 페인팅합니다.
문서 객체 모델(Document Object Model)을 나타내는 말로 HTML문서의 구조를 객체의 트리로 표현합니다. 이렇게만 말하면 무슨 말인지 감이 안잡히겠죠?
웹브라우저는 HTML문서를 해석하고 그 과정에서 위 그림과 같은 트리 형태의 DOM 트리 형태로 문서 구조를 변환합니다. DOM 트리는 document
객체 하위에 요소들이 트리 형태로 구성되는데 이를 각각 노드라고 합니다. 최상위 노드는 루트 노드라고 부릅니다. 이러한 트리 구조에서 각 노드들은 부모, 자식, 형제 관계를 가지게 됩니다.
DOM 트리를 왜 알아야 할까요? 그건 DOM에서 제공하는 DOM API를 통해 HTML의 각 요소에 접근하고 조작할 수 있기 때문입니다. document
객체는 트리를 탐색하면서 원하는 노드를 선택할 수 있는 속성을 제공해줍니다.
구분 | 속성 | 설명 |
---|---|---|
모든 노드 탐색 | parentNode | 부모 노드 반환 |
childNodes | 모든 자식 노드를 반환한다 | |
firstChild | 첫 번째 자식 노드를 반환한다 | |
lastChild | 마지막 자식 노드를 반환한다 | |
previousSibling | 이전 형제 노드를 반환한다 | |
nextSibling | 다음 형제 노드를 반환한다 | |
------------------- | ------------------------------ | ------------------------------------------- |
요소 노드 탐색 | parentElement | 부모 요소 노드 반환한다 |
children | 자식 요소 노드 반환한다 | |
firstElementChild | 첫 번째 자식 요소 노드를 반환한다 | |
lastElementChild | 마지막 요소 노드를 반환한다 | |
previouseElementSibling | 이전 요소 노드를 반환한다 | |
nextElementSibling | 다음 요소 노드를 반환한다 |
모든 노드와 요소 노드 탐색은 이름은 비슷해 보이지만 결과에 차이가 있습니다. 노드는 위의 DOM 트리 그림에서 알 수 있듯 문서 노드, 태그와 같은 요소 노드, href같은 속성 노드, 텍스트 노드 등 HTML문법으로 작성할 수 있는 구성 요소들이 포함되어 있습니다. 그래서 실제로 사용해보면 이런 결과가 나타나게 됩니다.
console.log(document.firstChild); /*<!DOCTYPE html>*/
console.log(document.firstElementChild); /*html*/
각각 document
객체의 첫 번째 자식 노드와 첫 번째 자식 요소를 반환합니다. 두 탐색은 document.childNodes[1].firstElementChild
와 같이 함께 사용할 수도 있습니다.
DOM 트리가 복잡해질 수록 속성만으론 원하는 노드를 찾기 어려워집니다. 그래서 보통 요소 노드를 바로 찾아갈 수 있는 메서드와 함께 사용하게 됩니다.
document.getElementById('id'); //id 속성값
document.getElementsByClass('class'); //class 속성값
document.getElementsByTagName('p'); //HTML 태그 이름
여기서 주의할 점은 getElementsByClass
와 getElementsByTagName
는 복수 존재할 수 있기 때문에 유사 배열(HTML Collection) 형태로 받게 된다는점! 이름도 Elements 복수로 되어있습니다.
메서드로 찾는 방법 중에는 CSS선택자를 통해 노드를 선택할 수도 있습니다.
document.querySelector('#id');
document.querySelectorAll('.class');
document.querySelectorAll('p');
querySelector
는 요소 노드를 1개만 선택할 때 사용되고 querySelectorAll
은 여러개의 노드를 선택할 때 사용합니다. 여러개의 노드를 선택할 때 getElementsByClass
, getElementsByTagName
은 HTML Collection을 반환했지만 querySelectorAll
은 NodeList객체에 담아 반환합니다.
두 메서드 다 원하는 요소를 선택하지만 query
는 매개변수로 CSS선택자를 전달받기 때문에 조금 더 범용적으로 사용할 수 있습니다. 단순 성능만 봤을땐 getElement
가 좋지만 현대적 웹에선 신경쓸 정도의 차이는 아닙니다.
둘 다 DOM API가 여러 개의 결과 값을 반환하는 DOM Collection객체로 유사 배열 객체이면서 이터러블입니다. 둘 모두의 중요한 특징은 노드 객체의 상태를 실시간으로 반영하는 살아있는 객체라는 것입니다. 다만 HTML Collection은 항상 살이있는 노드로 동작하는 한편 NodeList는 정적 상태를 유지하다가 경우에 따라 live로 동작합니다. 때문에 HTML Collection은 변동 사항이 있을 시 실시간으로 배열이 변경될 수도 있기 때문에 for문 순회시 주의해야 합니다.
const el = document.getElementByClassName('red');
for(let i = 0; i < el.length; i++){
el[i].className = 'blue';
}
위 코드의 경우 i=0
일 때 el[0]
이 blue가 되며 el에서 실시간으로 제거되어 i=1
일 때 el[1]
은 2번째 노드가 아닌 3번째 노드를 가리키게 됩니다. 때문에 역방향 for문을 사용하거나 원하는 결과까지 while문을 돌리는 등의 방법을 사용해야 합니다.
NodeList의 경우 childNodes
에 의해 리턴될 경우 live로 동작합니다. 때문에 유사 배열 객체들은 스프레드 문법 [...el]
과 같이 사용하거나 Array.from
으로 배열로 변환해 사용하는 것이 안전합니다.
textContent
는 요소의 모든 텍스트에 접근하고, innerText
는 요소 노드 중 웹 브라우저에 표시되는 텍스트에만 접근합니다. innerHTML
은 요소 노드의 텍스트 중 HTML 태그를 포함한 텍스트에만 접근합니다.
<!--html -->
<p id="title">오늘은 <span style="display:none;">날씨가 좋네요!</span></p>
//js
const title = document.querySelector('#title');
console.log(title.textContent);//오늘은 날씨가 좋네요!
console.log(title.innerText);//오늘은
console.log(title.innerHTML);//오늘은 <span style="display:none;">날씨가 좋네요!</span>
또한 값을 할당할 때에 textContent
와 innerText
는 텍스트로만 적용되어 태그가 그대로 출력되지만 innerHTML
은 값을 태그로 인식해 적용시킬 수 있습니다.
title.textContent = '<strong>아니요? 비가 오는데요</strong>';
title.innerText ='<strong>아니요? 비가 오는데요</strong>';
title.innerHTML = '<strong>아니요? 비가 오는데요</strong>';
요소 노드라면 style
속성으로 요소에 스타일을 지정할 수 있습니다. 아래 예시는 p
태그의 텍스트를 파란색으로 변경하는 예제입니다. style.속성명
을 통해 원하는 스타일을 줄 수 있습니다.
const el = document.querySelector('p');
el.style.color = 'blue';
하지만 background-color
같이 -
가 있는 속성은 어떻게 해야할까요? 이런 경우엔 카멜 표기법을 따라 backgroundColor
로 작성해주면 됩니다.
스타일 속성을 하나 하나 조작하는 것 보다는 정해둔 class의 css 속성으로 한 번에 여러 스타일을 적용하는 것이 훨씬 편합니다. 아래처럼 .active
를 준다던가 요소를 잠시 사라지게 한다던가 사용처는 무궁무진합니다.
<style>
.active{
color: blue;
font-size: 20px;
}
</style>
<body>
<p class='active'>active</text>
</body>
이러한 클래스 속성을 주기 위해선 다음과 같은 메서드들이 필요합니다.
노드.classList.add('class 속성값'); //추가
노드.classList.remove('class 속성값'); //삭제
노드.classList.toggle('class 속성값'); //추가 삭제 반복
이를 통해 원하는 클래스들을 추가하거나 삭제해 스타일을 적용할 수 있습니다.
다음 메서드를 사용하면 모든 속성을 조작할 수도 있습니다.
노드.getAttribute('속성명');
노드.setAttribute('속성명', '속성값');
노드.removeAttribute('속성명');
자바스크립트에서 노드를 추가해줄 수 있습니다. 먼저 insertAdjacentHTML
은 innerHTML
을 통한 조작과 비슷하지만 innerHTML
이 기존 자식 노드를 모두 제거하고 처음부터 새롭게 자식노드를 추가하는 반면 insertAdjacentHTML
은 새로운 요소만 파싱하여 자식노드를 추가해 조금 더 효율적이고 빠릅니다. 하지만 두 방법 모두 마크업 문자열을 파싱하기 때문에 크로스 사이트 스크립팅 공격에 취약합니다.
<node>.prototype.insertAdjacentHTML(position, DOMString) //위치에 새 요소 삽입
공격자가 상대방의 브라우저에 스크립트가 실행되도록 해 사용자 세션을 가로채거나, 웹사이트 변조, 악의적 콘텐츠 삽입, 피싱 공격 등을 진행하는 것을 말합니다.
다음은 DOM에서 제공하는 노드 조작 메서드 입니다. 노드를 생성하고 기존 노드에 새로운 노드를 연결할 수 있습니다.
document.createElement(); //요소노드 생성
document.createTextNode(); //텍스트 노드 생성
document.createAttribute(); //속성 노드 생성
<node>.appendChild(<child node>);//기존 노드에 자식노드 연결
<node>.setAttributeNode(<attribute node>);//기존 노드에 속성 노드 연결
그런데 이러한 추가 작업을 할 때 여러개의 노드 추가시 주의할 점이 있습니다. DOM이 변경되는 것은 코스트가 높기 때문에 횟수를 줄여주는 편이 성능적으로 좋기 때문입니다. 그래서 컨테이너 요소를 새로 만들고 그 밑에 여러개의 자식들을 연결한 후에 appendChild
를 통해 기존 노드에 추가시켜주면 DOM은 한 번만 변경되게 됩니다.
const foods = document.getElementById('foods');
const container = document.createElement('div');
['파인애플피자','펩시','지코'].forEach( text =>{
const li = document.createElement('li');
const textNode = document.createTextNode(text);
li.appendChild(textNode);
container.appendChild(li);
}
foods.appendChild(container); //DOM이 변경되는 것은 이거 한 번!
자식 노드를 추가하는 위치를 정하거나 복사, 교체 시키는 메서드도 있습니다.
<node>.insertBefore(<new node>, <child node>); //메서드를 호출한 노드의 자식이어야함
<node>.cloneNode([deep: true | false]); //깊은복사 : 자손까지 복사, 얕은 복사 : 노드 자신만 복사
<node>.replaceChild(<new child>, <old child>); //호출한 노드의 자식 노드를 다른 노드로 교체
자신의 자식 노드를 삭제할 수 있다.
<parent node>.removeChild(<child node>);