
문서 객체 모델
문서 객체 모델 (Document Object Model, DOM)
은 XHTML, HTML 문서용 API이다. DOM은 일종의 인터페이스로 해당 요소를 나타내는 노드, 노드의 속성을 나타내는 프로퍼티와 이를 조작할 수 있는 여러 메서드를 담아 구조화한 객체로 표현한다. 또한 DOM을 통해 자바스크립트 같은 여러 프로그래밍 언어로 해당 요소에 접근해 해당 구조나 내용, 스타일을 변경할 수 있다. DOM이 없다면 자바스크립트는 웹 페이지에 대한 정보를 얻을 수 없다.
DOM 표준은 W3C DOM, WHATWG DOM에서 살펴볼 수 있으며 브라우저에서 DOM을 구현하는데 기준이 되는 명세가 작성되어 있다. 다만, 브라우저마다 표준에 명시된 기능 외에 별도로 제공되는 추가 기능이 있을 수 있으므로 이점을 주의해야 한다.
DOM API는 Document를 통해 사용할 수 있다. Document
는 DOM 트리의 진입점이자 브라우저가 불러온 웹 페이지를 의미하는 Document 인터페이스를 의미한다.
DOM 트리
DOM은 문서를 노드의 계층적(hierarchical)인 트리 구조로 나타낸다. 노드는 서로 다른 특징, 데이터, 메서드를 가지며 다른 노드와의 관계가 있을 수 있다. 이런 관계는 계층을 생성하고 특정 노드에 뿌리를 둔 트리 노드로 표현된다. 노드 트리의 최상위 노드를 root라 하며 일반적으로 HTML 문서에서 <html>
이 root 노드가 된다.
<!DOCTYPE html>
<html>
<head>
<title>DOM에 대해 공부하자</title>
</head>
<body>
<h1 id="h1-id">DOM에 대해 공부하자</h1>
<!-- 주석 -->
</body>
</html>
트리의 시작점은 Document이며 각각 노드의 타입에 따라 Element 노드, Text 노드, Attr 등으로 분류되어 계층 구조를 가진다.
Text 노드는 각 트리 노드의 리프 노드(leaf node, 맨 마지막 끝 노드)가 되며 BODY 요소 안에 개행이나 띄어쓰기가 존재하는 경우 Text 노드가 만들어진다. 주석 또한 화면에 나타나지 않지만 노드로 만들어져 DOM 트리에 포함된다.
Node
노드 인터페이스는 전체 문서의 요소들에 대한 객체 형태의 기본 데이터 타입이다. DOM 트리의 구성 요소를 나타내며 nodeType이나 nodeName, nodeValue, 자식 노드와 형제 노드에 대한 관계 등 여러 정보를 갖는다. 또한 appendChild()
, removeChild()
같은 노드 조작 메서드 등을 사용할 수 있다.
노드의 계층구조
노드 인터페이스는 트리 형태의 계층 구조가 존재한다. 계층 구조의 제일 꼭대기에는 EventTarget
이 존재한다. 이 EventTarget 추상 클래스는 이벤트가 발생했을 때 대상이 되는 타깃을 의미한다. Node는 EventTarget을 상속받은 추상 클래스로 DOM 노드의 기초가 되며, DOM 노드의 관계를 나타내는 기능들이 포함된다. 차례대로 Element, Text, Comment는 Node 추상 클래스를 상속받아 구현된다.
Element
는 DOM 요소 생성의 가장 기본이 되는 클래스로 요소 탐색(querySelector()
, getElementsByName()
등)부터 이벤트 리스너 관리(addEventListener()
, removeEventListener()
) 등 여러 메서드를 제공한다. HTMLElement
는 Element를 상속받아 HTML 요소를 구현한 기본 클래스이며, 이 HTMLElement를 상속받아 HTMLDivElement, HTMLInputElement, HTMLLabelElement 등 다양한 HTML 요소를 구현하게 된다.
상속 여부는 instanceof를 통해 쉽게 확인할 수 있다.
<select>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<script>
const select = document.querySelector("select");
console.log(select instanceof HTMLSelectElement); // true
console.log(select instanceof HTMLElement); // true
console.log(select instanceof Node); // true
console.log(select instanceof HTMLInputElement); // false
</script>
nodeType, nodeName, nodeValue
노드는 nodeType
(12개의 노드 타입을 정수로 나타냄), nodeName
(노드의 이름), nodeValue
(노드의 값) 등 여러 프로퍼티를 갖는다.
-
ELEMENT_NODE
- nodeType : 1
- nodeName : 요소 태그 이름 (“DIV”, “SPAN”, “INPUT”, …)
- nodeValue : null
- 설명 :
<div>
나<span>
같은 HTML 요소 노드
-
TEXT_NODE
- nodeType : 3
- nodeName : #text
- nodeValue : 포함된 텍스트
- 설명 : Element나 Attr 노드에 담긴 실제 Text 노드
-
COMMENT_NODE
- nodeType : 8
- nodeName : #comment
- nodeValue : 주석 컨텐츠
- 설명 :
<!-- -->
로 작성된 Comment 노드
-
DOCUMENT_NODE
- nodeType : 9
- nodeName : #document
- nodeValue : null
- 설명 : Document 노드
-
DOCUMENT_TYPE_NODE
- nodeType : 10
- nodeName : Doctype 이름(“html”…)
- nodeValue : null
- 설명 :
<!DOCTYPE html>
같은 Doctype을 반환한다.
-
DOCUMENT_FRAGMENT_NODE
- nodeType : 11
- nodeName : null
- nodeValue : null
- 설명 : DocumentFragment 노드
DocumentFragment
는 마크업에 표현되지 않는 유일한 노드 타입이다. DOM에서 사용할 문서 버퍼 노드를 저장하여 경량화된 문서를 정의한다.DocumentFragment 노드를 조작하더라도 문서와 성능에 영향을 주지 않는다. 매번 문서의 DOM 요소를 변경할 경우 성능에 영향이 있을 수 있으므로 아래 예제 코드처럼 버퍼를 만들어 한 번에 업데이트할 수 있다. 단, DocumentFragment는 innerHTML로 HTML 노드를 삽입하는 것이 불가하므로appendChild()
와 같은 메서드를 이용해야만 한다.const fragment = document.createDocumentFragment(); const ul = document.getElementById("ul"); for (let i = 0; i < 3; i += 1) { const li = document.createElement("li"); li.appendChild(document.createTextNode("Hello")); fragment.appendChild(li); } ul.appendChild(fragment);
DOM 프로퍼티
DOM의 프로퍼티에 접근할 경우 자바스크립트 객체 접근 방식과 동일하게 점 표기법과 괄호 표기법 모두 사용할 수 있다.
<input type="checkbox" checked />
<script>
const inputElem = document.querySelector("input");
console.log(inputElem.checked); // true
</script>
미리 정의된 프로퍼티가 아닌 새로운 프로퍼티를 DOM 요소에 지정하고 싶을 경우 자바스크립트 객체에 프로퍼티를 지정하는 것과 동일한 구문으로 작성하면 된다. 각 노드는 어떤 프로퍼티든 가질 수 있으며 내장 프로퍼티에 접근할 경우 대소문자에 맞춰 작성되어야 한다.
document.body.customData = {
date: "2020-12-25",
name: "Christmas",
sayHelloWorld: function () {
console.log("Hello world!");
},
};
document.body.customData.sayHelloWorld(); // Hello world!
하지만 DOM 노드에 커스텀 프로퍼티를 할당해 사용하는 것은 다른 요소 사용 시 혼란을 줄 수 있어 권장하지 않는다.
Node 사이의 관계
최상위 노드를 제외한 모든 DOM 트리의 노드는 각각 하나의 parent를 갖는다. 또한 노드는 여러 개의 child를 가질 수 있다. 이렇게 같은 parent를 가진 노드의 관계를 sibling이라 한다.
DOM 트리에서 갖는 계층 관계를 노드에서 프로퍼티로 제공하며, 이를 이용해서 DOM 트리의 각 노드를 탐색할 수 있다. 해당 프로퍼티의 반환 값은 모두 읽기 전용이다.
parentNode
: 부모 노드를 반환한다.childNodes
: 요소의 자식 노드를 반환한다. 반환형은 NodeList이다.firstChild
: 자식 노드 중 첫 번째 자식을 반환한다. 자식이 없을 경우 null을 반환한다.lastChild
: 자식 노드 중 마지막 자식을 반환한다. 자식이 없을 경우 null을 반환한다.nextSibling
: 부모의 childNodes 목록 중 자신 다음에 있는 노드를 반환한다. 없을 경우 null을 반환한다.previousSibling
: 부모의 childNodes 목록 중 자신 이전에 있는 노드를 반환한다. 없을 경우 null을 반환한다.
프로퍼티를 통해 전체 노드를 순회할 수도 있다.
document.body.childNodes.forEach((node) => {
console.log(node); // DIV, TEXT
});
위 프로퍼티들은 항상 현재의 DOM 상태를 반영한다. 만약 해당 프로퍼티들을 통해 노드를 참조하고 있다면 새로운 DOM 노드가 추가, 제거될 때 같이 변경되므로 주의해서 사용해야 한다. 반환되는 NodeList는 살아있는 유사 배열 객체이다.
DOM Node 추가, 제거하기
DOM API를 사용해 동적으로 기존 노드의 위치를 바꾸거나, 노드를 추가하고, 제거하는 방법에 대해 살펴보자.
createElement
document.createElement(tagName)
을 호출하면 tagName에 맞는 요소 노드를 생성한다.
const divElem = document.createElement("div"); // <div></div>
const liElem = document.createElement("li"); // <li></li>
liElem.className = "li-class-name"; // class 속성 할당
liElem.id = "li-id"; // id 속성 할당
liElem.innerHTML = "<strong>삽입되는 요소</strong>"; // li 내부 HTML 노드 할당
appendChild()
, insertBefore()
이 메서드들을 이용해 노드를 이동하거나 새로운 노드를 추가할 수 있다. appendChild()
는 부모 노드의 마지막에 자식 노드를 추가하는 메서드이며, insertBefore()
는 부모의 특정 노드 앞에 노드를 삽입하는 메서드이다.
appendChild()
와 insertBefore()
메서드 모두 새로 생성한 노드뿐만 아니라 기존 노드의 위치도 이동시킬 수 있다.
<div>
<h1>리스트 시작</h1>
<ol id="ol-id">
<li id="first-li-id">1</li>
<li>2</li>
</ol>
</div>
<script>
const divElem = document.createElement("div");
const liElem = document.createElement("li");
liElem.className = "li-class-name";
liElem.id = "li-id";
liElem.innerHTML = "<strong>삽입되는 요소</strong>";
const olElem = document.getElementById("ol-id");
olElem.appendChild(liElem); // createElement로 생성한 요소 삽입
const firstLiElem = document.getElementById("first-li-id");
olElem.appendChild(firstLiElem); // 기존 요소 삽입
</script>
insertBefore()
의 경우 parentNode.insertBefore(삽입하는 노드, 참조 노드)
형태로 사용할 수 있으며 참조 노드의 앞으로 이동한다. 참조 노드가 null일 경우에는 appendChild()
와 동일하게 작동한다.
<div>
<h1>리스트 시작</h1>
<ol id="ol-id">
<li id="first-li-id">1</li>
<li>2</li>
</ol>
</div>
<script>
const divElem = document.createElement("div");
const liElem = document.createElement("li");
liElem.className = "li-class-name";
liElem.id = "li-id";
liElem.innerHTML = "<strong>삽입되는 요소</strong>";
const olElem = document.getElementById("ol-id");
const firstLiElem = document.getElementById("first-li-id");
olElem.insertBefore(liElem, firstLiElem);
</script>
append()
, prepend()
, after()
, before()
appendChild()
와 insertBefore()
외에도 노드를 이동하거나 새로운 노드를 추가할 수 있는 다양한 메서드들이 있다.
prepend()
: 요소 내부의 가장 앞으로 이동append()
: 요소 내부의 마지막으로 이동before()
: 요소 앞으로 이동after()
: 요소 뒤로 이동
이 메서드들은 문서에 나타내기 위해 어디에 삽입할지 명시해야 한다.
<div>
<h1>리스트 시작</h1>
<ol id="ol-id">
<li>1</li>
<li>2</li>
</ol>
</div>
<script>
const liElem = document.createElement("li");
liElem.id = "li-id";
liElem.innerHTML = "<strong>삽입되는 요소</strong>";
const orderedListElem = document.getElementById("ol-id");
orderedListElem.append(liElem); // 리스트의 가장 마지막 위치에 삽입되며 번호는 3이 매겨진다.
</script>
또한 append()
와 prepend()
메서드는 문자열(DOMString)을 인자로 받을 경우 자동으로 텍스트 노드를 생성하여 삽입한다.
// 직전 예제 코드에 이어서...
orderedListElem.prepend("텍스트 삽입");
// 리스트 가장 앞으로 텍스트 노드가 생성되어 삽입된다.
after()
와 before()
또한 append()
, prepend()
와 동일한 사용법을 가지며 이동시키고 싶은 위치에 따라 메서드를 선택해 사용한다.
append()
vs appendChild()
appendChild()
, insertBefore()
가 이미 존재함에도 불구하고 새롭게 append()
, prepend()
, after()
, before()
가 등장한 이유는 무엇일까? append()
와 appendChild()
를 대표로 두 그룹의 차이점을 비교해보자.
append()
의 경우 Node 객체뿐만 아니라 문자열을 인자로 넘길 수 있다.append()
에 문자열 인자를 넘겨줄 경우, 자동으로 텍스트 노드를 생성해 삽입해준다. 하지만appendChild()
는 문자열을 인자로 넘겨줄 경우 에러가 발생한다.
const bodyElem = document.querySelector("body");
const divElem = document.createElement("div");
const str = "Hello world";
bodyElem.append(divElem); // 가능
bodyElem.appendChild(divElem); // 가능
bodyElem.append(str); // 가능
bodyElem.appendChild(str); // error
append()
의 경우 여러 노드를 한 번에 추가할 수 있다. 하지만appendChild()
는 여러 노드를 추가하고 싶을 경우 메서드를 여러 번 호출해야 한다.
const bodyElem = document.querySelector("body");
const elem1 = document.createElement("div");
const elem2 = document.createElement("div");
bodyElem.append(elem1, elem2); // 두 노드 모두 순서대로 추가
bodyElem.appendChild(elem1, elem2); // 첫 번째 노드만 추가됨. 나머지 노드는 무시된다.
이런 특성 외에 더 유연하고 일관된 사용법을 제공하며 직관적이기 때문에 appendChild()
보다 append()
의 사용을 더 권장하고 있다. 하지만 append()
는 IE 브라우저에서 지원되지 않는다. 그렇기 때문에 저사양의 IE 브라우저를 지원해야 한다면 appendChild()
를 사용해야 한다.
removeChild()
DOM 트리 내에 노드를 제거할 때 사용한다. 제거하고자 하는 노드가 존재할 경우 부모 노드에서 removeChild()
메서드를 호출해 제거한다.
<div id="div-id">id가 div-id인 div 요소</div>
<script>
const divElem = document.getElementById("div-id"); // id가 div-id인 요소 선택
setTimeout(() => {
divElem.parentNode.removeChild(divElem); // 자식 요소 삭제
}, 1000);
</script>
remove()
removeChild()
와 비슷하게 DOM 트리 내에서 노드를 제거하고 싶을 경우 node.remove()
를 이용한다. remove()
메서드는 제거하고 싶은 노드에서 직접 메서드를 호출할 수 있기 때문에 removeChild()
보다 더 간결하게 작성할 수 있다. 하지만 이 메서드 또한 IE에서 지원하지 않으므로 IE를 사용하는 경우 removeChild()
를 사용해야 한다.
<div id="div-id">id가 div-id인 div 요소</div>
<script>
const divElem = document.getElementById("div-id"); // id가 div-id인 요소 선택
setTimeout(() => {
divElem.remove(); // 요소 삭제
}, 1000);
</script>
insertAdjacentHTML()
XML 또는 HTML로 해석될 수 있는 문자열을 파싱한 뒤 적절한 노드를 생성해 특정 위치에 삽입한다.
elem.insertAdjacentHTML("beforebegin", "<span>Hello world!</span>");
지정할 수 있는 위치는 beforebegin, afterbegin, beforeend, afterend 네 가지이다. beforebegin은 요소의 앞에, afterbegin은 요소 안 첫 번째 자식 요소로, beforeend는 요소 안 마지막 자식 요소로, afterend는 요소의 바로 뒤에 삽입된다.
innerHTML과 이름, 역할이 비슷하게 느껴질 수 있지만, innerHTML을 사용할 경우 이미 사용 중인 요소를 전부 없애고 다시 파싱을 하게 된다. innerAdjacentHTML()
은 이미 사용 중인 요소는 파싱하지 않고 새로 추가되는 노드만 작업한다. 이런 특성 때문에 innerAdjacentHTML()
은 innerHTML에 비해 성능상 이점을 갖는다.
요소 검색하기
parentNode, childNodes 같은 노드 프로퍼티를 사용하면 원하는 노드에 직접 접근할 수 있다. 하지만 이 방법은 문서 구조가 자주 변경되거나 계층적으로 깊게 위치한 요소에 접근하는 경우 효율적으로 탐색할 수 없다. 이런 경우 querySelector()
, getElementById()
같은 요소 검색 메서드를 통해 해당 요소에 바로 접근할 수 있다.
getElementById()
요소의 id 속성을 이용해 document.getElementById(id)
로 접근할 수 있다. id는 문서에서 유일하기 때문에 요소를 찾을 때 가장 빠르게 찾을 수 있다. 일치하는 id를 가진 요소가 있을 경우 해당 DOM 요소인 Element 객체를 반환하며 일치하는 요소가 없을 경우 null을 반환한다.
<div id="test-id">id가 test-id인 div 요소</div>
<script>
const elem = document.getElementById("test-id");
// id가 test-id인 요소 가져오기
</script>
querySelector()
와 querySelectorAll()
querySelector()
는 선택자(selector)와 일치하는 첫 번째 요소를 반환한다. 선택자와 일치하는 요소가 없을 경우 null을 반환한다.
const elem = document.querySelector(selectors);
// 선택자와 일치하는 첫 번째 요소 반환
선택자는 CSS 선택자와 동일하게 id는 ‘#’, class는 ‘.’, 요소는 요소 이름을 직접 작성한다. 선택자에 여러 선택자를 ‘,’로 이어서 작성할 경우 ‘or’ 조건으로 동작하기 때문에 선택자에 일치하는 첫 번째 요소를 반환한다.
<div id="test-id">id가 test-id인 div 요소</div>
<p class="class-name">class가 class-name인 p 요소</p>
<script>
const elem1 = document.querySelector("#test-id"); // DIV 요소
const elem2 = document.querySelector(".class-name"); // P 요소
const elem3 = document.querySelector(".class-name, #test-id"); // DIV 요소 (DIV 요소가 P 요소보다 먼저 선언되었기 때문에 첫 번째로 찾아 반환한다.)
</script>
querySelectorAll()
은 선택자와 일치하는 모든 요소를 NodeList 형태로 반환한다. 선택자와 일치하는 요소가 없을 경우 빈 NodeList를 반환한다. 이 NodeList는 살아 있지 않은 유사 배열 객체로 DOM의 변경이 있어도 바뀌지 않는다.
<div id="test-id">id가 test-id인 div 요소</div>
<p class="class-name">class가 class-name인 p 요소</p>
<script>
const elemList = document.querySelectorAll("div");
console.log(elemList); // [DIV#test-id, DIV.class-name]
setTimeout(() => {
const divElem = document.getElementById("test-id");
divElem.remove();
console.log(elemList);
// [DIV#test-id, DIV.class-name]
// querySelectorAll을 호출했을 때 시점의 상태가 저장
}, 1000);
</script>
getElementsBy*
그 외에 태그나 클래시를 이용해 노드에 접근할 수 있는 메서드도 존재한다. 앞서 소개한 getElementById()
와 다르게 이 메서드들은 일치하는 여러 요소를 모두 반환하므로 이름에 ‘s’가 붙은 getElements 형태로 작성된다. 대표적으로 getElementsByClassName()
, getElementsByTagName()
같은 메서드가 존재한다. 반환값은 HTMLCollection으로 실시간으로 업데이트되는 유사 배열 객체이다.
<div id="test-id">id가 test-id인 div 요소</div>
<p class="class-name">class가 class-name인 p 요소</p>
<script>
const elemList1 = document.getElementsByTagName("div"); // [DIV#test-id]
const elemList2 = document.getElementsByClassName("class-name"); // [p.class-name]
</script>
실시간으로 업데이트된다는 의미는 선택자에 해당하는 요소가 바뀔 때 HTMLCollection 또한 업데이트된다는 것을 의미한다.
<div id="test-id">id가 test-id인 div 요소</div>
<script>
const elemList = document.getElementsByTagName("div");
console.log(elemList); // [DIV#test-id]
setTimeout(() => {
const elem = document.createElement("div");
elem.id = "new-div-id";
document.body.appendChild(elem);
console.log(elemList); // [DIV#test-id, DIV#new-div-id] 실시간으로 업데이트
}, 1000);
</script>
DOM 이벤트
이벤트(Event)
는 어떠한 일(또는 인터렉션)이 발생했을 때 그 시점에 발생하는 신호를 의미한다. DOM에서는 요소를 클릭하거나 특정 키를 입력했을 때처럼 다양한 상황에서 이벤트가 발생한다.
Event 객체
Event 객체는 DOM 내에서 발생한 이벤트에 대한 정보를 담고 있다. Event 객체는 발생한 이벤트의 종류부터 요소에 대한 정보, 캡처링 여부, 이벤트 발생 위치 등 여러 정보를 갖고 있다.
target과 currentTarget
이벤트가 발생한 요소에 접근하고 싶은 경우 target 혹은 currentTarget 프로퍼티를 통해 접근할 수 있다. 두 프로퍼티는 같은 값을 갖기도 하고 다른 값을 갖기도 해 많은 사람이 헷갈려하는 프로퍼티 중 하나이다.
target
: 이벤트가 처음 발생했던 대상 DOM 요소의 참조를 갖는다.currentTarget
: 발생한 이벤트가 등록된 DOM 요소의 참조를 갖는다.
<ul id="list">
<li id="1">list1</li>
<li id="2">
<!-- 클릭 -->
list2
</li>
<li id="3">list3</li>
</ul>
<script>
const list = document.getElementById("list");
list.addEventListener("click", (ev) => {
console.log(ev.target.id); // 2
console.log(ev.currentTarget.id); // list
});
</script>
이 예제 코드에서는 id가 list인 ul 요소에 이벤트를 추가했고 ul 요소 내부에 id가 2인 li 요소를 클릭했다. 이 경우 이벤트가 처음 발생한 요소는 li 요소이기 때문에 이 요소가 target이 되고, 이벤트가 등록된 ul 요소는 currentTarget이 된다.
stopPropagation()
과 preventDefault()
Event 객체에는 여러 값뿐만 아니라 Event를 제어할 수 있는 메서드도 존재한다.
preventDefault()
는 이벤트를 취소할 수 있는 경우 이벤트를 취소한다. 대신 전파(캡처링, 버블링)되는 이벤트를 막지 않으며, 현재 이벤트의 기본 동작만 중단한다. stopPropagation()
은 preventDefault()
처럼 기본 동작을 중단하지는 못하지만 이벤트가 전파되는 것을 막는다.
<div id="div-id">
div 영역
<p id="p-id">
p 영역
<a href="https://www.google.com/" id="a-id">google로 이동</a>
</p>
</div>
<script>
const div = document.getElementById("div-id");
const p = document.getElementById("p-id");
const a = document.getElementById("a-id");
div.addEventListener("click", () => {
console.log("div 클릭!");
});
p.addEventListener("click", () => {
console.log("p 클릭!");
});
a.addEventListener("click", () => {
console.log("a 클릭!");
});
</script>
p 영역을 클릭할 경우 이벤트 버블링으로 인해 ‘p 클릭!’, ‘div 클릭!’이 순서대로 발생한다.
p 영역에서 클릭 이벤트가 발생한 뒤 상위 요소로 이벤트가 전파되지 않도록 막고 싶다면 stopPropagation()
을 호출하여 막을 수 있다.
p.addEventListener("click", (ev) => {
console.log("p 클릭!");
ev.stopPropagation();
});
a.addEventListener("click", () => {
console.log("a 클릭!");
});
a 요소를 클릭했을 경우 ‘a 클릭!’ 로그가 출력되며 구글 페이지로 이동하게 된다. a 요소의 기본 동작이 링크를 통해 페이지를 이동하는 것이기 때문인데, 이러한 기본 동작을 중단시키고 싶을 경우 preventDefault()
를 사용한다.
a.addEventListener("click", (ev) => {
console.log("a 클릭!");
ev.preventDefault();
});
이벤트 종류
User Interface 이벤트
User Interface(UI) 이벤트는 문서나 요소의 조작 시 발생하는 이벤트이다.
load
: 문서나 종속된 리소스(이미지나 스크립트 파일, CSS 파일 등)가 완전히 로드되었을 때 이벤트가 발생한다. 일반적으로 window에 접근할 수 없어 주로<body>
에 이벤트 리스너를 할당해 사용한다.unload
: 문서나 종속된 리소스(이미지나 스크립트 파일, CSS 파일 등)가 완전히 제거되었을 때 이벤트가 발생한다. 주로 페이지를 완전히 종료하거나 다른 페이지로 이동 시 발생한다. 메모리 누수를 방지하는 목적으로 주로 사용된다.abort
: 리소스가 중단된 경우(로드가 진행되는 동안 사용자가 취소했을 때 같은 경우) 이 이벤트가 발생한다.error
: 리소스가 로드되었지만 유효하지 않을 때, 스크립트 실행 오류, 잘못된 형식 등의 에러가 발생했을 때 이 이벤트가 발생한다.select
:<input>
,<textarea>
요소 안에 작성된 텍스트를 선택할 경우 발생한다.
Focus 이벤트
Focus 이벤트는 대상이 포커스를 받거나 잃을 때 발생하는 이벤트이다.
focusin
: 포커스를 받으려 할 때 이벤트가 발생한다. 포커스가 실제로 가기 전 이벤트가 발생한다. target은 이벤트를 받을 요소이다.focusout
: 포커스를 잃으려 할 때 이벤트가 발생한다. 포커스가 실제로 잃기 전 이벤트가 발생한다. target은 이벤트를 잃게 될 요소이다.focus
: 대상이 포커스를 받을 때 이벤트가 발생한다. focusin과 비슷하지만 포커스를 받은 뒤 이벤트가 발생해 시점의 차이가 있다.blur
: 대상이 포커스를 잃을 때 이벤트가 발생한다. focusout과 비슷하지만 포커스를 잃은 뒤 이벤트가 발생해 시점의 차이가 있다.
마우스 이벤트
마우스 이벤트는 마우스 또는 트랙패드, 트랙볼 같은 포인팅 입력 장치를 탐지한다. 마우스 이벤트는 항상 가장 깊이 중첩된 요소를 대상으로 한다.
mousedown
: 타깃을 눌렀을 때 이벤트가 발생한다. mousedown 이벤트의 기본 동작을 취소할 경우 click이나 dbclick 같은 이후 이벤트를 방지할 수 있다.mouseup
: 타깃의 위에서 눌렀던 포인트가 해제될 때 이 이벤트가 발생한다. mousedown과 마찬가지로 해당 이벤트의 기본 동작을 취소할 경우 이후 이벤트를 방지할 수 있다.click
: 타깃에 클릭 동작을 했을 경우 이벤트가 발생한다. mousedown과 mouseup 이벤트가 발생한 뒤 발생한다.dbclick
: 더블 클릭을 했을 경우 이벤트가 발생한다. click 이벤트의 동작을 취소해도 dbclick 이벤트는 동일하게 발생한다.mousemove
: 타깃 내에서 포인터가 이동할 경우 이벤트가 발생한다. 지속적으로 마우스 포인터를 추적할 필요가 있을 때 주로 사용한다.mouseenter
: 타깃 밖에서 타깃으로 처음 포인터가 이동할 때 발생한다. 포인터가 자식 요소에 올라갈 때는 발생하지 않는다. 요소에 처음 진입했을 때를 탐지할 때 주로 사용한다.mouseleave
: 타깃 밖으로 이동할 때 발생한다.mouseover
: mouseenter와 동일한 조건에서 발생한다. mouseenter와 다른점은 이벤트가 버블링이 발생한다.mouseout
: mouseout과 동일한 조건에서 발생한다. mouseout과 다른점은 이벤트가 버블링이 발생한다.
마우스 이벤트의 경우 Event 객체에 이벤트가 발생한 시기의 마우스 좌표를 제공한다. 대표적으로 client, offset, page* 등이 그에 해당하는 좌표이다. 이 값들은 스크롤되어 있는 화면을 포함하는 영역을 기준으로 하는지, 현재 보이는 페이지만 기준으로 하는지, 모니터 영역을 기준으로 하는지 등 각각의 기준으로 값을 계산해 반환한다.
<div id="wrapper-id" style="height:100px; width:100px; border: 1px solid #ccc;">
mouse move 탐지 영역
</div>
<script>
const wrapper = document.getElementById("wrapper-id");
// 영역 내에 이벤트가 발생한 지점의 좌표를 반환
wrapper.addEventListener("mousemove", (ev) => {
console.log(ev.offsetX, ev.offsetY);
console.log(ev.clientX, ev.clientY);
});
</script>
Input 이벤트
Input 이벤트는 편집 가능한 영역에서의 키보드 입력이나 삭제, 서식 지정 등을 통해 DOM 요소가 업데이트될 때 발생하는 이벤트이다.
beforeInput
: 입력 후 DOM 요소에 업데이트되기 전 이벤트가 발생한다.input
: 입력 후 DOM 요소에 업데이트된 뒤 바로 이벤트가 발생한다.
키보드 이벤트
키보드 이벤트는 키보드를 누르는 시점과 떼는 시점에 따라 이벤트가 발생한다.
keydown
: 키보드를 누를 때 발생한다.keyup
: 누른 키를 뗄 때 발생한다.
키보드 이벤트의 경우 Event 객체에서 어떤 키를 입력했는지, 함께 누른 키는 어떤 키인지에 대한 정보를 함께 제공한다. 대표적으로 key, ctrlKey, altKey, shiftKey, metaKey 등이 존재한다.
key는 입력한 키를 문자열로 반환하며 ctrlKey, altKey, shiftKey, metaKey 값은 해당 버튼을 입력했는지 불리언 값으로 반환한다.
<input type="text" id="input-id" />
<script>
const input = document.getElementById("input-id");
input.addEventListener("keydown", (ev) => {
if (ev.key.toUpperCase() === "S" && ev.ctrlKey) console.log("save!");
});
</script>
이벤트 리스너 추가하기
이벤트 리스너를 추가하는 방법은 DOM 프로퍼티로 할당하는 방법, HTML 요소에 속성으로 할당하는 방법, addEventListener()
세 가지가 있다.
HTML요소 속성으로 할당하기
on<event>
형태로 되어 있는 속성에 리스너를 할당할 수 있다. 속성에 들어가는 코드는 자바스크립트 코드이다.
<button onClick="alert('hello world!')">alert 띄우기</button>
별도로 작성한 자바스크립트 이벤트 리스너를 등록하고 싶은 경우 다음 예제 코드처럼 on<event>
속성에 삽입해야 한다. 또한 HTML 속성을 할당할 때 event를 인자로 넘겨줄 경우 이벤트 정보를 가진 Event 객체를 사용할 수 있다.
<button id="button-id" onClick="clickEventHandler(event)">클릭!</button>
<script>
function clickEventHandler(ev) {
console.log(ev); // Event
}
</script>
DOM 프로퍼티로 할당하기 (DOM level 0)
DOM 프로퍼티를 할당하여 이벤트 리스너를 등록할 수도 있다. DOM 요소의 on<event>
형태의 프로퍼티에 함수를 할당하면 이벤트 리스너로 등록된다.
<button id="button-id">alert 띄우기</button>
<script>
document.getElementById("button-id").onclick = (event) => {
alert("hello world");
};
</script>
이 때 onclick에 할당되는 리스너 함수는 이벤트 정보를 담은 Event 객체를 매개변수로 갖는다. 하지만 HTML 요소의 속성으로 이벤트 리스너를 등록하거나 DOM 프로퍼티로 이벤트 리스너를 등록하는 방법은 동일한 이벤트를 대상으로 여러 이벤트 리스너를 동시에 등록할 수 없다는 단점이 있다. 이벤트 리스너를 등록한 뒤 새로운 이벤트 리스너를 할당할 경우 기존 값은 덮어씌워지기 때문에 마지막에 할당된 이벤트 리스너만 실행된다.
addEventListener 사용하기 (DOM level 2)
타깃이 되는 요소에 addEventListener()
를 사용해 이벤트 리스너를 등록할 수 있다. addEventListener()
는 앞 두 방식과 다르게 한 이벤트 타입에 여러 개의 리스너를 등록할 수 있으며 버블링을 사용할지, 캡처링을 사용할지 등 정밀한 제어를 할 수 있어 가장 권장되는 방식이다. 이 때 이벤트 리스너에는 발생한 이벤트의 정보가 담긴 Event 객체가 매개변수로 포함된다.
<button id="button-id">alert 띄우기</button>
<script>
// 'click'으로 타입 명시
document.getElementById("button-id").addEventListener(
"click",
(ev) => {
alert("hello world!");
},
true
); // 캡처링(capturing) 사용
</script>
할당한 이벤트를 해제하려면 removeEventListener()
메서드를 사용한다. 해제하고 싶은 이벤트 리스너의 참조를 인자로 넘겨주면 이벤트가 해제된다. 이 때 가장 많이 하는 실수로, 등록한 이벤트 핸들러와 동일한 형태의 함수를 선언하여 넘겨주는 경우가 있는데 그런 경우 이벤트 리스너는 해제되지 않는다. 반드시 동일한 참조를 가진 리스너를 넘겨주어야 올바르게 해제할 수 있다.
function sayHello() {
alert("hello");
}
document.addEventListener("click", sayHello); // 이벤트 핸들러 등록
document.removeEventListener("click", sayHello); // 이벤트 핸들러 제거
document.removeEventListener("click", function () {
alert("hello");
}); // 이벤트 핸들러가 제거되지 않는다.
버블링과 캡처링
계층적 구조를 가진 DOM에 이벤트가 발생할 경우 연쇄적으로 이벤트가 전파(Propagation)된다. 그리고 이벤트가 전파되는 방향에 따라 버블링(Bubbling)과 캡처링(Capturing)으로 구분된다.
버블링(Bubbling)
DOM 요소에 이벤트가 발생할 경우 부모 요소로 올라가며 차례대로 이벤트가 전파되는 흐름을 버블링이라고 한다. 대부분의 이벤트는 버블링을 기본 동작으로 갖는다.
<form id="form-id">
form 영역
<div id="div-id">
div 영역
<p id="p-id">p 영역</p>
</div>
</form>
<script>
const form = document.getElementById("form-id");
const div = document.getElementById("div-id");
const p = document
.getElementById("p-id")
[(form, div, p)].forEach((target) => {
target.addEventListener("click", () => {
console.log(`${String(target.tagName)} 클릭!`); // 클릭한 DOM 요소의 태그 이름 출력
});
});
</script>
p 영역을 클릭할 경우 등록된 이벤트 리스너가 실행되며, 부모 요소에 등록된 클릭 이벤트 리스너 또한 차례대로 실행된다. 따라서 “P 클릭!”, “DIV 클릭!”, “FORM 클릭!” 이 순서대로 출력된다.
캡처링(Capturing)
버블링과 반대로 DOM 요소에 이벤트가 발생할 경우 가장 상위의 부모 요소부터 자식 요소로 내려가며 이벤트가 전파되는 것을 캡처링이라 한다. addEventListener()
의 세 번째 옵션을 통해 이벤트 리스너를 캡처링 단계에서 실행시킬 수 있다.
[form, div, p].forEach((target) => {
target.addEventListener(
"click",
() => {
console.log(`${String(target.tagName)} 클릭!`);
},
true
); // 캡처링 이벤트 핸들러
});
캡처링 이벤트 리스너를 등록한 후 동일하게 p 영역을 클릭하면 버블링과는 반대로 상위 요소인 <form>
부터 <div>
, <p>
의 순서로 이벤트가 발생한다.
표준 DOM의 이벤트는 캡처링, 타킷, 버블링의 흐름 순서를 갖는다. 만약 특정 DOM 요소에 등록된 이벤트 리스너가 버블링과 캡처링을 모두 사용한다면 DOM 이벤트 흐름 순서를 명확히 이해해야 혼란 없이 사용할 수 있다.
[form, div, p]
.forEach((target) => {
target.addEventListener(
"click",
() => {
console.log(`${String(target.tagName)} 클릭!`);
},
true
); // 캡처링 이벤트 핸들러
})
[(form, div, p)].forEach((target) => {
target.addEventListener("click", () => {
console.log(`${String(target.tagName)} 클릭!`);
}); // 버블링 이벤트 핸들러
});
이 코드에서는 버블링 이벤트와 캡처링 이벤트 리스너가 모두 등록되어 있다. p 요소를 클릭했을 경우 DOM 이벤트 발생 흐름에 따라 다음과 같이 전파된다.
FORM 캡쳐링 -> DIV 캡쳐링 -> P 캡쳐링 -> P 버블링 -> DIV 버블링 -> FORM 버블링
이벤트 위임
DOM 요소의 이벤트 흐름을 이용해 이벤트 제어를 상위 요소로 위임(delegation)할 수도 있다.
<ul id="article">
<li id="article-1">게시글 1</li>
<li id="article-2">게시글 2</li>
<li id="article-3">게시글 3</li>
<!-- 수많은 게시글들 -->
</ul>
<li>
요소를 클릭했을 때 특정 이벤트 리스너를 등록한다고 가정해보자. 이벤트 리스너를 등록하는 가장 단순한 방법은 addEventListener()
를 사용해 각각의 <li>
요소에 이벤트 리스너를 등록하는 것이다.
const firstArticle = document.getElementId("article-1");
const secondArticle = document.getElementId("article-2");
const thirdArticle = document
.getElementId("article-3")
[(firstArticle, secondArticle, thirdArticle)].forEach((target) => {
target.addEventListener("click", () => {
console.log(target.id);
});
});
이 예제 코드처럼 모든 요소에 이벤트 리스너를 등록할 경우 불필요한 코드나 중복된 코드가 양산될 수 있다. CSS 선택자나 반복문을 사용한다면 중복 코드를 개선할 수 있지만, 동적으로 추가되는 요소들의 이벤트는 제어하기 어려울 수 있다. 이런 경우 위임을 사용하면 좀 더 간결하게 코드를 작성할 수 있다.
const articleList = document.getElementById("article");
articleList.addEventListener("click", (ev) => {
console.log(ev.target.id);
});
각각의 <li>
요소에 이벤트 리스너를 등록하는 대신 상위 <ul>
요소에만 이벤트 리스너를 등록했다. 이런 경우 이벤트 버블링을 통해 이벤트가 발생한 target 요소로부터 상위 요소로 이벤트가 전파된다. 이렇게 이벤트 버블링을 이용하여 이벤트를 제어하는 패턴을 이벤트 위임이라고 하며, 이벤트 위임을 사용하면 동적으로 추가되는 요소나 많은 자식 요소들의 이벤트를 효과적으로 다룰 수 있다.
위임은 DOM 이벤트를 다룰 때 매우 유용하게 사용할 수 있는 패턴이지만, 이벤트 위임은 이벤트 버블링이 발생하는 경우에만 가능하며 이벤트가 발생한 target을 기준으로 정의한 것이 아니기 때문에 이벤트 동작을 파악할 때 가독성이 떨어질 수 있다.
브라우저 객체 모들
DOM이 문서를 다룰 수 있도록 만들어진 객체 모델이라면, 브라우저 객체 모델(Browser Object Model, BOM)은 웹 브라우저와 관련된 객체 모델을 의미한다. BOM은 DOM과 다르게 표준이 없어 브라우저별로 자유롭게 구현되어 있다.
window 객체
브라우저에서는 자바스크립트가 구동되기 위한 환경과 여러 가지 기능을 제공하고 있다. 브라우저에서 제공하는 여러 가지 기능들은 window 인터페이스를 통해 사용할 수 있다.
위 그림은 구동되고 있는 환경이 브라우저일 때 제공되는 기능을 나타내고 있다. 최상단의 window 인터페이스는 일반적으로 두 가지 의미가 있다. 첫 번째로 브라우저 환경에서의 자바스크립트 전역 객체를 의미한다. 따라서 window를 통해 전역에 선언된 변수나 함수에 접근할 수 있다.
// 전역 함수로 선언
function sayHelloWorld() {
console.log("hello world");
}
// 전역 함수에 window 객체를 통해 접근
console.log(window.sayHelloWorld()); // hello world
두 번째로 브라우저 창(window)을 의미한다. 창을 닫거나 경고를 띄우고, 새 창을 띄우는 등 여러 메서드를 제공한다. 여러 탭을 다루는 브라우저는 각각의 탭을 window로 보게 된다.
// alert 메서드 사용
window.alert("Hello world!");
// 창 내부의 너비 출력
console.log(window.innerWidth);
// 창 열기
window.open("https://google.com");
// 창 닫기
window.close();
window로 접근할 수 있는 메서드나 변수는 window가 생략된 상태에서 사용할 수 있다.
alert("Hello world!");
console.log(innerWidth);
open("https://google.com");
close();
History 객체
History 객체는 현재 브라우저의 세션 기록(현재 문서의 탭이나 프레임에서의 방문 기록)을 갖고 있다. window.history로 History 객체에 접근할 수 있으며 보안상의 이유로 사용자가 방문한 목록을 보는 것은 불가능하다. 하지만 현재 페이지를 기준으로 세션 기록 내의 앞뒤로 이동하거나 특정 위치로 이동시킬 수 있는 go()
나 back()
, pushState()
같은 메서드를 제공한다. 메서드뿐만 아니라 현재 세션에 연결할 상태를 저장하는 state 객체 또한 존재한다. 이를 통해 페이지를 이동할 때 기억하고 있어야 할 값들을 저장할 수 있다.
forward()
, back()
세션 기록 내에서 앞으로 이동하려면 forward()
, 뒤로 가려면 back()
을 사용한다.
history.forward();
history.back();
go()
현재 위치를 기준으로 오프셋(offset)에 해당하는 숫자를 넣어 위치를 이동할 수 있다.
history.go(1); // forward()와 동일하다.
history.go(-1); // back()과 동일하다.
history.go(-2); // 현재 위치를 기준으로 뒤로 2페이지 이동한다.
history.go(0); // 현재 페이지를 새로고침한다.
pushState()
브라우저의 세션 기록에 상태를 추가한다. 새로운 세션으로 넘겨줘야 할 정보를 나타내는 state, 이동할 페이지의 title, 그리고 이동할 페이지의 url을 매개변수로 가진다. title은 대부분의 브라우저에서 아직 지원하지 않아 빈 문자열을 넘겨 사용한다. 새로운 url은 호출한 곳과 동일한 출처(origin)이어야 한다.
const state = { userId: 123 };
const title = "";
const url = "/docs/1";
history.pushState(state, title, url);
window.location.href에 값을 넣어 페이지를 이동하는 것과 비슷하게 느껴질 수 있지만 동작은 전혀 다르다. pushState()
의 경우 새로운 HTTP 호출을 만들지 않아 페이지의 존재 여부와 상관없이 추가된다.
replaceState()
브라우저의 현재 세션 기록을 인자로 넘어온 상태로 대체한다. 메서드의 매개변수와 사용법은 pushState()
와 동일하며, 새롭게 추가되는 것이 아닌 현재 세션이 대체된다는 점이 다르다.
const state = { userId: 123 };
const title = "";
const url = "/docs/1";
history.replaceState(state, title, url);
pushState()
, replaceState()
그리고 popstate 이벤트
popstate는 같은 문서에 대해 히스토리 엔트리가 변화할 때 발생하는 이벤트이다. popstate 이벤트는 브라우저의 버튼을 눌러 페이지를 이동하거나 go()
, back()
, forward()
같은 API를 호출할 때 발생한다. 다만, pushState()
나 replaceState()
를 호출할 때는 발생하지 않는다. popstate 발생 시의 Event 객체에는 세션의 정보인 history.state가 담긴다.
<button id="pushStateBtn">pushState</button>
<button id="backBtn">backBtn</button>
<script>
const pushStateBtn = document.querySelector("#pushStateBtn");
const backBtn = document.querySelector("#backBtn");
pushStateBtn.addEventListener("click", () => {
history.pushState({ id: 100 }, "", "/docs/1");
});
backBtn.addEventListener("click", () => {
history.back();
});
window.addEventListener("popstate", (ev) => {
console.log(ev.state); // history.state 접근 가능
});
</script>
이러한 특징은 SPA(Single-Page Application)의 라우터를 구현할 때 사용된다. SPA 환경에서는 화면 간 이동 시 전체 화면을 다시 그리지 않아 빠른 전환이 가능하다. 즉 기존 방식의 이동을 사용한다면 뒤로 가기 버튼을 눌렀을 때 유입되었던 초기 사이트로 이동될 것이다. 이런 동작을 방지하기 위해 SPA 라우터는 replaceState()
, pushState()
, popstate를 이용해 페이지 이동을 구현한다.
Location 객체
Location 객체는 현재 페이지의 URL, 프로토콜, hostname, 포트 번호 등 위치에 관련된 정보를 포함한다. window.location
혹은 document.location
으로 현재 문서의 위치 정보를 나타내는 Location 객체에 접근할 수 있다.
URL 예시 : https://test.com:443/tutorial?value=123#what-are-we-building
href
: 온전한 URL을 나타낸다. 이 값이 바뀔 경우 문서가 새로운 페이지로 이동한다.- https://test.com:443/tutorial?value=123#what-are-we-building
protocol
: URL의 프로토콜로 ‘:’ 까지 포함된다.- https:
host
: URL의 호스트 부분을 나타내며 hostname과 ‘:’, 포트 번호를 포함한다.- test.com:443
hostname
: URL의 도메인을 나타낸다.- test.com
port
: URL에 포트 번호가 포함되어 있을 경우 포트 번호를 나타낸다. 포트 번호가 명시되어 있지 않은 경우 빈 문자열 ‘‘이 된다.- 443
pathname
: ‘/’ 뒤 URL의 경로를 나타내는 문자열이다.- /tutorial
search
: ‘?’ 뒤 URL의 쿼리스트링을 나타낸다.- ?value=443
hash
: ‘#’ 뒤 URL의 프래그먼트 식별자를 나타낸다.- #what-are-we-building
Location 객체는 또한 location을 조작할 수 있는 메서드들을 제공한다.
assign()
매개변수에 해당하는 URL로 이동한다. history 스택에 추가되며 뒤로 가기 시 이전 페이지로 이동할 수 있다. location.href에 URL을 지정했을 때 이동하게 되는 이유 또한 assign()
메서드가 호출되기 때문이다.
location.assign("https://test.com");
// https://test.com으로 이동
location.assign("/tutorial");
// 현재 도메인의 pathname을 "/tutorial"로 한다.
replace()
매개변수에 해당하는 URL로 이동한다. assign()
과 동작은 동일하지만 현재 페이지에 대한 history 스택이 초기화된다는 점이 다르다.
location.replace("https://test.com");
// https://test.com으로 이동
location.replace("/tutorial");
// 현재 도메인의 pathname을 "/tutorial"로 한다.
reload()
현재 URL의 리소스를 다시 불러온다. 인자로 true를 전달할 경우 캐시를 무시하고 다시 불러온다.
location.reload();
navigator 객체
Navigator 인터페이스의 navigator 객체는 클라이언트의 ID 및 상태를 나타낸다. window.navigator나 navigator로 객체에 접근할 수 있으며 사용자의 브라우저 이름과 벤더사, 모바일 사용 여부, 버전 등의 정보를 가진다. 다만, navigator 객체에서 제공하는 정보가 브라우저마다 다를 수 있으므로 이 부분을 꼭 확인하여 사용하는 것을 권장한다.
appVersion
: 브라우저의 버전이 반환된다.platform
: 브라우저가 실행되고 있는 플랫폼이 반환된다. MacIntel, Win32, FreeBSD i386, WebTV OS 혹은 빈 문자열이 반환된다.vendor
: 브라우저 공급 업체를 나타낸다. 브라우저 호환 모드에 따라 Google Inc, Apple Computer, Inc., 빈 문자열이 반환된다.userAgent
: 사용자 에이전트가 담긴 User-Agent 문자열이 반환된다. HTTP 헤더와 navigator 등을 통해 접근할 수 있다. 반환되는 문자열은 여러 정보로 분해될 수 있게 구조화되어 반환된다. 하지만 브라우저마다 추가되는 정보가 있으므로 주의해서 사용해야 한다. 일반적으로 사용되는 User-Agent의 형태는User-Agent: Mozilla/5.0(<system-information>)<platform>(<platform-details>)<extensions>
이다.serviceWorker
: 서비스 워커는 브라우저가 백그라운드에서 실행하는 스크립트로 웹페이지와는 별개로 작동하며, 웹페이지 또는 사용자와의 상호작용이 필요하지 않은 푸시 알림 및 백그라운드 동기화와 같은 기능에 사용한다.
console.log(navigator.userAgent); // Mac OSX Chrome 86.0.4240.75 사용
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36"
2020년 크롬 브라우저는 userAgent의 지원 여부를 점차적으로 중단해 나가겠다고 발표했다. 90년대 NetScape 브라우저의 일부로 개발되었던 UA는 이후 광고주나 다른 사람들이 웹 사이트 방문자를 추적하고 탐지할 때 사용되었다. 이런 개인정보 문제 외에 사이트에서 브라우저를 판단해 특정 기능을 제한하는 등의 문제가 발생했고 이런 문제를 해결하기 위해 크롬팀은 단계적으로 UA의 중단과 새로운 대안으로 “Client Hints”를 제안했다.
“Client Hints”는 기존 UA에서 공개되고 있던 OS의 버전이나 브라우저의 버전, 핸드폰의 종류 등 여러 구체적인 정보들이 공개되지 않는다. “Client Hints”는 아직 정식 명세로 인정받지 않았지만 애플(Safari), MS(Edge), Mozilla(Firefox) 또한 UA를 점진적으로 제거하는 제안에 지지를 표했기 때문에 이후 UA의 사용에 큰 변화가 있을 것이라 예상된다.
이 외에 사용자의 장치의 위치 정보에 접근할 수 있는 Geolocation
객체나 장치를 진동시키는 vibrate()
등 여러 프로퍼티와 메서드가 제공된다.
Web Storage
Web Storage는 클라이언트 측에 이름:값 쌍의 데이터를 저장하는 두 가지 메커니즘을 의미한다. 첫 번째는 localStorage, 두 번째는 sessionStorage이다. 브라우저에 따라 제한 용량은 다를 수 있지만, 대부분의 브라우저 모두 2MB 이상의 데이터를 저장할 수 있다. Web Storage의 데이터는 쿠키와 다르게 HTTP 헤더를 통한 조작이 불가능하며 서버로 전송되지 않는다. 또한 Web Storage는 도메인, 프로토콜, 포트로 구성된 origin으로 관리되어 도메인이나 포트가 동일해도 프로토콜이 다를 경우 해당 데이터에 접근할 수 없다. 이런 특징을 이용해 서버로 전송하지 않아도 되는 게시글 임시 저장이나 다크 테마 상태 저장 같은 개인에 맞춰진 UI 상태를 저장하기 적합하다.
getItem(key)
: 키에 해당하는 값을 얻는다. 해당하는 값이 없으면 null을 반환한다.setItem(key, value)
: 키에 해당하는 값을 설정한다.removeItem(key)
: 키에 해당하는 값을 삭제한다.clear()
: 저장된 모든 키-값 쌍을 제거한다.key(index)
: index에 해당하는 키를 얻는다.length
: 저장된 항목의 개수를 얻는다.
localStorage.setItem("TEST-VALUE", 123);
localStorage.getItem("TEST-VALUE"); // "123"
localStorage.key(0); // "TEST-VALUE"
localStorage.removeItem("TEST-VALUE"); // 제거
localStorage.getItem("TEST-VALUE"); // null
값은 항상 문자열이며 다른 타입의 값을 넣어도 문자열 형토래 타입이 변환되어 저장된다. 객체의 형태로 데이터를 저장하고 싶은 경우 JSON.stringify()
와 JSON.parse()
를 통해 저장할 수 있다.
const obj = { name: "hanjung", age: 24 };
localStorage.setItem("OBJECT-DATA", obj);
localStorage.getItem("OBJECT-DATA"); // [object Object]
localStorage.setItem("OBJECT-STRINGIFY-DATA", JSON.stringify(obj));
const data = localStorage.getItem("OBJECT-STRINGIFY-DATA"); // "{"name" : "hanjung", "age": 24}"
console.log(JSON.parse(data)); // {name: "hanjung", age: 24}
localStorage와 sessionStorage의 차이
localStorage는 origin이 같을 경우 여러 탭과 창에서 공유된다. 또한 세션 이후에도 지속되는 저장소용으로 설계되어 컴퓨터를 종료하거나 브라우저를 종료하더라도 지속된다.
sessionStorage는 한 탭에서 페이지의 세션이 유지되는 동안 origin별로 스토리지를 관리한다. 페이지가 열려 있는 동안이나 페이지 리로딩 혹은 복원 시에는 데이터가 유지되지만 다른 세션이나 창이 종료될 경우 데이터에 접근할 수 없다. localStorage보다 좀 더 제한적으로 사용된다.
storage 이벤트
WebStorage의 데이터가 변경될 때 storage 이벤트가 발생한다.
window.addEventListener("storage", (ev) => {
console.log(ev);
});
storage 이벤트의 이벤트 객체에는 데이터에 대한 여러 가지 정보가 담긴다.
key
: 변경된 데이터의 키를 나타낸다.clear()
시 null이 반환된다.oldValue
,newValue
: 이전 값과 새로운 값을 나타낸다. 새로 추가되었을 경우oldValue
는 null이 되며 제거될 경우newValue
는 null이 된다.url
: 문서의 URL을 나타낸다.storageArea
: 갱신이 일어난 WebStorage 객체를 나타낸다.
다른 이벤트와 다른 점은 storage 이벤트는 변경을 일으킨 탭 외에도 해당 스토리지에 접근 가능한 다른 탭 혹은 창에서도 이벤트가 발생한다. 즉 origin이 같은 창끼리 통신할 수 있다. 하지만 지원하지 않는 브라우저가 많으므로 지원 여부를 살펴본 뒤 사용해야 한다.
< 참고 : https://velog.io/@solmii/TIL-DOM%EC%9D%B4%EB%9E%80
https://ko.javascript.info/basic-dom-node-properties
https://github.com/john015/TIL/blob/master/javaScript/dom.md
https://postlude.github.io/2020/02/16/javascript-object/
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=diceworld&logNo=220180222883 >