문서 객체 모델 ( DOM : Document Object Model ) 은 XHTML, HTML 문서용 API 이다.
DOM 표준은 W3C DOM, WHATWG DOM 에서 살펴볼 수 있다.
다만, 브라우저마다 표준에 명시된 기능 외에 별도로 제공되는 추가 기능이 있을 수 있다.
DOM API 는 Document를 통해 사용할 수 있다.
DOM은 문서를 노드의 계층적인 트리 구조로 나타낸다.
노드 트리의 최상위 노드를 root라 하며 일반적으로 HTML 문서에서 <html>
이 root 노드가 된다.
각각 노드는 타입에 따라 Element 노드, Text 노드, Attr 노드 등으로 분류되어 계층 구조를 가진다.
Text 노드는 각 트리 노드의 리프 노드가 되며 Body 엘리먼트 안에 개행이나 띄어쓰기가 존재하는 경우 Text 노드가 만들어 진다.
주석 또한 화면에 나타나지는 않지만 노드로 만들어져 DOM 트리에 포함된다.
노드의 계층 구조
Element는 DOM 엘리먼트 생성의 가장 기본이 되는 클래스로 요소 탐색 ( querySeletor()
, getElementsByName()
등 ) 부터 이벤트 리스너 관리 ( addEventListener()
, removeEventListener()
등 ) 여러 메서드를 제공한다.
상속 여부는 instanceof
를 통해 쉽게 확인할 수 있다.
<select>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
const select = document.querySelector('select');
console.log(select instanceof HTMLSelectElement); // true
console.log(select instanceof HTMLElement); // true
console.log(select instanceof Element); // true
console.log(select instanceof Node); // true
console.log(select instanceof EventTarget); // true
console.log(select instanceof Object); // true
노드는 여러 프로퍼티를 갖고 있는데 다음 3가지의 프로퍼티를 살펴보자.
nodeType : 12개의 노드 타입을 정수로 나타낸다. 읽기 전용 프로퍼티다.
예 ) Node.ELEMENT_NODE (1) 또는 Node.ATTRIBUTE_NODE (2) 등
nodeName : 노드의 이름을 문자열로 나타낸다. 읽기 전용 프로퍼티다.
예 ) "Attr" , "Comment" , "Element"
nodeValue : 주석 및 텍스트 노드일 경우 그 내용이 반환되고 속성 노드일 경우 속성 값이 반환된다. 그 밖의 노드는 null
을 반환한다.
DocumentFragment는 마크업에 표현되지 않는 유일한 노드 타입이다.
매번 문서의 DOM 요소를 변경할 경우 성능에 영향이 있을 수 있으므로 DocumentFragment를 버퍼로써 사용하여 한 번에 업데이트할 수 있다.
단,innerHTML
로 삽입하는 것은 불가하므로appendChild()
와 같은 메서드를 이용해야 한다.
DOM 트리에서 갖는 계층 관계를 노드에서 프로퍼티로 제공하며, 이를 이용해서 DOM 트리의 각 노드를 탐색할 수 있다.
아래의 프로퍼티의 반환 값은 모두 읽기 전용이다.
parentNode : 부모 노드를 반환한다.
childNodes : 요소의 자식 노드를 반환한다. 반환형은 NodeList 이다.
firstChild : 자식 노드 중 첫 번째 자식 ( 없으면 null ) 을 반환한다.
lastChild : 자식 노드 중 마지막 자식 ( 없으면 null ) 을 반환한다.
nextSibling : 부모의 childNodes 목록 중 자신 다음에 있는 노드 ( 없으면 null ) 를 반환한다.
previousSibling : 부모의 childNodes 목록 중 자신 이전에 있는 노드 ( 없으면 null ) 를 반환한다.
위의 프로퍼티들은 항상 current DOM 상태를 유지한다.
DOM 노드가 추가 및 제거 될때 같이 변경되므로 사용에 주의해야 한다.
<div>
<h1>리스트 시작</h1>
<ol id="ol-id">
<li id="first-li-id">1</li>
<li>2</li>
</ol>
</div>
createElement()
const divElement = document.createElement('div');
const liElement = document.createElement('li');
liElement.className = 'li-class-name'; // 클래스 추가
liElement.id = 'li-id'; // id 추가
liElement.innerHTML = "<strong>삽입되는 요소</strong>"; // li 내부 HTML노드 할당
appendChild(tagName)
const divElement = document.createElement('div');
const liElement = document.createElement('li');
liElement.className = 'li-class-name'; // 클래스 추가
liElement.id = 'li-id'; // id 추가
liElement.innerHTML = "<strong>삽입되는 요소</strong>"; // li 내부 HTML노드 할당
const olElement = document.getElementById("ol-id");
olElement.appendChild(liElement); // 생성한 노드 객체를 삽입
const firstLiElement = document.getElementById("first-li-id");
olElement.appendChild(firstLiElement); // 기존 엘리먼트를 삽입 => 위치가 마지막으로 바뀜
insertBefore(삽입하는 노드, 참조 노드)
참조노드가 null일 경우에는 appendChild()
와 동일하게 동작한다.
const divElement = document.createElement('div');
const liElement = document.createElement('li');
liElement.className = 'li-class-name'; // 클래스 추가
liElement.id = 'li-id'; // id 추가
liElement.innerHTML = "<strong>삽입되는 요소</strong>"; // li 내부 HTML노드 할당
const olElement = document.getElementById("ol-id");
const firstLiElement = document.getElementById("first-li-id");
olElement.insertBefore(liElement, firstLiElement);
부모노드.prepend(삽입하는 노드)
: 엘리먼트 내부의 가장 앞으로 이동
부모노드.append(삽입하는 노드)
: 엘리먼트 내부의 마지막으로 이동
노드.before(삽입하는 노드)
: 엘리먼트 앞으로 이동
노드.after(삽입하는 노드)
: 엘리먼트 뒤로 이동
위 4개의 메서드는 문자열을 인자로 받을 경우 자동으로 텍스트 노드를 생성하여 삽입한다.
append() vs appendChild()
appendChild()
와 insertBefore()
가 이미 존재함에도 불구하고 새롭게 append()
, prepend()
, after
, before()
가 등장한 이유 무엇일까?
append()
와 appendChild()
를 대표로 두 그룹의 차이점을 알아보자.
문자열 인자를 넘겨줄 경우, append()
는 자동으로 텍스트 노드를 생성해 주지만 appendChild()
는 에러를 발생시킨다.
append()
의 경우 append(노드1, 노드2)
와 같이 여러 노드를 한 번에 추가할 수 있다. 하지만 appendChild()
는 여러 노드를 추가하고 싶을 경우엔 여러 번 호출해야 한다.
권장하는 방식은
append()
이지만 IE브라우저를 고려해야 한다면appendChild()
를 사용해야 한다. 이노무 IE 자식은 안끼는 데가 없네.
insertAdjacentHTML(position, text)
XML 또는 HTML로 해석될 수 있는 문자열을 파싱한 뒤 적절한 노드를 생성해 특정 위치에 삽입한다.
position은 아래 있는 단어만 사용 가능하다.
'beforebegin' : element 앞에
'afterbegin' : element 안에 가장 첫번째 child
'beforeend' : element 안에 가장 마지막 child
'afterend' : element 뒤에
const h1Element = document.querySelector("h1");
h1Element.insertAdjacentHTML('afterend', "<h2>리스트</h2>");
removeChild()
노드를 DOM 트리 내에서 제거할 때 사용한다.
부모 노드에서 removeChild()
메서드를 호출해 제거한다.
const olElement = document.getElementById("ol-id");
const firstLiElement = document.getElementById("first-li-id");
olElement.removeChild(firstLiElement);
// 부모 노드를 아래와 같은 방식으로 접근 할 수도 있다.
// firstLiElement.parentNode.removeChild(firstLiElement);
remove()
remove()
메서드는 removeChild()
메서드와는 다르게 제거하고 싶은 노드에서 직접 메서드를 호출할 수 있기 때문에 더욱 간결하지만 아니나 다를까, 이 메서드 또한 IE에서 지원하지 않으므로 사용에 주의해야 한다.
const firstLiElement = document.getElementById("first-li-id");
firstLeElement.remove();
앞서 배웠던 parentNode, childNodes 같은 노드 프로퍼티를 사용하면 원하는 노드에 직접 접근할 수 있다.
하지만 문서 구조가 자주 변경되거나 계층적으로 깊게 위치한 엘리먼트에 접근하는 경우 비효율적이다.
이런 경우 querySelector()
, getElementById()
와 같은 엘리먼트 검색 메서드를 통해 해당 엘리먼트에 바로 접근할 수 있다.
getElementById(아이디)
: Element 객체를 반환하거나 없다면 null을 반환한다.
querySelector(CSS selectors)
: Element 객체를 반환하거나 없다면 null을 반환한다.
여러 선택자를 콤마 ( , ) 로 이어서 전달할 경우 OR 조건으로 동작한다.
querySelectorAll(CSS selectors)
: CSS selectors와 일치하는 모든 엘리먼트를 NodeList형태로 반환한다.
이 NodeList는 살아 있지 않은 유사 배열 객체로 후에 DOM의 변경이 있어도 바뀌지 않는다.
getElementsByClassName()
, getElementsByByTagName()
: 일치하는 여러 엘리먼트를 모두 반환한다. 반환 값은 HTMLCollection으로 살아있는 유사 배열 객체로 후에 DOM의 변경이 있으면 실시간으로 업데이트 된다.
Event 객체는 DOM 내에서 발생한 이벤트에 대한 정보를 담고 있다.
Event 객체는 발생한 이벤트의 종류부터 엘리먼트에 대한 정보, 캡처링 여부, 이벤트 발생 위치 등 여러 정보를 갖고 있다.
target : 이벤트가 처음 발생했던 대상 DOM 엘리먼트의 참조를 갖는다.
currentTarget : 발생한 이벤트가 등록된 DOM 엘리먼트의 참조를 갖는다.
preventDefault()
: 현재 이벤트의 기본 동작을 중단한다.
하지만 전파 ( 캡처링, 버블링 ) 는 막지 못한다.
stopPropagation()
: 전파를 막는다.
하지만 현재 이벤트의 기본 동작을 중단하지는 못한다.
HTML 엘리먼트 속성으로 할당하기
<button onclick="clickEventHandler(event)">버튼</button>
<script>
function clickEventHandler(event) {
console.log(event)
}
</script>
DOM 프로퍼티로 할당하기
<button>버튼</button>
<script>
document.querySelector('button').onclick = (event) => {
console.log(event);
};
</script>
addEventListener 사용하기
<button>버튼</button>
<script>
function printLog(event){
console.log(event);
}
// 세 번째 인자로 true를 주면 캡처링 이벤트 리스너로 사용, 기본은 false
document.querySelector('button').addEventListener('click', printLog, ture);
// 이벤트 핸들러 제거
document.querySelector('button').removeEventListener('click', printLog);
</script>
<form>
<div>
<p>p영역</p>
</div>
</form>
버블링
DOM 엘리먼트에 이벤트가 발생할 경우 부모 엘리먼트로 올라가며 차례대로 이벤트가 전파되는 흐름을 버블링이라 한다.
대부분의 이벤트는 버블링을 기본 동작으로 갖는다.
위의 HTML 계층구조에서 p영역을을 클릭할 경우 이벤트 버블링이 실행되며 클릭 이벤트가 최상위 엘리먼트까지 올라간다.
이벤트 발생 순서는 다음과 같다.
p 엘리먼트에서의 클릭 ➡️ div 엘리먼트에서의 클릭 ➡️ form 엘리먼트에서의 클릭 ➡️ body 엘리먼트에서의 클릭 ➡️ html 엘리먼트에서의 클릭 ➡️ document 객체에서의 클릭 ➡️ window 객체에서의 클릭
캡처링
버블링과 반대로 DOM 엘리먼트에 이벤트가 발생한 경우 가장 상위의 부모 엘리먼트부터 자식 엘리먼트로 내려가며 이벤트가 전파되는 것을 캡처링이라 한다.
addEventListener()
의 세 번째 매개변수를 통해 이벤트 리스너를 캡처링 단계에서 실행시킬 수 있다.
흐름 순서
표준 DOM의 이벤트는 캡처링 ➡️ 타깃 ➡️ 버블링의 흐름 순서를 갖는다.
<li>
엘리먼트를 클릭했을 때 원하는 동작을 위해 특정 이벤트 리스너를 등록한다고 가정해보자.
가장 단순한 방법은 <li>
엘리먼트에 이벤트 리스너를 등록하는 것이다.
하지만 <li>
엘리먼트가 100개라면 이벤트 리스너를 100개나 등록해야 한다.
이런 경우 위임을 사용하면 좀 더 간결하게 코드를 작성할 수 있다.
<li>
엘리먼트에 이벤트 리스너를 등록하는 대신 상위 <ul>
엘리먼트에만 이벤트 리스너를 등록하면 된다.
이벤트 위임은 DOM 이벤트를 다룰 때 매우 유용하게 사용할 수 있는 패턴이다.
하지만 이벤트 위임은 이벤트 버블링이 발생하는 경우에만 가능하며, 이벤트가 발생한 target을 기준으로 정의한 것이 아니기 때문에 가독성이 떨어질 수 있다.
BOM은 웹 브라우저와 관련된 객체 모델을 의미한다.
BOM은 DOM과 다르게 표준이 없어 브라우저별로 자유롭게 구현되어 있다.
최상단의 window 인터페이스는 일반적으로 두 가지 의미가 있다.
브라우저 환경에서의 자바스크립트 전역 객체
브라우저 창
window.open("https://naver.com"); // 창 열기
// open("https://naver.com"); window 생략해도 된다.
window.close(); // 창 닫기
// close(); window 생략해도 된다.
History 인터페이스는 브라우저의 세션 기록, 즉 현재 페이지를 불러온 탭 또는 프레임의 방문 기록을 조작할 수 있는 방법을 제공한다.
forward()
, back()
세션 기록 내에서 앞, 뒤로 이동할 수 있다.
go()
현재 위치를 기준으로 오프셋 ( offset) 에 해당하는 숫자를 넣어 위치를 이동할 수 있다.
history.go(1); // forward() 와 동일
history.go(-1); // back() 와 동일
history.go(0); // 새로고침
pushState()
브라우저의 세션 기록 스택에 상태를 추가한다.
const state = { 'page_id': 1, 'user_id': 5 }
const title = '' // 사파리를 제외한 나머지 브라우저에서는 지원안함
const url = 'hello-world.html' // 호출한 곳과 동일한 출처(origin)이어야 한다. 상대
history.pushState(state, title, url)
출처란?
웹 콘텐츠의 출처(origin)는 접근할 때 사용하는 URL의 스킴(프로토콜), 호스트(도메인), 포트로 정의된다.
두 객체의 스킴, 호스트, 포트가 모두 일치하는 경우 같은 출처를 가졌다고 말한다.
replaceState()
브라우저의 현재 세션 기록을 인자로 넘어온 상태로 대체한다.
사용법은 pushState()
와 동일하며, 새롭게 추가되는 것이 아닌 현재 세션이 대체된다는 점이 다르다.
popstate 이벤트는 같은 문서에 대해 히스토리 엔트리가 변화할 때 발생하는 이벤트이다.
popstate 이벤트는 브라우저의 버튼을 눌러 페이지를 이동하거나 go()
, back()
, forward()
와 같은 API를 호출할 때 발생한다.
다만, pushState()
나 replaceState()
를 호출할 때는 발생하지 않는다.
popstate 발생 시 Event 객체에는 세션의 정보인 history.state 가 담긴다.
// Event 객체로 history.state에 접근 가능
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}
이러한 특징은 SPA의 라우터를 구현할 때 사용된다.
뒤로가기 버튼을 눌렀을 때 유입되었던 초기 사이트로 이동되는 것을 방지하기 위해 SPA의 라우터는replaceState()
,pushState()
, popstate 이벤트를 이용해 페이지 이동을 구현한다.
Location 인터페이스는 객체가 연결된 장소(URL)를 표현한다.
Location 인터페이스에 변경을 가하면 연결된 객체에도 반영되는데, Document와 Window 인터페이스가 이런 Location을 가지고 있다.
각각 document.location
과 window.location
으로 접근할 수 있다.
// document.location 또는 window.location으로 접근하는 것이 올바르지만
// 예제를 보여주기 위해 anchor 엘리먼트를 이용하여 나타내었다.
// 아래의 url의 속성은 Location 객체의 속성과 동일한 결과를 나타낸다.
const url = document.createElement('a');
url.href = 'https://developer.mozilla.org:8080/en-US/search?q=URL#search-results-close-container';
console.log(url.href); // https://developer.mozilla.org:8080/en-US/search?q=URL#search-results-close-container
console.log(url.protocol); // https:
console.log(url.host); // developer.mozilla.org:8080
console.log(url.hostname); // developer.mozilla.org
console.log(url.port); // 8080
console.log(url.pathname); // /en-US/search
console.log(url.search); // ?q=URL
console.log(url.hash); // #search-results-close-container
console.log(url.origin); // https://developer.mozilla.org:8080
assign()
매개변수에 해당하는 URL로 이동한다.
history 스택에 추가되며 뒤로 가기 시 이전 페이지로 이동할 수 있다.
location.href가 바뀔 경우 연결된 문서도 새로운 페이지로 이동한다.
그 이유 또한 assign()
메서드가 내부적으로 호출되기 때문이다.
연결된 문서와 다른 오리진에서도 설정할 수 있다.
replace()
assign()
과 동작은 동일하지만 현재 페이지에 대한 history 스택이 초기화된다는 점이 다르다.
reload()
현재 URL의 리소스를 다시 불러온다.
인자고 true를 전달할 경우에는 캐시를 무시하고 다시 불러온다.
Navigator 인터페이스는 사용자 에이전트의 상태와 신원 정보를 나타내며, 스크립트로 해당 정보를 질의할 때와 애플리케이션을 특정 활동에 등록할 때 사용한다.
장치의 네트워크 연결 정보, 장치의 메모리, 장치의 위치 정보 등 다양한 속성을 갖고 있으며
장치를 진동시키는 메서드도 지원한다.
localStorage 와 sessionStorage 를 의미한다.
쿠키와는 다르게 HTTP 헤더를 통한 조작이 불가능하며 서버로 전송되지 않는다.
또한 Web Storage는 origin으로 관리되어 도메인이나 포트가 동일해도 프로토콜이 다를 경우 해당 데이터에 접근할 수 없다.
이런 특징을 이용해 서버로 전송하지 않아도 되는 게시글 임시 저장이나 다크 테마 상태 저장 같은 개인에 맞춰진 UI 상태를 저장하기 적합하다.
속성 및 메서드는 아래와 같다.
length
: 저장된 항목의 개수
key(index)
: index에 해당하는 키를 얻는다.
getItem(key)
setItem(key, value)
removeItem(key)
claer()
localStorage는 origin이 같을 경우 여러 탭과 창에서 공유된다.
또한 세션 이후에도 지속되는 저장소용으로 설계되어 컴퓨터를 종료하거나 브라우저를 종료하더라도 지속된다.
sessionStorage는 한 탭안에서 페이지의 세션이 유지되는 동안 origin별로 스토리지를 관리하므로 여러 탭과 창에서 공유할 수 없다.
페이지가 열려 있는 동안이나 페이지 리로딩 혹은 복원 시에는 데이터가 유지되지만 다른 세션이나 창이 종료될 경우 데이터에 접근할 수 없다.
WebStorage의 데이터가 변경될 때 storage 이벤트가 발생한다.
다른 이벤트와 달리 storage 이벤트는 변경을 일으킨 탭 외에도 해당 스토리지에 접근 가능한 다른 탭 혹은 창에서도 이벤트가 발생한다.
따라서 origin이 같은 탭 혹은 창끼리 통신할 수 있다.
(단, 지원하지 않는 브라우저가 있으므로 확인해야 한다.)
storage 이벤트 객체에는 다음과 같은 정보가 담긴다.
key : 변경된 데이터의 키
oldValue, newValue : 이전 값과 새로운 값을 나타낸다.
데이터가 새로 추가 되었을 시 oldValue 값은 null이 되며 제거될 시 newValue 값은 null이 된다.
url : 문서의 URL을 나타낸다.
storageArea : localStorage 나 sessionStorage 중 갱신이 일어난 WebStorage 객체를 나타낸다.
[참고] : 기초부터 완성까지, 프런트엔드 책