Home Javascript DOM_03
Post
Cancel

Javascript DOM_03

DOM 조작

DOM 조작은 새로운 노드를 추가하거나 기존 노드를 삭제 또는 교체하는 것을 말한다. DOM이 변하면 리플로우나 리페인트가 발생하므로 성능에 영향을 준다. 따라서 DOM을 조작할 때는 성능 최적화를 고려해야 한다.

innerHTML

Element.prototype.innerHTML 프로퍼티는 setter와 getter 모두 존재하는 프로퍼티로서 요소 노드의 HTML 마크없을 취득하거나 변경한다. 이때 요소 노드 내 포함된 HTML 마크업은 문자열로 반환한다.

1
2
3
4
5
6
7
8
9
10
11
    <!DOCTYPE html>
    <html>
        <body>
            <div id="foo">Hello <span>world!</span></div>
        </body>
        <script>
            console.log(document.getElementById('foo').innerHTML);  // "Hello <span>world!</span>
        </script>
    </html>

textContent 프로퍼티는 HTML 마크업을 무시하고 텍스트만 반환하지만 innerHTML 프로퍼티는 HTML 마크업을 포함한 문자열을 반환한다. innerHTML의 경우 문자열을 할당할 경우 요소 노드의 모든 자식 노드는 제거되고 할당한 문자열에 포함된 HTML 마크업을 파싱하여 요소 노드의 자식 노드로 DOM에 반영한다.

innerHTML은 사용자의 입력 데이터에서 HTML 마크업을 파싱하므로 크로스 사이트 스크링팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하다. HTML 마크업 내 악성 코드가 포함되어 있다면 실행될 가능성이 있기 때문이다.

기본적으로 HTML5는 innerHTML 프로퍼티에 삽입된 script 코드를 실행하지 않는다. 하지만 script 요소 없이도 크로스 사이트 스크립팅 공격은 가능하므로 innerHTML 사용은 안하는 것이 좋다.

innerHTML의 또 다른 단점은 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당할 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업을 파싱한다는 점이다. 이는 제거되지 않아야 할 자식 노드를 삭제하게 되고, 필요할 경우 기존 자식들을 innerHTML에 다시 할당해야 한다.

또 새로운 요소를 삽일할 때 삽입될 위치를 지정할 수 없다는 단점도 있다.

따라서 innerHTML 프로퍼티는 복잡하지 않은 요소를 새롭게 추가할 때 유용하지만 기존 요소를 제거, 또는 새로운 요소를 추가할 때는 사용하지 않는 것이 좋다.

insertAdjacentHTML

Element.prototype.insertAdjacentHTML 메서드는 기존 요소를 제거하지 않고 위치를 지정해서 새로운 요소를 삽입한다. 첫 번째 인수로 위치를 전달하고 두 번째 인수로 HTML 마크업 문자열을 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    <!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>beforebegin</p>');
            $foo.insertAdjacentHTML('beforeend', '<p>beforebegin</p>');
            $foo.insertAdjacentHTML('afterend', '<p>beforebegin</p>');
        </script>
    </html>

단, innerHTML 프로퍼티와 마찬가지로 insertAdjacentHTML 메서드는 HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약하다.

노드 생성과 추가

DOM은 노드를 직접 생성/삽입/삭제/치환하는 메서드를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <!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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    <!DOCTYPE html>
    <html>
        <body>
            <ul id="fruits"></ul>
        </body>
        <script>
            const $fruits = document.getElementById('fruits');
            ['Apple', 'Banana', 'Orange'].forEach(text => {
                // 1. 요소 노드 생성
                const $li = document.createElement('li');
                // 2. 텍스트 노드 생성
                const textNode = document.createTextNode(text);
                // 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
                $li.appendChild(textNode);
                // 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
                $fruits.appendChild($li);
            });
        </script>
    </html>

위 코드처럼 순회를 이용해서 복수의 노드를 생성하고 추가하는 방법이 있다. 하지만 이 코드는 문제가 있다. 3개의 요소를 생성하고 3개의 요소를 DOM에 추가하면서 리플로우와 리페인트가 3번 발생한다. 따라서 위 코드는 비효율적이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    <!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 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
                $container.appendChild($li);
            });

            // 5. 컨테이너 요소 노드를 #fruits 요소 노드 마지막에 추가
            $fruits.appendChild($container);
        </script>
    </html>

위 코드의 경우 DOM의 변화가 한 번만 일어나므로 성능에 유리하다. 하지만 컨테이너 요소가 불필요하게 추가되는 부작용이 있다. 이러한 문제는 DocumentFragment 노드를 통해 해결할 수 있다.

DocumentFragment 노드는 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로, 부모 노드가 없어서 기존 DOM과 별도로 존재한다. DocumentFragment 노드는 위 코드의 컨테이너 노드와 같이 자식 노드들의 부모 노드로서 별도의 서브 DOM을 구성하여 기존 DOM에 추가하기 위한 용도로 사용한다.

DocumentFragment 노드는 DOM에 추가하면 자신은 제거되고 자식 노드만 DOM에 추가된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    <!DOCTYPE html>
    <html>
        <body>
            <ul id="fruits"></ul>
        </body>
        <script>
            const $fruits = document.getElementById('fruits');

            // DocumentFragment 노드 생성... 
            // Document.prototype.createDocumentFragment 메서드는 비어있는 DocumentFragment 노드를 생성 후 반환
            const $fragment = document.createDocumentFragment();

            ['Apple', 'Banana', 'Orange'].forEach(text => {
                // 1. 요소 노드 생성
                const $li = document.createElement('li');
                // 2. 텍스트 노드 생성
                const textNode = document.createTextNode(text);
                // 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
                $li.appendChild(textNode);
                // 4. $li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
                $fragment.appendChild($li);
            });

            // 5. DocumentFragment 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
            $fruits.appendChild($fragment);
        </script>
    </html>

여러 개의 요소 노드를 DOM에 추가하는 경우 DocumentFragment 노드를 사용하는 것이 효율적이다.

This post is licensed under CC BY 4.0 by the author.