● 요소 노드의 텍스트 조작
⚬ nodeValue
Node.prototype.nodeValue 프로퍼티는 setter,getter 둘 다 존재하는 접근자 프로퍼티이다. 즉, 참조와 할당 모두 가능하다는 의미이다. 노드 객체의 값이란 텍스트 노드의 텍스트이다. 따라서 텍스트 노드가 아닌 노드, 즉 문서 노드나 요소 노드의 nodeValue 프로퍼티를 참조하면 null을 반환한다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const apple = document.querySelector('.apple');
console.log(apple.nodeValue) // null
console.log(apple.firstChild.nodeValue) // apple
</script>
즉, 텍스트 노드의 nodeValue를 참조할 때만 텍스트 노드의 값 즉, 텍스트를 반환한다. 텍스트 노드가 아닌 노드 객체의 nodeValue를 참조하면 null을 반환한다.
=> 쓰기도 번거롭고, 복잡하다..
⚬ textContent
Node.prototype.textContent 프로퍼티도 setter,getter 있는 접근자 프로퍼티이다. 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 취득,변경할 수 있다. 즉 요소 노드의 콘텐츠 영역 내에 있는 모든 텍스트를 반환한다. 요소 노드의 textContent 프로퍼티에 새로운 문자열을 할당 시 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이때 HTML 마크업은 무시된다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const outer = document.getElementById('outer');
console.log(outer.textContent)
// 개행까지 모두 출력된다.
//
// banana
// apple
// grape
//
outer.textContent = '';
console.log(outer.textContent)
// 공백출력
</script>
textContent는 할당한 문자열에 HTML 마크업이 포함되어있다하더라도 문자열 그대로 인식되어 텍스트로 취급한다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const outer = document.getElementById('outer');
outer.textContent = '<span>나는 span태그를 추가했어요.</span>';
</script>
이를 해결할 방법으로 innerText 프로퍼티를 사용할 수 있다. 하지만 이는 사용하지 않는 것이 좋다.
- innerText 프로퍼티는 CSS에 순종적이다. CSS에 의해 비표시(visibility: hidden;)로 지정된 요소 노드의 텍스트를 반환하지 않는다.
- 또한 innerText 프로퍼티는 CSS를 고려해야하므로 textContent 보다 느리다.
<body>
<div id="outer">
<div class="hidden" style="visibility: hidden">가려진 존재랍니다..</div>
</div>
</body>
<script>
const outer = document.getElementById('outer');
console.log(outer.innerText)
// 공란이 출력된다.
</script>
HTML 마크업까지 파싱되어 보여지고 싶다면 innerHTML 프로퍼티를 사용하면 된다
● DOM 조작
⚬ innerHTML
- Element.prototype.innerHTML
- getter,setter 모두 있는 접근자 프로퍼티이다.
- 해당 요소의 콘텐츠 영역에 있는 모든 HTML 마크업을 문자열로 반환한다.
- 문자열을 할당 시 요소의 모든 자식 노드가 제거되고 할당한 문자열의 HTML 마크업이 자식 노드가 된다
- 크로스 사이트 스크립팅 공격에 취약하다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const outer = document.getElementById("outer");
console.log(outer.innerHTML);
// 아래의 출력값
// <div class="banana">banana</div>
// <div class="apple">apple</div>
// <div class="grape">grape</div>
</script>
예시에는 그냥 HTML 마크업로 보이지만 반환할 때는 모두 문자열로 반환한다. 또한 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고, 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식노드로 DOM에 반영된다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const outer = document.getElementById("outer");
outer.innerHTML = `<span>span 태그로 모두 변경!</span>`;
</script>
이러한 DOM조작은 구현이 간단하고 직관적이라는 점이 있지만, 크로스 사이트 스크립팅 공격에 취약하다는 단점이 있다. 아래와 같이 에러 이벤트를 발생시켜 자바스크립트 코드가 실행되게 할 수 있다.
<!DOCTYPE html>
<html>
<body>
<div id="box">
</div>
<script>
const box = document.getElementById('box');
box.innerHTML = '<img src="x" onerror="alert(document.cookie)">';
</script>
</body>
</html>
innerHTML의 문제점들
- 모든 노드의 자식을 제거하고 새롭게 할당하므로 비효율적이다.
- 또한 요소 노드 안에, 한개의 자식노드만 추가하고 싶을때 기존의 요소들이 모두 삭제되기때문에 위치를 지정해서 노드를 추가하는 것이 어렵다.
<body>
<div id="outer">
<div class="banana">banana</div>
<div class="apple">apple</div>
<div class="grape">grape</div>
</div>
</body>
<script>
const outer = document.getElementById("outer");
outer.innerHTML += `<div class="orange">orange</div>`;
</script>
위의 예제만 보면 orange div만 추가될 것 같지만, 그게 아니라 모든 div들을 다시 만들고, 그 다음에 orange div를 추가한다.
그리고 banana div 다음에 orange div를 추가하고싶은데, 그 위치에만 찾아서 넣을 수 없다. 따라서 그냥 다시 다 적어주어야함.
⚬ insertAdjancentHTML
- Element.prototype.insertAdjacentHTML(position,DOMString)
- 기존 요소를 제거하지 않고 위치를 지정해 새로운 요소를 삽입한다.
- position은 총 4 가지 : 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
- innerHTML 프로퍼티보다 효율적이고 빠르다.
- 크로스 사이트 스크립팅 공격에 취약한건 동일하다.
<body>
<!-- beforebegin -->
<div id="outer">
<!-- afterbegin -->
<!-- beforeend -->
</div>
<!-- afterend -->
</body>
<script>
const outer = document.getElementById("outer");
outer.insertAdjacentElement('beforebegin', '<p>beforebegin</p>')
outer.insertAdjacentElement('afterbegin', '<p>afterbegin</p>')
outer.insertAdjacentElement('beforeend', '<p>beforeend</p>')
outer.insertAdjacentElement('afterend', '<p>afterend</p>')
</script>
innerHTML과 다르게 노드들을 모두 새로생성하고 추가하지 않고, 추가될 부분에만 자식요소를 추가하기 때문에 효율적이고 빠르다.
⚬ 노드생성과 추가
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
<script>
const $fruits = document.getElementById("fruits");
const $li = document.createElement("li");
const $text = document.createTextNode("banana");
$li.appendChild($text);
$fruits.appendChild($li);
</script>
위의 예제는 단 하나의 요소 노드를 DOM에 한번 추가하므로 DOM은 한 번 변경된다. 이때 리플로우와 리페인트가 실행된다.
⚬ 복수의 노드 생성과 추가
<body>
<ul>
<li>Peach</li>
</ul>
<script>
const table = document.querySelector("ul");
["Apple", "Orange", "Grape"].forEach((text) => {
const newNode = document.createElement("li");
newNode.textContent = text;
table.appendChild(newNode); // 리플로우,리페인트 3번이나 발생
});
</script>
</body>
위의 예제는 리플로우와 리페인트를 3번이나 하기 때문에 효율적이지 못하다.
<body>
<ul>
<li>Peach</li>
</ul>
<script>
const table = document.querySelector("ul");
const divBox = document.createElement("div");
["Apple", "Orange", "Grape"].forEach((text) => {
const newNode = document.createElement("li");
newNode.textContent = text;
divBox.appendChild(newNode); // 리플로우,리페인트 3번이나 발생
});
table.appendChild(divBox);
</script>
위의 같은 예제로 변경하면 단 한번으로 리플로우와 리페인트를 할 수 있다. 하지만 불필요한 컨테이너 요소가 DOM에 추가되는 부작용이 있다. 이는 바람직하지 못하다.
이러한 문제는 DocumentFragement노드를 통해 해결할 수 있다. DocumentFragement 노드는 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다.
DocumentFragement 노드에 자식요소를 추가하여도 기존 DOM은 어떠한 변경도 생기지 않는다. DocumentFragement 노드를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 DOM에 추가된다.
쉽게 말하면 자식노드들의 가상 컨테이너 박스라고 생각하면 좋을 것 같다.
<body>
<ul>
<li>Peach</li>
</ul>
<script>
const table = document.querySelector("ul");
const fragment = document.createDocumentFragment();
["Apple", "Orange", "Grape"].forEach((text) => {
const newNode = document.createElement("li");
newNode.textContent = text;
fragment.appendChild(newNode); // 리플로우,리페인트 3번이나 발생
});
table.appendChild(fragment);
</script>
위의 예제에서 실제로 DOM변경이 발생하는 것은 한번 뿐이며 리플로우와 리페인트도 한번만 실행한다. 따라서 여러 개의 요소 노드를 DOM에 추가하는 경우, DocumentFragement 노드를 사용하는 것이 좋다.
⚬ 노드의 삽입
<body>
<ul>
<li>Peach</li>
<li>Grape</li>
</ul>
<script>
const $table = document.querySelector("ul");
const $li = document.createElement("li");
$li.textContent = "Banana";
$table.appendChild($li)
</script>
</body>
appendChild를 활용해, 선택한 요소 노드의 맨 마지막 자식 노드로 추가된다. 위치를 잡고 넣고 싶으면 insertBefore 메서드를 활용한다.
<body>
<ul>
<li>Peach</li>
<li>Grape</li>
</ul>
<script>
const $table = document.querySelector("ul");
const $li = document.createElement("li");
$li.textContent = "Banana";
$table.insertBefore($li, $table.lastElementChild);
</script>
</body>
insertBefore을 활용할 때는 첫 번째 인수에는 넣어줄 요소 노드, 두번째 인수에는 꼭 선택한 요소의 자식노드여야한다.
⚬ 노드의 복사
노드는 깊은 복사, 얕은 복사로 할 수 있으며, 깊은 복사는 노드 안의 텍스트 노드까지 복사되지만 얕은 복사는 텍스트 노드는 복사되지 않는다.
<body>
<ul>
<li>Peach</li>
<li>Grape</li>
<!-- <li>Banana</li> -->
<!-- <li>Peach</li> -->
</ul>
<script>
const $table = document.querySelector("ul");
const $peach = $table.firstElementChild;
const $li = $peach.cloneNode();
$li.textContent = "Banana";
$table.appendChild($li);
const $deepli = $peach.cloneNode(true);
$table.append($deepli);
</script>
</body>
⚬ 노드의 교체
replaceChild 메서드를 활용해서 노드를 교체할 수 있다. 이때 인수로 전달된 노드는 replaceChild를 호출한 노드의 자식노드여야한다.
<body>
<ul>
<li>Peach</li>
<li>Grape</li>
</ul>
<script>
const $table = document.querySelector("ul");
const $li = document.createElement("li");
$li.textContent = "Banana";
$table.replaceChild($li, $table.firstElementChild);
</script>
</body>
⚬ 노드의 삭제
removeChild 메서드를 활용해서 노드를 삭제할 수 있다. 인수로 전달한 노드는 removeChild를 호출한 노드의 자식 노드여야한다.
<body>
<ul>
<li>Peach</li>
<li>Grape</li>
</ul>
<script>
const $table = document.querySelector("ul");
const $peach = $table.firstElementChild;
$table.removeChild($peach);
</script>
</body>