[JS] DOM 조작법 ☑️
Achievement Goals
DOM을 JavaScript로 조작하여 HTML Element를 추가하거나 삭제, 혹은 내용을 변경할 수 있다.
- createElement - CREATE
- querySelector, querySelectorAll - READ
- textContent, id, classList, setAttribute - UPDATE
- remove, removeChild,
innerHTML = ""
,textContent = ""
- DELETE- appendChild - APPEND
- innerHTML과 textContent의 차이
Advanced Challenge
DOM의 더 깊은 내용에 대해서 이해할 수 있다.
- createDocumentFragment를 활용하여, 더 효율적으로 DOM을 제어할 수 있다.
- HTML5 template tag 사용법을 이해할 수 있다.
- element와 node의 차이를 이해할 수 있다.
- children과 childNodes의 차이를 이해할 수 있다.
- remove와 removeChild의 차이를 이해할 수 있다.
- 같은 엘리먼트를 appendChild 하면, 기존 엘리먼트를 복사할까?
- 좌표 정보 조회를 할 수 있다. - offsetTop…
- 크기 정보 조회를 할 수 있다. - offsetWidth…
노드 취득
HTML 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다.
-
id를 이용한 요소 노드 취득 :
Document.prototype.getElementById()
-
태그 이름을 이용한 요소 노드 취득 :
Document.prototype/Element.prototype.getElementByTagName()
-
클래스 이름을 이용한 요소 노드 취득 :
Document.prototype/Element.prototype.getElementByClassName()
-
CSS 선택자를 이용한 요소 노드 취득
/* 전체 선택자: 모든 요소를 선택 */
\* { ... }
/* 태그 선택자: 모든 p 태그 요소를 모두 선택 */
p { ... }
/* id 선택자: id 값이 'foo'인 요소를 모두 선택 */
\#foo { ... }
/* class 선택자: class 값이 'foo'인 요소를 모두 선택 */
.foo { ... }
/* 어트리뷰트 선택자: input 요소 중에 type 어트리뷰트 값이 'text'인 요소를 모두 선택 */
input[type=text] { ... }
/* 후손 선택자: div 요소의 후손 요소 중 p 요소를 모두 선택 */
div p { ... }
/* 자식 선택자: div 요소의 자식 요소 중 p 요소를 모두 선택 */
div > p { ... }
/* 인접 형제 선택자: p 요소의 형제 요소 중에 p 요소 바로 뒤에 위치하는 ul 요소를 선택 */
p + ul { ... }
/* 일반 형제 선택자: p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택 */
p ~ ul { ... }
/* 가상 클래스 선택자: hover 상태인 a 요소를 모두 선택 */
a:hover { ... }
/* 가상 요소 선택자: p 요소의 콘텐츠의 앞에 위치하는 공간을 선택
일반적으로 content 프로퍼티와 함께 사용된다. */
p::before { ... }
Document.prototype/Element.prototype.querySelector()
인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환
Document.prototype/Element.prototype.querySelectorAll()
인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환.
노드 탐색
-
자식 노드 탐색
Node.prototype.childNodes // 자식 노드를 모두 탐색해 DOM 컬렉션 객체인 NodeList에 담아 반환 Element.prototype.childNodes // 자식 노드중 요소 노드만 탐색해 DOM 컬렉션 객체인 HEMLCollection에 담아 반환 Node.prototype.firstChild // 첫 번째 자식 노드를 반환. 반환한 노드는 텍스트 노드나 요소 노드 Node.prototype.lastChild // 마지막 자식 노드를 반환. 반환한 노드는 텍스트 노드나 요소 노드 Element.prototype.firstElementChild // 첫 번째 자식 노드를 반환. 요소 노드만 반환 Element.prototype.lastElementChild // 마지막 자식 노드를 반환. 요소 노드만 반환
-
부모 노드 탐색
Node.prototype.parenNode
// 텍스트 노드는 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없다. -
형제 노드 탐색
Node.prototype.previousSibling // 부모 노드가 같은 형제 요소 노드 중 자신의 이전 형제 요소 노드를 탐색하여 반환 Node.prototype.nextSibling // 부모 노드가 같은 형제 요소 노드 중 자신의 다음 형제 요소 노드를 탐색하여 반환 Element.prototype.previousElementSibling // 부모 노드가 같은 형제 요소 노드 중 자신의 이전 형제 요소 노드를 탐색하여 반환 Element.prototype.nextElementSibling // 부모 노드가 같은 형제 요소 노드 중 자신의 다음 형제 요소 노드를 탐색하여 반환
DOM 조작
-
nodeValue
: 노드의 값을 반환. 텍스트 노드의 값만을 반환, 텍스트 노드가 아닌 노드 객체의 nudeValue 프로퍼티를 참조하면 null 반환// 해당 텍스트 노드의 부모 요소 노드를 선택한다. const one = document.getElementById('one'); console.dir(one); // HTMLLIElement: li#one.red // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다. console.log(one.nodeName); // LI console.log(one.nodeType); // 1: Element node // firstChild 프로퍼티를 사용하여 텍스트 노드를 탐색한다. const textNode = one.firstChild; // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다. console.log(textNode.nodeName); // #text console.log(textNode.nodeType); // 3: Text node // nodeValue 프로퍼티를 사용하여 노드의 값을 취득한다. console.log(textNode.nodeValue); // Seoul // nodeValue 프로퍼티를 이용하여 텍스트를 수정한다. textNode.nodeValue = 'Pusan';
4.3 HTML 콘텐츠 조작(Manipulation)
-
요소의 텍스트 콘텐츠를 취득 또는 변경한다. 이때 마크업은 무시된다. textContent를 통해 요소에 새로운 텍스트를 할당하면 텍스트를 변경할 수 있다. 이때 순수한 텍스트만 지정해야 하며 마크업을 포함시키면 문자열로 인식되어 그대로 출력된다.
<!DOCTYPE html>
<html>
<head>
<style>
.red { color: #ff0000; }
.blue { color: #0000ff; }
</style>
</head>
<body>
<div>
<h1>Cities</h1>
<ul>
<li id="one" class="red">Seoul</li>
<li id="two" class="red">London</li>
<li id="three" class="red">Newyork</li>
<li id="four">Tokyo</li>
</ul>
</div>
<script>
const ul = document.querySelector('ul');
// 요소의 텍스트 취득
console.log(ul.textContent);
/*
Seoul
London
Newyork
Tokyo
*/
const one = document.getElementById('one');
// 요소의 텍스트 취득
console.log(one.textContent); // Seoul
// 요소의 텍스트 변경
one.textContent += ', Korea';
console.log(one.textContent); // Seoul, Korea
// 요소의 마크업이 포함된 콘텐츠 변경.
one.textContent = '<h1>Heading</h1>';
// 마크업이 문자열로 표시된다.
console.log(one.textContent); // <h1>Heading</h1>
</script>
</body>
</html>
-
innerText 프로퍼티를 사용하여도 요소의 텍스트 콘텐츠에만 접근할 수 있다. 하지만 아래의 이유로 사용하지 않는 것이 좋다.비표준이다.CSS에 순종적이다. 예를 들어 CSS에 의해 비표시(visibility: hidden;)로 지정되어 있다면 텍스트가 반환되지 않는다.CSS를 고려해야 하므로 textContent 프로퍼티보다 느리다
-
해당 요소의 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.
const ul = document.querySelector('ul');
// innerHTML 프로퍼티는 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.
console.log(ul.innerHTML);
// IE를 제외한 대부분의 브라우저들은 요소 사이의 공백 또는 줄바꿈 문자를 텍스트 노드로 취급한다
/*
<li id="one" class="red">Seoul</li>
<li id="two" class="red">London</li>
<li id="three" class="red">Newyork</li>
<li id="four">Tokyo</li>
*/
innerHTML 프로퍼티를 사용하여 마크업이 포함된 새로운 콘텐츠를 지정하면 새로운 요소를 DOM에 추가할 수 있다.
const one = document.getElementById('one');
// 마크업이 포함된 콘텐츠 취득
console.log(one.innerHTML); // Seoul
// 마크업이 포함된 콘텐츠 변경
one.innerHTML += '<em class="blue">, Korea</em>';
// 마크업이 포함된 콘텐츠 취득
console.log(one.innerHTML); // Seoul <em class="blue">, Korea</em>
위와 같이 마크업이 포함된 콘텐츠를 추가하는 것은 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하다.
// 크로스 스크립팅 공격 사례
// 스크립트 태그를 추가하여 자바스크립트가 실행되도록 한다.
// HTML5에서 innerHTML로 삽입된 <script> 코드는 실행되지 않는다.
// 크롬, 파이어폭스 등의 브라우저나 최신 브라우저 환경에서는 작동하지 않을 수도 있다.
elem.innerHTML = '<script>alert("XSS!")</script>';
// 에러 이벤트를 발생시켜 스크립트가 실행되도록 한다.
// 크롬에서도 실행된다!
elem.innerHTML = '<img src="#" onerror="alert(\'XSS\')">';
DOM 조작 방식
innerHTML 프로퍼티를 사용하지 않고 새로운 콘텐츠를 추가할 수 있는 방법은 DOM을 직접 조작하는 것이다. 한 개의 요소를 추가하는 경우 사용한다. 이 방법은 다음의 수순에 따라 진행한다.
- 요소 노드 생성 createElement() 메소드를 사용하여 새로운 요소 노드를 생성한다. createElement() 메소드의 인자로 태그 이름을 전달한다.
- 텍스트 노드 생성 createTextNode() 메소드를 사용하여 새로운 텍스트 노드를 생성한다. 경우에 따라 생략될 수 있지만 생략하는 경우, 콘텐츠가 비어 있는 요소가 된다.
- 생성된 요소를 DOM에 추가 appendChild() 메소드를 사용하여 생성된 노드를 DOM tree에 추가한다. 또는 removeChild() 메소드를 사용하여 DOM tree에서 노드를 삭제할 수도 있다.
-
태그이름을 인자로 전달하여 요소를 생성한다.Return: HTMLElement를 상속받은 객체모든 브라우저에서 동작한다.
-
텍스트를 인자로 전달하여 텍스트 노드를 생성한다.Return: Text 객체모든 브라우저에서 동작한다.
-
인자로 전달한 노드를 마지막 자식 요소로 DOM 트리에 추가한다.Return: 추가한 노드모든 브라우저에서 동작한다.
-
인자로 전달한 노드를 DOM 트리에 제거한다.Return: 추가한 노드모든 브라우저에서 동작한다.
innerHTML vs. DOM 조작 방식 vs. insertAdjacentHTML()
innerHTML
장점 | 단점 |
---|---|
DOM 조작 방식에 비해 빠르고 간편하다. | XSS공격에 취약점이 있기 때문에 사용자로 부터 입력받은 콘텐츠(untrusted data: 댓글, 사용자 이름 등)를 추가할 때 주의하여야 한다. |
간편하게 문자열로 정의한 여러 요소를 DOM에 추가할 수 있다. | 해당 요소의 내용을 덮어 쓴다. 즉, HTML을 다시 파싱한다. 이것은 비효율적이다. |
콘텐츠를 취득할 수 있다. |
DOM 조작 방식
장점 | 단점 |
---|---|
특정 노드 한 개(노드, 텍스트, 데이터 등)를 DOM에 추가할 때 적합하다. | innerHTML보다 느리고 더 많은 코드가 필요하다. |
insertAdjacentHTML()
장점 | 단점 |
---|---|
간편하게 문자열로 정의된 여러 요소를 DOM에 추가할 수 있다. | XSS공격에 취약점이 있기 때문에 사용자로 부터 입력받은 콘텐츠(untrusted data: 댓글, 사용자 이름 등)를 추가할 때 주의하여야 한다. |
삽입되는 위치를 선정할 수 있다. |
결론
innerHTML과 insertAdjacentHTML()은 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하다. 따라서 untrusted data의 경우, 주의하여야 한다. 텍스트를 추가 또는 변경시에는 textContent, 새로운 요소의 추가 또는 삭제시에는 DOM 조작 방식을 사용하도록 한다.