2024. 11. 27. 16:37ㆍ기록/Modern JavaScript Deep Dive
- 목차
DOM
HTML 문서의 계층적 구조와 정보를 표현하며
이를 제어할 수 있는 API인 프로퍼티와 메서드를 제공하는 트리 자료구조
노드
<div class="cat">Hello</div>
- <div : 시작태그
- class : 어트리뷰트 이름
- cat : 어트리뷰트 값
- Hello : 콘텐츠
- </div> : 종료 태그
HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환됨
- HTML 요소의 어트리뷰트 => 어트리뷰트 노드
- HTML 요소의 텍스트 콘텐츠 => 텍스트 노드
트리 자료구조
부모 노드와 자식 노드로 구성되어
노드 간의 계층적 구조를 표현하는 비선형 자료구조
- 하나의 최상위 노드에서 시작함
- 최상위 노드는 부모 노드가 없고 루트 노드라고 함
- 루트 노드: 0개 이상의 자식 노트를 가짐
- 리프 노드: 자식 노드가 없는 노드
DOM (Document Object Model) : 노드 객체들로 구성된 트리 자료구조
주요 노드 타입
문서 노드 (Document Node)
- 루트 노드로서 document 객체를 가리킴
- window.document 또는 document 로 참조 가능
- DOM 트리의 노드들에 접근하기 위한 진입점 역할
- 요소, 어트리뷰트, 텍스트 노드에 접근하기 위해서는 문서 노드를 통해야 함
요소 노드 (Element Node)
- HTML 요소를 가리키는 객체
- 문서의 구조를 표현함
어트리뷰트 노드 (Attribute Node)
- HTML 요소의 어트리뷰트를 가리키는 객체
- 어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있음
- 요소 노드에만 연결되어 있기 때문에 부모 노드가 없으므로 요소 노드의 형제 노드는 아님
- 어트리뷰트를 참조/변경하려면 먼저 요소 노드에 접근해야 함
텍스트 노드 (Text Node)
- HTML 요소의 텍스트를 가리키는 객체
- 문서의 정보를 표현함
- 요소 노드의 자식 노드이며 자식 노드가 없는 리프 노드임 (DOM 트리의 최종단)
- 텍스트 노드에 접근하려면 부모 노드인 요소 노드에 접근해야 함
노드 객체의 상속 구조
DOM을 구성하는 노드 객체는 자신의 구조/정보를 제어할 수 있는 DOM API를 사용할 수 있음
이를 통해 노드 객체는 부모/형제/자식을 탐색할 수 있고, 자신의 어트리뷰트/텍스트를 조작할 수 있음
노드 객체
- ECMAScript 사양에 정의된 표준 빌트인 객체가 아니라
- 브라우저 환경에서 추가적으로 제공하는 호스트 객체임
- 자바스크립트 객체이기 때문에 프로토타입에 의한 상속 구조를 가짐
- 모든 노드 객체는 Object, EventTarget, Node 인터페이스를 상속받음
요소 노드
- Element 인터페이스를 상속받음
- 추가적으로 HTMLElement 인터페이스를 상속받음
- 태그의 종류별로 세분화된 HTMLHeadElement, HTMLBodyElement 등의 인터페이스를 상속받음
예) input 요소 노드 객체는 HTMLInputElement, HTMLElement, Element, Node, EventTarget, Object 의
프로토타입에 바인딩 되어있는 프로토타입 객체를 상속받음
프로토타입 체인에 있는 모든 프로토타입의 프로퍼티/메서드를 상속받아 사용 가능
요소 노드 취득
1. id 를 이용
document.getElementById('cat')
- id 값이 'cat' 인 요소 노드를 탐색하여 반환함
- 중복된 id 값을 가진 요소가 있을 경우 첫 번째 요소 노드만 반환함
- 존재하지 않을 경우 null 반환
2. 태그 이름을 이용
document.getElementsByTagName('li');
- 해당 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환함
- 반환하는 DOM 컬렉션 객체인 HTMLCollection 객체는 유사 배열 객체이면서 이터러블임
- 인수로 * 를 전달하면 HTML 문서의 모든 요소 노드 취득 가능
3. class 를 이용
document.getElementsByClassName('animal cat');
- 전달한 class 값을 갖는 모든 요소 노드들을 탐색하여 반환함
- 공백으로 구분하여 여러 개의 class 지정 가능
- 반환하는 DOM 컬렉션 객체인 HTMLCollection 객체는 유사 배열 객체이면서 이터러블임
- 존재하지 않을 경우 빈 HTMLCollection 객체 반환
4. CSS 선택자를 이용
document.querySelector('.cat');
document.querySelectorAll('.cat');
- querySelectorAll 메서드는 전달한 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환함
- 반환하는 DOM 컬렉션 객체인 NodeList 객체는 유사 배열 객체이면서 이터러블임
- 존재하지 않을 경우 빈 NodeList 객체 반환
- 선택자가 문법에 맞지 않을 경우 DOMException 에러 발생
- 인수로 * 를 전달하면 HTML 문서의 모든 요소 노드 취득 가능
5. 특정 요소 노드를 취득할 수 있는지 확인
const catEl = document.querySelector('.cat');
// catEl 노드는 #animal > li.cat 선택자로 취득 가능
console.log(catEl.matches('#animal > li.cat')); // true
// carEl 노드는 #animal > li.dog 선택자로 취득 불가능
console.log(catEl.matches('#animal > li.dog')); // false
- 전달한 css 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인 가능
6. HTMLCollection과 NodeList
- 유사 배열 객체이면서 이터러블임
- 스프레드 문법이나 Array.from 메서드를 사용하여 배열로 변환 가능
- 예상과 다르게 동작할 때가 있기 때문에 배열로 변환하여 사용하는 것이 권장됨
- 배열로 변환하면 배열의 유용한 고차 함수 사용 가능 (forEach, map, filter, reduce 등)
노드 탐색
자식 노드 탐색
childNodes
- 자식 노드를 모두 탐색하여 NodeList 에 담아 반환
- 텍스트 노드 O
children
- 자식 노드 중에서 요소 노드만 모두 탐색하여 HTMLCollection 에 담아 반환
- 텍스트 노드 X
firstChild
- 첫 번째 자식 노드 반환
- 텍스트 노드 O
lastChild
- 마지막 자식 노드 반환
- 텍스트 노드 O
firstElementChild
- 첫번째 자식 요소 노드 반환
- 텍스트 노드 X
lastElementChild
- 마지막 자식 요소 노드 반환
- 텍스트 노드 X
hasChildNodes
- 자식 노드 존재 여부를 반환
- 텍스트 노드 O
- 텍스트 노드가 아닌 요소 노드가 존재하는지 확인하려면
children.length 또는 childElementCount 사용
부모 노드 탐색
parentNode
형제 노드 탐색
previousSibling
- 자신의 이전 형제 노드를 탐색하여 반환함
- 텍스트 노드 O
nextSibling
- 자신의 다음 형제 노드를 탐색하여 반환함
- 텍스트 노드 O
previousElementSibling
- 자신의 이전 형제 요소 노드를 탐색하여 반환함
- 텍스트 노드 X
nextElementSibling
- 자신의 다음 형제 요소 노드를 탐색하여 반환함
- 텍스트 노드 X
노드 정보 취득
nodeType
- 노트 타입을 나타내는 상수 반환
- Node.ELEMENT_NODE: 요소 노드 타입, 상수 1 반환
- Node.TEXT_NODE: 텍스트 노드 타입, 상수 3 반환
- Node.DOCUMENT_NODE: 문서 노트 타입, 상수 9 반환
nodeName
- 노드 이름을 문자열로 반환
- 요소 노드: 대문자 문자열로 태그 이름 반환 ("UL", "LI" 등)
- 텍스트 노드: 문자열 "#text" 반환
- 문서 노드: "#document" 반환
요소 노드의 텍스트 조작
nodeValue
- 텍스트 노드의 텍스트 반환
- 텍스트 노드가 아닌 노드의 nodeValue 프로퍼티를 참조하면 null 반환
- 참조와 할당 모두 가능
textContent
- 요소 노드의 텍스트 + 모든 자손 노드의 텍스트를 모두 취득/변경 가능
- HTML 마크업은 무시됨
- 요소 노드의 콘텐츠 영역에 텍스트만 존재할 경우 textContent 가 더 간단함
- 문자열 할당 시 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가됨 (HTML 마크업 파싱 X)
innerText
- CSS 에 순정적임 (visibility: hidden; 일 경우 해당 요소 노드의 텍스트를 반환하지 않음 등)
- CSS 를 고려해야 하기 때문에 textContent 프로퍼티보다 느림
DOM 조작
innerHTML
- 요소 노드의 HTML 마크업을 취득/변경 가능
- 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환함
- 문자열 할당 시 모든 자식 노드가 제거되고
할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 자식 노드로 반영됨 - 기존의 자식 노드를 모두 제거하고 새로 생성하기 때문에 효울적이지 않음
- 삽입 위치를 지정할 수 없기 때문에 위치를 지정해야 할 경우 사용하지 않는 것이 좋음
- 크로스 사이트 스크립팅 공격에 취약하기 때문에
HTML 새니티제이션을 사용해야 함
HTML 새니티제이션
DOMpurify.sanitize('<img scr="x" onerror="alert(document.cookie)">');
// <img src="x">
- 사용자로부터 입력받은 데이터에 의해 발생할 수 있는
크로스 사이트 스크립팅 공격을 예방하기 위해 잠재적 위험을 제거하는 기능 - DOMPurify 라이브러리 사용
insertAdjacentHTML
- 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입함
- 두 번째 인수로 전달한 HTML 마크업 문자열을 파싱하고
그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치에 삽입함 - 첫 번째 인수로 전달 가능한 문자열
- beforebegin
- afterbegin
- beforeend
- afterend
- 기존 요소에 영향을 주지 않기 때문에 innerHTML 보다 효울적이고 빠름
- HTML 마크업 문자열을 파싱하기 때문에 크로스 사이트 스크립팅 공격에 취약함
노드 생성과 추가
const liEl = document.createElement('li'); // 요소 노드 생성
const textNode = document.createTextNode('cat'); // 텍스트 노드 생성
liEl.appendChild(textNode); // 텍스트 노드를 li 요소 노드의 자식 노드로 추가
liEl.textContent = 'cat'; // 요소 노드에 자식이 없을 경우 이 방법이 더 간편함
DocumentFragment
const fragment = document.createDocumnetFragment();
- 노드 객체의 일종, 부모 노드가 없어 기존 DOM 과는 별도로 존재함
- 별도의 서브 DOM 을 구성하여 기존 DOM 에 추가하기 위한 용도로 사용
- 실제 DOM 변경은 한 번만 발생되기 때문에 리플로우/리페인트도 한 번만 실행됨
(기존 DOM 에 여러 번 추가하는 것 보다 서브 DOM 을 만들어 한 번에 추가하는 것이 좋음)
지정한 위치에 노드 삽입
insertBefore(newNode, childNode)
- 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입함
- 두 번째 인수로 전달받은 노드가 null 일 경우 appendChild 처럼 동작함 (마지막 자식 노드로 추가됨)
노드 이동
DOM 에 이미 존재하는 노드를 appendChild / insertBefore 을 사용하여 DOM 에 다시 추가할 경우
현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가함 (이동함)
노드 복사
cloneNode([deep: true | false])
- 매개변수 deep 에 true 를 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성함
- false 를 인수로 전달하거나 생략하면 노드를 얇은 복사하여 노드 자신만의 사본을 생성함 (텍스트 노드도 없음)
노드 교체
replaceChild(newChild, oldChild)
- 첫 번째 매개변수에는 교체할 새로운 노드를 전달함
- 두 번째 매개변수에는 이미 존재하는 교체될 노드를 전달함
- (자신을 호출한 노드의 자식 노드인 oldChild 노드를 newChild 노드로 교체함)
노드 삭제
removeChild(child)
- 매개변수로 전달한 노드를 DOM 에서 삭제함
- 전달할 노드는 메서드를 호출한 노드의 자식 노드여야 함
어트리뷰트
<input id="user" type="text" value="cat"/>
HTML 요소는 여러 개의 어트리뷰트를 가질 수 있음
HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공함
요소 노드의 attributes 프로퍼티로 취득 가능 (요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체 반환)
글로벌/이벤트 핸들러 어트리뷰트는 모든 HTML 요소에서 공통으로 사용 가능
특정 HTML 요소에만 한정적으로 사용 가능한 어트리뷰트도 있음
(type, value, checked 어트리뷰트는 input 요소에서만 사용 가능)
- 글로벌 어트리뷰트: id, cless, style, title, lang, tabindex, draggable, hidden 등
- 이벤트 핸들러 어트리뷰트: onclick, onchange, onfocus, onblur, oninput, onkeypress 등
HTML 어트리뷰트 조작
getAttribute(attributeName)
어트리뷰트 값 참조
setAttribute(attributeName, attributeValue)
어트리뷰트 값 변경
hasAttribute(attributeName)
특정 어트리뷰트 존재 확인
removeAttribute(attributeName)
특정 어트리뷰트 삭제
HTML 어트리뷰트 vs DOM 프로퍼티
요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재함
프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 가짐
HTML 어트리뷰트
- HTML 요소의 초기 상태를 관리함
- 사용자의 입력에 의해 상태가 변경되어도 변하지 않고 초기 상태를 유지함
DOM 프로퍼티
- 사용자가 입력한 최신 상태를 관리함
- 사용자의 입력에 의한 상태 변화에 반응하며 항상 최신 상태를 유지함
data 어트리뷰트와 dataset 프로퍼티
<div id="1" data-user-id="1234" data-role="admin">Lee</div>
...
<script>
const user = document.querySelector('#1');
console.log(user.dataset.userId); // 취득
user.dataset.role = 'user'; // 변경
user.dataset.animal = 'cat'; // 추가
console.log(user.dataset);
// DOMStringMap {userId: "1234", role: "user", animal: "cat"}
// <div id="1" data-user-id="1234" data-role="user" data-animal="cat">Lee</div>
</script>
- HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간 데이터 교환 가능
- data 어트리뷰트는 data-접두사 다음에 임의의 이름을 붙여 사용함
- data 어트리뷰트의 값은 dataset 프로퍼티로 취득 가능 (data-접두사 다음에 카멜케이스)
- data-접두사 다음에 존재하지 않는 이름을 키로 사용해 값을 할당하면 data 어트리뷰트가 추가됨 (케밥케이스로 자동 변경됨)
스타일
<body>
<div style="color: red">Hello World</div>
<script>
const divEl = document.querySelector('div');
console.log(divEl.style); // 인라인 스타일 취득
// CSSStyleDeclaration { 0: "color", ... }
divEl.style.color = 'blue'; // 인라인 스타일 변경
divEl.style.width = '100px'; // 인라인 스타일 추가
</script>
</body>
- style 프로퍼티를 참조하면 CSSStyleDeclaration 타입의 객체를 반환함
- CSSStyleDeclaration 객체는 다양한 CSS 프로퍼티에 대응하는 프로퍼티를 가지고 있음
- 이 프로퍼티에 값을 할당하면 해당 CSS 프로퍼티가 인라인 스타일로 HTML 요소에 추가되거나 변경됨
- CSS 프로퍼티는 케밥케이스를 따르고 CSSStyleDeclaration 객체의 프로퍼티는 카멜케이스를 따름
- 단위 지정이 필요한 CSS 프로퍼티의 값은 반드시 단위를 지정해야 함 (지정하지 않으면 적용 X)
클래스 조작
className 프로퍼티
- 문자열을 반환하기 때문에 여러 개의 클래스를 반환하는 경우 다루기 불편함
- class 는 예약어이기 때문에 사용이 불가능
classList 프로퍼티
- class 어트리뷰트의 정보를 나타내는 컬렉션 객체인 DOMTokenList 객체를 반환함
- DOMTokenList 객체는 유사 배열 객체이면서 이터러블이고 유용한 메서드들을 제공함
add(...className)
- 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가함
remove(...className)
- 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제함
- 일치하는 클래스가 class 어트리뷰트에 없으면 무시됨
item(index)
- 인수로 전달한 index 에 해당하는 클래스를 class 어트리뷰트에서 반환함
contains(className)
- 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어있는지 확인함
replace(oldClassName, newClassName)
- 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경함
toggle(className)
- 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가함
- 두 번째 인수로 불리언 값으로 평가되는 조건식을 전달할 수 있음
- 조건식의 결과가 true 이면 첫 번째 인수로 전달받은 문자열을 추가함
- 조건식의 결과가 false 이면 첫 번째 인수로 전달받은 문자열을 제거함
요소에 적용되어 있는 CSS 스타일 참조
window.getComputedStyle(element[, pseudo])
- style 프로퍼티는 인라인 스타일만 반환함
- 모든 CSS 스타일을 참조해야 할 경우 getComputedStyle 메서드 사용
- 첫 번째 인수로 전달한 요소 노드에 적용되어있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환함
- 두 번째 인수로 :after, :before 와 같은 의사 요소를 지정하는 문자열 전달 가능