[javascript] DOM을 알아보자

야생피카츄·2023년 9월 26일
0
post-thumbnail

🍨 브라우저의 렌더링

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요소를 페인팅합니다.

🍨 DOM 이란?

문서 객체 모델(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 태그 이름

여기서 주의할 점은 getElementsByClassgetElementsByTagName는 복수 존재할 수 있기 때문에 유사 배열(HTML Collection) 형태로 받게 된다는점! 이름도 Elements 복수로 되어있습니다.

🍧 CSS 선택자

메서드로 찾는 방법 중에는 CSS선택자를 통해 노드를 선택할 수도 있습니다.

document.querySelector('#id');
document.querySelectorAll('.class');
document.querySelectorAll('p');

querySelector는 요소 노드를 1개만 선택할 때 사용되고 querySelectorAll은 여러개의 노드를 선택할 때 사용합니다. 여러개의 노드를 선택할 때 getElementsByClass, getElementsByTagName은 HTML Collection을 반환했지만 querySelectorAll은 NodeList객체에 담아 반환합니다.

🍦query와 getElement

두 메서드 다 원하는 요소를 선택하지만 query는 매개변수로 CSS선택자를 전달받기 때문에 조금 더 범용적으로 사용할 수 있습니다. 단순 성능만 봤을땐 getElement가 좋지만 현대적 웹에선 신경쓸 정도의 차이는 아닙니다.

🍦HTML Collection과 NodeList

둘 다 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>

또한 값을 할당할 때에 textContentinnerText는 텍스트로만 적용되어 태그가 그대로 출력되지만 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('속성명');

🍨 노드의 추가 삭제

🍧 노드 추가

자바스크립트에서 노드를 추가해줄 수 있습니다. 먼저 insertAdjacentHTMLinnerHTML을 통한 조작과 비슷하지만 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>);
profile
각성개발자

0개의 댓글