[스터디]모던 자바스크립트 딥다이브 39장 DOM
39.6 DOM 조작
- Dom 조작은 새로운 노드를 생성해서 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 의미한다.
- DOM 조작에 의해 DOM에 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 준다. 따라서 주의해서 다루어야 한다.
39.6.1 innerHTML
Element.prototype.innerHTML 프로퍼티
는 setter와 getter 모두 존재하는 접근자 프로퍼티이다.innerHTML 프로퍼티
는 요소 노드의 HTML 마크업을 취득하거나 변경하는데, 요소 노드의 innerHTML 프로퍼티를 참조하면 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환한다. 아래의 코드로 확인 할 수 있다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소의 콘텐츠 영역 내의 HTML 마크업을 문자열로 취득한다.
console.log(document.getElementById('foo').innerHTML);
// "Hello <span>world!</span>"
</script>
</html>
innerHTML 프로퍼티
를 사용하면 아래처럼 HTML 마크업 문자열로 간단히 DOM 조작이 가능하다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 노드 추가
$fruits.innerHTML += '<li class="banana">Banana</li>';
// 노드 교체
$fruits.innerHTML = '<li class="orange">Orange</li>';
// 노드 삭제
$fruits.innerHTML = '';
</script>
</html>
-
하지만 크로스 사이트 스크립팅 공격에 취약하기때문에 사용에 주의하여야 한다!
-
크로스 사이트 스크립팅 공격에 예방하기위해 HTML 새니티제이션(HTML sanitization)인
DOMPurify 라이브러리
의 사용을 권장한다.
XSS(Cross-Site-Scripting)이란? 사이트 간 스크립팅(또는 크로스 사이트 스크립팅, 영문 명칭 cross-site scripting, 영문 약어 XSS)은 웹 애플리케이션에서 많이 나타나는 취약점의 하나로 웹사이트 관리자가 아닌 이가 웹 페이지에 악성 스크립트를 삽입할 수 있는 취약점이다.
39.6.2 insertAdjacentHTML 메서드
Element.prototype.insertAdjacentHTML(position, DOMString) 메서드
는 기존 요소를 제거하지 않고 위치를 지정해 새로운 요소를 삽입한다.insertAdjacentHTML 메서드
의 첫 번째 인수로는 전달할 위치를 받는다. 이때 첫 번째 인수로 ‘beforebegin’, ‘afterbegin’, ‘beforeend’, ‘afterend’ 이렇게 4가지를 받을 수 있다. 두 번째 인수로는 HTML 마크업 문자열을 파싱한다.
beforebegin : 형제요소로 앞에 afterbegin : 자식요소로 맨앞에 beforeend : 자식요소로 맨뒤에 afterend : 형제요소로 뒤에
<!DOCTYPE html>
<html>
<body>
<!-- beforebegin -->
<div id="foo">
<!-- afterbegin -->
text
<!-- beforeend -->
</div>
<!-- afterend -->
</body>
<script>
const $foo = document.getElementById('foo');
$foo.insertAdjacentHTML('beforebegin', '<p>beforebegin</p>');
$foo.insertAdjacentHTML('afterbegin', '<p>afterbegin</p>');
$foo.insertAdjacentHTML('beforeend', '<p>beforeend</p>');
$foo.insertAdjacentHTML('afterend', '<p>afterend</p>');
</script>
</html>
39.6.3 노드 생성과 추가
-
DOM은 노드를 직접 생성/삽입/삭제/치환하는 메서드도 제공한다.
-
아래의 예제 코드로 과정을 확인해보자.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 1. 요소 노드 생성
const $li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTextNode('Banana');
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
</script>
</html>
1. 요소 노드 생성
createElement 메서드
로 요소 노드를 생성하고 반환한다.
createElement 메서드
로 생성한 요소 노드는 위 그림과 같이 기존 DOM에 추가되지 않고 따로 존재하는 상태이다.- 따라서 생성된 요소 노드를 DOM에 추가하는 과정이 필요하다.
2. 텍스트 노드 생성
createTextNode 메서드
를 사용하여 텍스트 노드를 생성하고 반환한다.
- 텍스트 노드는 요소 노드의 자식 노드이다. 하지만 아직 자식 노드로 추가되지 않을 것을 확인할 수 있다.
- 따라서 이후에 텍스트 노드를 요소 노드의 자식으로 추가하는 과정이 필요하다.
3. 텍스트 노드를 요소 노드의 자식 노드로 추가
appendChild 메서드
는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.
appendChild 메서드
를 통해 요소 노드와 텍스트 노드를 부자 관계로 연결해주었다.
4. 요소 노드를 DOM에 추가
appendChild 메서드
로 요소 노드를 기존 DOM에 연결시켜준다.
39.6.4 복수의 노드 생성과 추가
- DOM을 여러번 변경하게 되면 그 때마다 리플로우와 리페인트가 실행된다. 이는 비효율적이기때문에 컨테이너 요소를 사용해서 문제를 해결할 수 있다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 컨테이너 요소 노드 생성
const $container = document.createElement('div');
['Apple', 'Banana', 'Orange'].forEach(text => {
// 1. 요소 노드 생성
const $li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTextNode(text);
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
// 4. $li 요소 노드를 컨테이너 요소의 마지막 자식 노드로 추가
$container.appendChild($li);
});
// 5. 컨테이너 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($container);
</script>
</html>
createElement
로 전체의 큰 틀을 만들어서 그 안에 다 넣어서(1,2,3,4 과정) 한번만 추가(5번 과정)하면 리플로우를 한번만 실행하게되서 더욱 효율적이다!- 하지만 불필요한 컨테이너 요소도 추가되는 부작용이 있다.
-
이 문제점은 DocumentFragment 노드를 통해 해결할 수 있다.
DocumentFragment 노드
는 노드 객체의 일종으로, 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다.DocumentFragment 노드
를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 DOM에 추가된다.
39.6.5 노드 삽입
마지막 노드로 추가
Node.prototype.appendChild 메서드
는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다. 단, 노드를 추가할 위치를 지정할 수 없고 언제나 마지막 자식 노드로 추가한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</body>
<script>
// 요소 노드 생성
const $li = document.createElement('li');
// 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
$li.appendChild(document.createTextNode('Orange'));
// $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
document.getElementById('fruits').appendChild($li);
</script>
</html>
지정한 위치에 노드 삽입
Node.prototype.insertBefore(newNode, childNode) 메서드
는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다.- 두 번쨰 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드여야한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 요소 노드 생성
const $li = document.createElement('li');
// 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
$li.appendChild(document.createTextNode('Orange'));
// $li 요소 노드를 #fruits 요소 노드의 마지막 자식 요소 앞에 삽입
$fruits.insertBefore($li, $fruits.lastElementChild);
// Apple - Orange - Banana
</script>
</html>
39.6.6 노드 이동
- DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가한다. 즉, 노드가 이동한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 이미 존재하는 요소 노드를 취득
const [$apple, $banana, ] = $fruits.children;
// 이미 존재하는 $apple 요소 노드를 #fruits 요소 노드의 마지막 노드로 이동
$fruits.appendChild($apple); // Banana - Orange - Apple
// 이미 존재하는 $banana 요소 노드를 #fruits 요소의 마지막 자식 노드 앞으로 이동
$fruits.insertBefore($banana, $fruits.lastElementChild);
// Orange - Banana - Apple
</script>
</html>
39.6.7 노드 복사
Node.prototype.cloneNode([deep: true | false]) 메서드
는 노드의 사본을 생성하여 반환한다.- deep에 true를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고, false를 전달하거나 생략하면 얕은 복사를 해서 노드 자신만의 사본을 생성한다.
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits'); const $apple =
$fruits.firstElementChild; // $apple 요소를 얕은 복사하여 사본을 생성.
텍스트 노드가 없는 사본이 생성된다. const $shallowClone =
$apple.cloneNode(); // 사본 요소 노드에 텍스트 추가
$shallowClone.textContent = 'Banana'; // 사본 요소 노드를 #fruits 요소
노드의 마지막 노드로 추가 $fruits.appendChild($shallowClone); // #fruits
요소를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성 const $deepClone =
$fruits.cloneNode(true); // 사본 요소 노드를 #fruits 요소 노드의 마지막
노드로 추가 $fruits.appendChild($deepClone);
</script>
</html>
39.6.8 노드 교체
Node.prototype.replaceChild(newChild, oldChild) 메서드
는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.- 첫 번째 매개변수 newChild : 교체할 새로운 노드
- 두 번째 매개변수 oldChild : 이미 존재하는 교체될 노드
- replaceChild 메서드는 자신을 호출한 노드의 자식인 oldChild 노드를 newChild 노드로 교체한다. oldChild 노드는 DOM에서 제거된다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 기존 노드와 교체할 요소 노드를 생성
const $newChild = document.createElement('li');
$newChild.textContent = 'Banana';
// #fruits 요소 노드의 첫 번째 자식 요소 노드를 $newChild 요소 노드로 교체
$fruits.replaceChild($newChild, $fruits.firstElementChild);
</script>
</html>
39.6.9 노드 삭제
Node.prototype.removeChild(child) 메서드
는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제한다.
39.7 어트리뷰트
39.7.1 어트리뷰트 노드와 attributes 프로퍼티
- HTML 문서의 구성 요소인 HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다.
- HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공한다.
- 대부분의 어트리뷰트는 모든 HTML 요소에서 사용될 수 있지만, type, value, checked 어트리뷰트는 input 요소에만 사용할 수 있는 경우도 있다.
- HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다.
39.7.2 HTML 어트리뷰트 조작
- HTML 어트리뷰트 값을 참조하려면
Element.prototype.getAttribute(attributeName) 메서드
를 사용하고, HTML 어트리뷰트 값을 변경하려면Element.prototype.setAttribute(attributeName, attributeValue) 메서드
를 사용한다.
39.7.3 HTML 어트리뷰트 vs DOM 프로퍼티
- HTML 어트리뷰트와 DOM 프로퍼티는 대부분 1:1 대응한다.
HTML 어트리뷰트
- HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. -> 즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다.
DOM 프로퍼티
- 사용자가 입력한 최신 상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리한다.
- DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지한다.
- DOM 프로퍼티에 값을 할당하는 것은 사용자가 상태를 변경하는 행위와 같다.
39.7.4 data 어트리뷰트와 dataset 프로퍼티
-
data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다.
-
data 어트리뷰트는 data-user-id. data-role과 같이 data- 접두사 다음에 이름을 붙여서 사용한다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621" data-role="admin">Lee</li>
<li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
</ul>
</body>
</html>
- dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다.
- DOMStringMap 객체는 data 어트리뷰트의 data- 접두사 뒤에 이름을 카멜 케이스로 변환한 프로퍼티를 가지고 있다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621" data-role="admin">Lee</li>
<li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children];
// user-id가 '7621'인 요소 노드를 취득한다.
const user = users.find(user => user.dataset.userId === '7621');
// user-id가 '7621'인 요소 노드에서 data-role의 값을 취득한다.
console.log(user.dataset.role); // "admin"
// user-id가 '7621'인 요소 노드의 data-role 값을 변경한다.
user.dataset.role = 'subscriber';
// dataset 프로퍼티는 DOMStringMap 객체를 반환한다.
console.log(user.dataset); // DOMStringMap {userId: "7621", role: "subscriber"}
</script>
</body>
</html>
- 만약 존재하지 않는 이름을 키로 사용하여 dataset 프로퍼티에 값을 할당하면 HTML 요소에 data 어트리뷰트가 추가된다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621">Lee</li>
<li id="2" data-user-id="9524">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children];
// user-id가 '7621'인 요소 노드를 취득한다.
const user = users.find(user => user.dataset.userId === '7621');
// user-id가 '7621'인 요소 노드에 새로운 data 어트리뷰트를 추가한다.
user.dataset.role = 'admin';
console.log(user.dataset);
/*
DOMStringMap {userId: "7621", role: "admin"}
-> <li id="1" data-user-id="7621" data-role="admin">Lee</li>
*/
</script>
</body>
</html>
39.8 스타일
39.8.1 인라인 스타일 조작
HTMLElement.prototype.style 프로퍼티
는 setter와 getter 모두 존재하는 접근자 프로퍼티이며 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경한다.
<!DOCTYPE html>
<html>
<body>
<div style="color: red">Hello World</div>
<script>
const $div = document.querySelector('div');
// 인라인 스타일 취득
console.log($div.style); // CSSStyleDeclaration { 0: "color", ... }
// 인라인 스타일 변경
$div.style.color = 'blue';
// 인라인 스타일 추가
$div.style.width = '100px';
$div.style.height = '100px';
$div.style.backgroundColor = 'yellow';
</script>
</body>
</html>
39.8.2 클래스 조작
className
Element.prototype.className 프로퍼티
는 setter와 getter 모두 존재하는 접근자 프로퍼티이며 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경한다.- className 프로퍼티는 문자열을 반환하기때문에 공백으로 구분된 여러 개의 클래스를 반환하는 경우 다루기가 불편하다.
classList
Element.prototype.classList 프로퍼티
는 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px; height: 100px;
background-color: antiquewhite;
}
.red { color: red; }
.blue { color: blue; }
</style>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 취득
// classList가 반환하는 DOMTokenList 객체는 HTMLCollection과 NodeList와 같이
// 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는(live) 객체다.
console.log($box.classList);
// DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]
// .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
$box.classList.replace('red', 'blue');
</script>
</body>
</html>
- DOMTokenList 객체가 제공하는 메서드
39.8.3 요소에 적용되어 있는 CSS 스타일 참조
- style 프로퍼티는 인라인 스타일만 반환하기 때문에 클래스를 적용한 스타일이나 상속을 통해 적용된 스타일은 style 프로퍼티로 참조할 수 없다.
39.9 DOM 표준
블로그에 작성한 파트 외 정리본은 아래의 깃헙에서 확인 가능합니다!
https://github.com/hyeun427/javascript-study
댓글남기기