DOM (Document Object Model)은 HTML 요소를 Object처럼 다룰 수 있는 Model입니다.
우리는 Javascript를 사용한다면 DOM으로 HTML을 조작할 수 있습니다.
DOM을 조작할 수 있는 주요 메서드와 개념을 살펴보도록 하겠습니다.
const newDiv = document.createElement('div');
새로운 element를 만드는 메서드입니다.
DOM에 새로운 element가 생성되었지만 붕 떠 있는 상태입니다. 즉, DOM 트리의 어떠한 요소와도 연결되어 있지 않기 때문에 화면 상에 보이지 않습니다. newDiv를 추후에 DOM의 적절한 위치에 삽입하면 화면에 나타나게 됩니다.
const newDiv = document.createElement('div');
newDiv.textContent = "Hi!"
document.body.append(newDiv);
document.body.appendChild(newDiv);
위와 같이 div에 텍스트를 추가하고 body에 append 혹은 appendChild를 하게 되면 body의 마지막 자식 요소로 추가되어 화면에 보이게 됩니다.
하지만, 화면에는 Hi! 가 2개가 아닌 하나만 보이게 됩니다. 그 이유는 특정 노드는 반드시 하나의 부모만을 가져야 하기 때문에 만약 하나의 노드로 여러 군데 append를 하게 되면 가장 마지막에 append한 부모 밑으로만 삽입됩니다. 이런 특성을 활용하여 특정 노드를 이동시켜야 할 때 굳이 삭제 후 append하지 않고 append만 하여도 되겠습니다.
append와 appendChild는 비슷해보이지만 차이점이 존재합니다.
또한, append는 리턴값으로 아무것도 반환하지 않지만 appendChild는 추가한 element를 반환합니다.
textContent는 콘텐츠를 text/plain으로 파싱한 결과이므로 파싱이 빨라서 성능이 좋습니다. 해당 노드가 가지고 있는 텍스트 값을 그대로 읽습니다.
innerText는 textContent와 비슷하지만 style 등의 마크업 언어가 적용된 상태입니다. 예를 들면, 어떤 element의 스타일에 display: none이 적용되어 있다면 그 안에 있는 innerText는 불러들이지 않습니다.
innerHTML은 text/html로 파싱한 결과로 파싱이 느리고 xss 공격에 취약합니다. 텍스트, 태그를 비롯하여 html이 허용하는 모든 것이 들어갈 수 있기 때문입니다. 특정 노드 자식 노드를 모두 리셋할 때에 innerHTML = "";를 사용하고 있다면 반복문을 돌리고 그 안에서 remove나 removeChild 메서드를 통해 하는 것이 좋습니다.
remove() 는 노드를 메모리에서 삭제하지만 removeChild()는 노드를 삭제하지 않습니다.
메모리에 해당 노드는 그대로 존재하며, 부모 노드와의 부모-자식관계를 끊어 DOM 트리에서 해제하는 것입니다. 최종적으로는 관계를 끊은 해당 노드의 참조를 반환합니다.
const newDiv = document.createElement('div');
newDiv.textContent = "Hi!"
document.body.append(newDiv);
document.body.appendChild(newDiv);
newDiv.remove(); // newDiv를 삭제
document.body.removeChild(newDiv); // 특정 노드의 자식인 newDiv를 삭제
이 두 메서드에는 아주 중요한 규칙이 있습니다.
반드시 id와 class 이름을 사용해서 불러올 때는 # 또는 .을 앞에 추가하여야 합니다.
const newDiv = document.createElement('div');
newDiv.textContent = "Hi!"
document.body.append(newDiv);
document.body.appendChild(newDiv);
const oldDiv = document.querySelector("div");
console.log(oldDiv.textContent); // Hi!
여기서 '가장 먼저 조회된'이란 보통 HTML을 위에서 아래로 탐색하기 때문에 해당 조건을 만족하는 가장 위의 노드라고 보시면 됩니다.
하지만 엄밀히 말하면, querySelectorAll로 불러온 노드들이 담긴 것은 배열이 아닌 NodeList로 유사 배열입니다. 따라서, NodeList의 prototype에 정의되어 있는 메서드인 forEach는 바로 사용할 수 있지만 reduce같이 정의되어 있지 않은 메서드들은 바로 사용이 불가합니다.
유사 배열은 Array.from() 메서드를 통해 배열로 바꿀 수 있습니다.
보통 인라인으로 스타일을 적용하기보다는 class에 스타일을 정의하고 이 클래스를 특정 노드에 추가 또는 삭제하는 방향으로 적용합니다. 그래야 관심사 분리를 통해 스타일과 로직을 분리하여 개발할 수 있기 때문입니다.
노드의 속성을 추가, 변경할 수 있습니다.
const button = document.querySelector("#registerButton");
button.setAttribute("disabled", "disabled");
button.removeAttribute("disabled");
만약, 특정 버튼을 가져와서 disabled 속성을 추가하고 싶으면 setAttribute를 위와 같이 사용하고, disabled 속성을 제거하고 싶다면 removeAttribute 메서드를 사용하면 됩니다.
children은 자식 요소에 접근하고 childNodes는 자식 노드에 접근합니다.
children은 자식 요소가 포함된 HTMLCollection을 반환하며 비 요소 노드는 모두 제외됩니다.
childNodes는 자식 노드가 포함된 NodeLIst를 반환하며 요소 노드와 주석 노드 같은 비 요소 노드도 포함합니다.
DocumentFragment는 DOM의 단편적인 부분을 정의할 수 있는 노드입니다. 부모가 없는 최소화된 경량화된 문서 객체라고도 합니다.
위의 특성으로 Javascript 성능을 최적화할 수 있습니다. DocumentFragment에 변경이 일어나도 DOM에는 어떠한 변화도 일어나지 않기 때문에 브라우저가 화면을 다시 렌더링 하지 않습니다. 이것은 Reflow나 Repaint가 일어나지 않는다는 뜻이므로 불필요한 렌더링을 줄일 수 있습니다.
const documentFragment = new DocumentFragment();
const ulElement = document.createElement("ul");
documentFragment.appendChild(ulElement);
["code", "states", "frontend", "course"].forEach((text) => {
const liElement = document.createElement("li");
liElement.textContent = text;
ulElement.appendChild(liElement);
});
console.log(documentFragment.textContent); // codestatesfrontendcourse