MDN에 따르면 문서 객체 모델 ( The Document Object Model, 이하 DOM ) 은 HTML, XML 문서의 프로그래밍 인터페이스다.
텍스트 파일로 만들어져 있는 웹 문서를 브라우저에 렌더링하려면 웹 문서를 브라우저가 이해할 수 있는 구조로 메모리에 올려야 하는데, 브라우저의 렌더링 엔진은 웹 문서를 로드한 후, 파싱해 웹 문서를 브라우저가 이해할 수 있는 구조로 구성해 메모리에 적재하는데 이를 DOM이라고 한다.
즉, HTML 문서의 내용을 각각의 객체로 만들고 이들 객체를 부자관계로 표현 할 수 있는 트리형태로 구조화해 웹페이지와 프로그래밍 언어를 연결시켜주는 역할을 하고, 이때 각각의 요소와 속성, 콘텐츠를 표현하는 단위를 노드 ( node )라고 한다.
1. HTML 문서에 대한 모델 구성
2. HTML 문서 내의 각 요소에 접근 / 수정
// 해당하는 Id를 가진 요소 노드 하나에 접근
// 복수개가 선택된 경우, 첫번째 요소만 반환
document.getElementById('id');
// 태그명으로 해당하는 모든 요소 노드에 접근
document.getElementsByTagName('tagName');
// 해당하는 클래스를 가진 모든 요소 노드에 접근
// 공백으로 구분해 여러 개의 class를 지정
document.getElementsByClassName('class');
// css 선택자로 단일 요소 노드에 접근
// 복수개가 선택된 경우, 첫번째 요소만 반환
document.querySelector('Selector');
// css 선택자로 여러 요소 노드에 접근
document.querySelectorAll('Selector');
<button>DOM! DOM!</button>
const btn = document.querySelector('button');
// target.addEventListener( type, listener ) 문법 형태
// 이벤트의 타입에는 click, mouseover, mouseout, wheel 등 다양한 이벤트를 감지
// listener 함수의 인수에는 이벤트에 대한 정보가 담김
btn.addEventListener('click', function() {
console.log('DOM World~!');
})
<button>I Love green!</button>
const btn = document.querySelector('button');
btn.addEventListener('click', function(){
// DOM api를 통해 요소의 class 속성을 제어
// green 라는 클래스의 속성 값 지정 가능
btn.classList.add('green');
// btn.classList.remove('green'); 클래스를 제거
// btn.classList.toggle('green'); 클래스를 토글합니다. 없으면 넣어주고, 있으면 제거
// btn.classList.contains('green'); 해당하는 클래스가 있는지 확인
})
<ul></ul>
<button>Make me MORE!</button>
// document.createElement(target); target 요소를 생성
// document.createTextNode(target); target 텍스트를 생성
// element.appendChild(target); target 요소를 element의 자식으로 위치
// element.removeChild(target); element의 target 자식 요소를 제거
const btn = document.querySelector("button");
const ul = document.querySelector("ul");
btn.addEventListener('click', function(){
for(let i=0; i < 5; i++){
const li = document.createElement('li');
ul.appendChild(li);
}
})
<div id="parentElement">
<span id="childElement">hello green</span>
</div>
// parentElement.insertBefore(target, location);
// target요소를 parentElement의 자식인 location 위치를 앞으로 이동
const span = document.createElement("span");
const sibling = document.getElementById("childElement");
const parentDiv = document.getElementById("parentElement");
parentDiv.insertBefore(span, sibling);
<p></p>
<input type="text">
<button>Write Something!</button>
const btn = document.querySelector('button');
const p = document.querySelector('p');
const input = document.querySelector('input');
btn.addEventListener('click', function () {
p.textContent = input.value;
});
// input 요소에 'input' 이벤트를 연결하면 실시간으로 값이 반영되게 만들 수 있음
input.addEventListener('input', () => {
p.textContent = input.value;
});
p.innerHTML = '<strong>나는 강하다 무하하</strong>';
// innerHTML은 요소(element) 내에 포함된 HTML 마크업을 가져오거나 설정
// innerText 속성은 요소의 렌더링 텍스트 콘텐츠를 나타냄 (렌더링됨에 주목, innerText는 "사람이 읽을 수 있는" 요소만 처리)
// textContent 속성은 노드의 텍스트 콘텐츠를 표현
// innerText, textContent 차이점 주의!!
<strong class="sayHi"> 하이하이! </strong>
// insertAdjacentHTML : 요소 노드를 주어진 위치에 배치
const sayHi = document.querySelector('.sayHi');
sayHi.insertAdjacentHTML('beforebegin', '<span>안녕하세요! 제 이름은</span><br>');
sayHi.insertAdjacentHTML('afterbegin', '<span>주노입니다.</span><br>');
sayHi.insertAdjacentHTML('beforeend', '<span>추노아닙니다.</span><br>');
sayHi.insertAdjacentHTML('afterend', '<span>주우~ 노우~ 오케이~?!</span><br>');
<article class="cont">
<h1>안녕하세요. <br />저는 주니어 개발자입니다.</h1>
<p>지금부터 자기소개를 해보겠습니다.</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt incidunt voluptates
laudantium fugit, omnis dolore itaque esse exercitationem quam culpa praesentium, quisquam
repudiandae aut. Molestias qui quas ea iure officiis.
</p>
<strong>감사합니다!</strong>
</article>
const cont = document.querySelector('.cont');
// 첫번째 자식을 찾음 - <h1>
console.log(cont.firstElementChild);
// 마지막 자식을 찾음 - <strong>
console.log(cont.lastElementChild);
// 다음 형제요소를 찾음 - 없으니까 null 반환
console.log(cont.nextElementSibling);
// 이전 형제노드를 찾음 - <p>
console.log(cont.previousSibling);
// 모든 직계자식을 찾음 - 모든 직계 자식 요소 <h1>, <p>, <strong>
console.log(cont.children);
// 부모 요소를 찾음 - 없으니까 null 반환
console.log(cont.parentElement);
브라우저 화면에서 이벤트가 발생하면 브라우저는 가장 먼저 이벤트 대상을 찾는다.
캡처링 단계는 가장 상위의 window 객체부터 document, body 순으로 DOM 트리를 따라 내려간다.
버블링 단계는 브라우저는 중간에 만나는 모든 캡처링 이벤트 리스너를 실행시킨다. 그리고 이벤트 대상을 찾고 캡처링이 끝나면 다시 DOM 트리를 따라 올라가며 만나는 모든 버블링 이벤트 리스너를 실행한다.
이벤트 전파는 이런 과정에서 이벤트 리스너가 차례로 실행되는 것이다.
<article class="parent">
<button class="btn" type="button">button</button>
</article>
const parent = document.querySelector('.parent');
const btnFirst = document.querySelector('.btn');
btnFirst.addEventListener('click', (event) => {
console.log('btn capture!');
});
window.addEventListener(
'click',
() => {
console.log('window capture!');
},
true
); // true : 캡처링 단계의 이벤트가 발생하도록 함
document.addEventListener(
'click',
() => {
console.log('document capture!');
},
true
);
parent.addEventListener(
'click',
() => {
console.log('parent capture!');
},
true
);
btnFirst.addEventListener('click', (event) => {
console.log('btn bubble!');
});
parent.addEventListener('click', () => {
console.log('parent bubble!');
});
document.addEventListener('click', () => {
console.log('document bubble!');
});
window.addEventListener('click', () => {
console.log('window bubble!');
});
부모부터 자식까지 일련의 요소를 모두 타고가며 진행되는 이런 이벤트의 특징 덕분에 이벤트 객체에는 target, currentTarget 이라는 속성이 존재한다.
target의 속성에는 이벤트가 발생한 곳의 정보가 담겨 있다. 이 속성을 통해 이벤트 리스너가 없는 요소의 이벤트가 발생했을 때도 해당 요소에 접근 가능하다.
currentTarget의 속성에는 이벤트 리스너가 연결돼 요소가 참조되고 있다.
<article class="parent">
<ol>
<li><button class="btn-first" type="button">button 1</button></li>
<li><button type="button">button 2</button></li>
<li><button type="button">button 3</button></li>
</ol>
</article>
const parent = document.querySelector('.parent');
parent.addEventListener('click', function (event) {
console.log(event.target);
console.log(event.currentTarget);
});
<article class="parent">
<ol>
<li><button class="btn-first" type="button">button 1</button></li>
<li><button type="button">button 2</button></li>
<li><button type="button">button 3</button></li>
</ol>
</article>
const parent = document.querySelector('.parent');
parent.addEventListener('click', function (event) {
console.log(event.target);
if (event.target.nodeName === "BUTTON") {
event.target.innerText = "button 4";
}
})
이벤트 리스너 함수 내부에서의 this 값은 이벤트가 연결된 노드를 참조한다.
event.currentTarget 속성의 참조값과 유사하다.
이벤트 리스너 함수를 화살표 함수로 쓴다면 this가 가리키는 대상이 달라지는 점을 유의하자!
※ 화살표 함수의 this는 자신을 둘러싸고 있는 외부 환경의 this 값을 참조한다.
<article class="parent">
<ol>
<li><button class="btn-first" type="button">button 1</button></li>
<li><button type="button">button 2</button></li>
<li><button type="button">button 3</button></li>
</ol>
</article>
const parent = document.querySelector('.parent');
parent.addEventListener('click', function (event) {
console.log(this);
})
preventDefault ( ) 는 브라우저의 기본 이벤트 동작을 취소한다. 브라우저는 HTML 태그를 통해 여러가지 기능들을 제공하지만, 가끔 그러한 기능이 방해가 되는 경우가 있다.
아래의 예제처럼 종종 브라우저의 기본 동작을 중지하고, 자바스크립트를 통해 기능을 처리하고자 할 때 사용한다.
<!-- 앵커의 기본 동작을 중지 -->
<a href="https://www.naver.com" class="link">네이버 링크입니다아아아아만...</a>
<script>
const link = document.querySelector('.link');
link.addEventListener('click', (event) => {
console.log('clicked');
event.preventDefault();
})
</script>
<!-- submit의 기본 동작을 중지 -->
<form action="">
<button type="submit" class="submit">submit</button>
</form>
<script>
const submit = document.querySelector('.submit');
submit.addEventListener('click', (event) => {
console.log('clicked');
event.preventDefault();
})
</script>
앞에서 우리는 preventDefault ( ) 를 통해 브라우저의 기본 이벤트 동작을 취소해봤는데,
이벤트 전파 ( 이벤트 프로파게이션 ) 를 막지는 못한다.
이때 이벤트 전파를 막고 싶다면, event.stopPropagation ( ) 를 추가한다.
<form action="">
<button type="submit" class="submit">submit</button>
</form>
<script>
const submit = document.querySelector('.submit');
submit.addEventListener('click', (event) => {
console.log('clicked');
// event.preventDefault();
event.stopPropagation();
});
document.body.addEventListener('click', () => {
console.log('event still alive!');
});
</script>