Front-end 개발

[멋쟁이사자처럼] 프론트엔드 스쿨 7기 - 32일차 기록 및 복습 (DOM)

로그 생성기 2023. 8. 21. 11:53

목차

1. JS DOM

2. 잡담


1. JS DOM (Document Object Model)


1-1. DOM 노드 요소 제어

- DOM API를 이용하면 요소를 새롭게 생성하고, 위치하고, 제거할 수 있다.

(1) document.createElement(target) : target 요소를 생성한다.

(2) document.createTextNode(target) : target 텍스트를 생성한다.

(3) document.appendChild(target) : target 요소를 element의 자식으로 위치한다.

(4) document.removeChild(target) : element 의 target 자식 요소를 제거

- 1~4 이후에 만들어진 새로운 메서드 (훨씬 편리하다)

(5) document.append(target) : target 요소를 element 의 자식으로 위치한다. appendChild와 다른점은 노드 뿐만 아니라 여러 개의 노드를 한번에, 그리고 텍스트도 자식 노드로 포함시킬 수 있다는 것.

(6) document.remove(target) : target 요소를 제거한다.

- DOM (Document Object Model) 이라는 것은 노드를 트리형태로 만든 집합체

- text는 요소 안에 들어가는 콘텐츠

- 자식 요소를 넣으려면 필요할 때마다 각각 새로 만들어야 한다. (요소 대량생산할 때는 반복문을 사용한다)

- createElement 또는 createTextNode 를 재사용할 수 없다. (노드는 1:1 매칭으로 한 요소에만 쓸 수 있다)

- remove 는 removeChild 와 달리 부모와 자식을 모두 알 필요없이 지정해서 다 지울 수 있다.

- append 는 여러개의 요소를 한번에 붙일 수 있다.

- 아래 예제는 버튼을 누르면 ul의 자식요소로 li 가 자동으로 생성되는 예제이다.

<body>
    <div class="parent"></div>
    <ul></ul>
    <button>생산시작!</button>
    
    <script>
        const myBtn = document.querySelector("button");
        const myUl = document.querySelector("ul");

        myBtn.addEventListener('click', function(){
            for(let i=0; i < 5; i++){
                const myLi = document.createElement('li');
                myUl.appendChild(myLi);
            }
        })
    </script>
</body>

- 스코프는 전역, 블록, 지역 스코프가 있다.

- 반복문에서는 블록 스코프가 순회를 돌 때마다 새롭게 내부 요소가 생성되기 때문에 노드가 새로 생성되고 추가된다.

<body>
    <div class="parent"></div>
    <ul></ul>
    <button>생산시작!</button>
    
    <script>
        const myBtn = document.querySelector("button");
        const myUl = document.querySelector("ul");

        myBtn.addEventListener('click', function () {
            for (let i = 0; i < 5; i++) {
                const myLi = document.createElement('li');
                const btnDel = document.createElement('button');
                const btnTxt = document.createTextNode('버튼');

                btnDel.append(btnTxt);
                btnDel.addEventListener('click', () => {
                    myLi.remove();
                });
                myLi.append('삭제하기: ', btnDel);
                myUl.appendChild(myLi);
            }
        });
    </script>
</body>

- element.append() 는 자식 요소의 제일 마지막에 붙는다.

- 해당 요소의 앞에 붙이고 싶을 때는 element.insertBefore() 를 사용한다. 여기엔 두개의 인자가 필요하다.

<body>
    <div id="parentElement">
        <span id="childElement">hello guys~</span>
    </div>
    <script>
        // parentElement.insertBefore(target, location); target요소를 parentElement의 자식인 location 위치 앞으로 이동합니다.

        var span = document.createElement("span");
        var sibling = document.getElementById("childElement");
        var parentDiv = document.getElementById("parentElement");
        parentDiv.insertBefore(span, sibling);
    </script>
</body>

- Element node 생성과 위치 지정을 한번에 할 수 있다.

- element.textContent = myInput.value; 처럼 한번에 생성하고 위치키시는 것을 생략할 수 있다.

- JS는 예전 코드는 그대로 두고 개발자들의 니즈를 반영하여 점점 새로운 기능을 추가한다.

<body>
    <p></p>
    <input type="text">
    <button>Write Something!</button>

    <script>
        const myBtn = document.querySelector('button');
        const myP = document.querySelector('p');
        const myInput = document.querySelector('input');

        myBtn.addEventListener('click', function () {
            myP.textContent = myInput.value;
        })
    </script>
</body>

- input 요소에 이벤트를 달아서 바로바로 반영할 수도 있다.

- element.innerHTML 을 통해 내부 HTML 구조를 반환할 수도 있고, 값을 할당할 수도 있다.

- innerHTML 은 HTML 요소로 바꿀 수 있다면 HTML 요소를 넣고, 아니면 그저 text를 textNode로 넣어준다.

- 기능은 비슷하긴 하지만 그냥 text 만 넣을거면 .textContent 를 사용하는 것을 권장한다.

- innerHTML 은 요소(element) 내에 포함된 HTML 마크업을 가져오거나 설정합니다.

- 여기서 중요한 점은 innerHTML로 값을 할당할 때, 마크업으로 변환할 수 있는 문자열이 있다면 마크업으로 만들어 보여준다는 것 입니다. 만약 그런 문자열이 없다면 그냥 문자열만 컨텐츠로 설정합니다.

- innerText 속성은 요소의 렌더링된 텍스트 콘텐츠를 나타냅니다. (렌더링된에 주목하세요. innerText는 텍스트 내에 문법적으로 처리가 가능한 텍스트가 있으면 처리가 끝난 결과물을 텍스트로 전달합니다.)

- textContent 속성은 노드의 텍스트 콘텐츠를 표현합니다. 컨텐츠를 단순히 텍스트로만 다룹니다.

<body>
    <p></p>
    <input type="text">
    <button>Write Something!</button>

    <script>
        const myBtn = document.querySelector('button');
        const myP = document.querySelector('p');
        const myInput = document.querySelector('input');

        // input 요소에 'input' 이벤트를 연결하면 실시간으로 값이 반영되게 만들 수도 있습니다.
        myInput.addEventListener('input', function () {
            myP.textContent = myInput.value;
        })
        
        myP.innerHTML = "<strong>I'm Strong!!</strong>";
    </script>
</body>

- element.innerText의 쓰임을 element.textContent 와 비교하며 알아보자.

- 기능상으로 보면 element.textContent 랑 매우 비슷하다.

- textContent는 innerHTML과 달리 HTML 로 변환할 수 있는 문자열을 넣어도 HTML 요소로 변환하지 않고 그냥 text로 넣는다.

- 모달을 넣어서 생성할 때 innerHTML로 넣는다. (createElement 보다 템플릿 리터럴(``)이 가독성이 높다.)

- 이렇게 보면 element.textContent 랑 더욱 비슷해 보인다.

<body>
    <h3>원본 요소:</h3>
    <p id="source">
        <style>
            #source {
                color: red;
            }
        </style>
        아래에서<br />이 글을<br />어떻게 인식하는지 살펴보세요.
        <span style="display:none">숨겨진 글</span>
    </p>
    <h3>textContent 결과:</h3>
    <textarea id="textContentOutput" rows="6" cols="30" readonly>...</textarea>
    <h3>innerText 결과:</h3>
    <textarea id="innerTextOutput" rows="6" cols="30" readonly>...</textarea>


    <script>
        const source = document.getElementById("source");
        const textContentOutput = document.getElementById("textContentOutput");
        const innerTextOutput = document.getElementById("innerTextOutput");

        textContentOutput.innerHTML = source.textContent;
        innerTextOutput.innerHTML = source.innerText;
    </script>
</body>

textContent 와 innerText 코드 실행 결과

- textContent는  HTML 내에서도 태그를 무시하고 순수한 Text 만 뽑아서 전달한다.

- innerText 는 HTML 태그를 인식하고 CSS 꾸며주는 속성을 제외하고 HTML 기능을 적용시킨다. 단, CSS display 속성만을 인식한다. HTML 문서가 실제 화면에 표현(렌더링)된 텍스트 콘텐츠를 나타낸다.

- innerText는 HTML을 인식하지만 CSS 는 일부 속성만 인식한다.

- innerHTML은 보안상의 이유로 사용을 권장하지 않는다.

- innerText는 이렇게 사용할 수 있다.

<body>
    <figure>
        <img src="" alt="청바지 삼종세트">
        <figcaption>상품 가격 : 29,900원 <span class="hidden">VIP 회원이라면 29,900원!!</span></figcaption>
    </figure>


    <script>
        const cost = document.querySelector('figcaption');
        console.log(cost.textContent); // 모든 텍스트를 가져온다.
        console.log(cost.innerText); // HTML 을 인식하고 화면에 렌더링된 Text 만을 가져온다.
    </script>
</body>

- innerHTML 은 createElement 나 removeElement 등을 생략하고 사용하기 편리한데 주의할 점이 있다.

- 보안적인 문제가 있다. MDN 에 의하면 innerHTML 이 웹 페이지에 텍스트를 삽입하는데 사용되는 것을 종종 볼 수 있데, 이것은 사이트의 공격 경로가 되어 잠재적인 보안 위험이 발생할 수 있다.

const name = "John";
// assuming 'el' is an HTML DOM element
el.innerHTML = name; // harmless in this case

// ...

name = "<script>alert('I am John in an annoying alert!')</script>";
el.innerHTML = name; // harmless in this case

- innerHTML 이 HTML 을 인식하고 동작한다면, script 태그를 인식하고 정상 작동한다면 웹 페이지를 공격할 수 있다. 하지만 다행이도 innerHTML은 sciprt 태그를 인식하지 않는다.

- 그러나 <script>요소를 사용하지 않고, 자바스크립트를 실행하는 방법이 있으므로, innerHTML을 사용하여 제어할 수 없는 문자열을 설정할 때 마다 여전히 보안위험이 있다.

- 이미지가 에러가 났을 때 alert(1) 이 작동하도록 해둔 이벤트 코드이다.

const name = "<img src='x' onerror='alert(1)'>";
el.innerHTML = name; // shows the alert
// el : element

- 위의 내용을 통해 innerHTML이 의도치 않게 자바스크립트 코드를 실행시킬 수 있다는 것을 알게 되었습니다. 그럼 innerHTML은 사용해서는 안되는 속성일까요? 그렇지 않습니다. innerHTML은 템플릿 리터럴과 조합해 복잡한 HTML 구조도 동적으로 손쉽게 생성할 수 있다는 장점이 있습니다. 

- 자바스크립트를 작동시킬 수 있는 가능성이 있으니 나쁜 의도를 가진 사용자가 코드를 입력 할 수 없도록 사용자의 입력 값을 innerHTML을 통해 할당 받는 일만 없도록 하면 안전하게 사용할 수 있습니다.

- 권장 사항으로 innerHTML 대신 innerText 혹은 textContent 속성을 이용하자.

 

1-2. JS 를 통해서 CSS 속성 제어하기

- 자바스크립트를 사용하여 요소의 속성을 제어하는 방법은 다양하다.

(1) 요소의 스타일을 제어하는 style 객체 (권장 X)

(2) 속성에 접근하고 수정할 수 있는 Attribute 메소드

(3) 요소에 데이터를 저장하도록 도와주는 data 속성

- text.style.textAlign

- text.style.color = 'blue';

- 보통 현재 style 객체의 제거(초기화)에는 null 을 사용한다. target.style.color = null;

- style 객체의 속성 식별자 규칙은 CSS 속성 이름을 대시의 경우 카멜케이스로 작성한다.

- float 속성의 경우 이미 자바스크립트의 예약어로 존재하기 때문에 cssFloat 로 사용된다.

- 하지만 style 로 속성을 수정하는 것은 CSS inline 스타일과 동일한 가중치를 가지므로 때문에 CSS를 통해 수정의 여지가 있는 스타일에는 많이 사용되지 않는 편입니다. 이럴 경우 classList를 이용한 클래스 제어가 더 효과적입니다.

- Attribute 메소드를 사용하면 속성에 접근하고 수정할 수 있다.

 (1) getAttribute 메소드는 요소의 특정 속성 값에 접근할 수 있도록 한다.

 (2) setAttribute 메소드는 요소의 특정 속성 값에 접근하여 값을 수정한다.

<body>
    <p id='myTxt'>hello lions</p>
    <div>
        <img
            src="https://static.ebs.co.kr/images/public/lectures/2014/06/19/10/bhpImg/44deb98d-1c50-4073-9bd7-2c2c28d65f9e.jpg">
    </div>


    <script>
        const target = document.querySelector('p');
        const myimg = document.querySelector('img');
        const idAttr = target.getAttribute('id');
        console.log(idAttr);
        myimg.setAttribute("src", "https://img.wendybook.com/image_detail/img159/159599_01.jpg");

        const div = document.querySelector('div');
        div.innerHTML = " <img src = 'https://img.wendybook.com/image_detail/img159/159599_01.jpg'> "
    </script>
</body>

- data-* 속성을 사용하면 HTML 요소에 추가적인 정보를 저장하여 마치 프로그램 가능한 객체처럼 사용할 수 있다.

- 단, data 속성의 이름에는 콜론(:) 이나 영문 대문자가 들어가서는 안된다.

- 아래 예제의 img 를 JS 객체처럼 활용할 수 있다.

<img
    class="terran battle-cruiser"
    src="battle-cruiser.png"
    data-ship-id="324"
    data-weapons="laser"
    data-health="400"
    data-mana="250"
    data-skill="yamato-cannon"
/>
<script>
    const img = document.querySelector('img')
    console.log(img.dataset);
    console.log(img.dataset.shipId);
</script>

- image.dataset.health 처럼 data 를 불러올 수 있다.

- querySelctor 로 DOM tree 를 탐색하는 것은 자원낭비가 심하다. 그렇기 때문에 data-* 속성으로 요소에 저장해두고 dataset 으로 찾아 오는 것이 훨씬 쉽고 효율적이다. (HTML 요소에 데이터를 저장하고 불러오는 기능)

<body>
    <ul>
        <li>
            <button data-name="redcola" data-cost="1000">
                <img src="" alt="">
                <span>redcola</span>
                <strong>1000</strong>
            </button>
        </li>
    </ul>

    <script>
        const btn = document.querySelector('button');
        btn.addEventListener('click', function () {
            console.log(this.dataset.name, this.dataset.cost);
            console.log(btn.querySelector('span').textContent);
        })
    </script>
</body>

- (append, appendChild 외) 요소를 배치하는 또 다른 방법

- 더 인접한 곳(Adjacent)으로 정밀하게 배치하기

- insertAdjacentHTML : 요소 노드를 대상의 인접한 주변에 배치합니다.

- begin 은 열린 태그, end 는 닫는 태그를 의미한다.

- 'beforebegin' : 여는 태그 앞(이전)

- 'afterbegin' : 여는 태그 바로 다음

- 'beforeend' : 닫는 태그 이전

- 'afterend' : 닫는 태그 바로 다음

<strong class="sayHi">
    반갑습니다.
</strong>
const sayHi = document.querySelector('.sayHi');
sayHi.insertAdjacentHTML('beforebegin', '<span>안녕하세요 저는</span>'); // 여는 태그 앞(이전)
sayHi.insertAdjacentHTML('afterbegin', '<span>재현입니다</span>'); // 여는 태그 바로 다음
sayHi.insertAdjacentHTML('beforeend', '<span>면접오시면</span>'); // 닫는 태그 이전
sayHi.insertAdjacentHTML('afterend', '<span>치킨사드릴게요</span>'); // 닫는 태그 바로 다음

 

1-3. DOM 안에서 노드 탐색하기

-  firstElementChild 는 첫번째 자식 요소를 찾고, firstChild 는 첫번째 node 요소(개행문자도 포함)를 찾는다.

- 주의할 것은 closest 는 자기 자신부터 시작해 가장 가까운 부모요소를 찾고 형제요소는 찾지 않는다.

- 외울 필요없고 필요할 때 찾아서 쓰면 된다.

<!-- 주석입니다 주석. -->
<article class="cont">
    <h1>안녕하세요 저는 이런 사람입니다.</h1>
    <p>지금부터 자기소개 올리겠습니다</p>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt incidunt voluptates laudantium fugit, omnis
    dolore itaque esse exercitationem quam culpa praesentium, quisquam repudiandae aut. Molestias qui quas ea iure
    officiis.
    <strong>감사합니다!</strong>
</article>
const cont = document.querySelector(".cont");
console.log(cont.firstElementChild);  // 첫번째 자식을 찾습니다.
console.log(cont.lastElementChild);   // 마지막 자식을 찾습니다.
console.log(cont.nextElementSibling); // 다음 형제요소를 찾습니다.
console.log(cont.previousSibling);    // 이전 형제노드를 찾습니다.
console.log(cont.children);           // 모든 직계자식을 찾습니다.
console.log(cont.parentElement);      // 부모 요소를 찾습니다.
// 자기 자신부터 시작해 부모로 타고 올라가며 가장 가까운 cont 클래스 요소를 찾습니다. 단, 형제요소는 찾지 않습니다.
console.log(cont.querySelector('strong').closest('.cont').innerHTML);​

 

1-4. 실습

- 우리가 한 실습은 기초

See the Pen Select-box Basic by redcontroller (@redcontroller) on CodePen.

 

- 선택자는 최대한 좁은 범위를 탐색하도록 지정해야 한다.

- 성능을 고려해야 하는 경우는, 메뉴 같은 것들이 나오는 속도가 느리면 고려해야 한다.

- 웹 접근성까지 고려한 정답 (중급 이상)

See the Pen Select-box Advance by redcontroller (@redcontroller) on CodePen.

 

1-5. 이벤트 객체와 흐름 (DOM 의 꽃  : 면접 단골 질문)

- 이벤트에 호출되는 핸들러(addEventListener 의 콜백함수)에는 이벤트와 관련된 모든 정보를 가지고 있는 매개변수가 전송된다. 이것이 바로 이벤트 객체.

<article class="parent">
    <ol>
        <li><button class="btn-first" type="button">버튼1</button></li>
        <li><button type="button">버튼2</button></li>
        <li><button type="button">버튼3</button></li>
    </ol>
</article>
const btnFirst = document.querySelector('.btn-first');
btnFirst.addEventListener('click', (event) => {
    console.log(event);
});

- 브라우저 화면에서 이벤트가 발생하면 브라우저는 가장 먼저 이벤트 대상을 찾기 시작한다.

- 브라우저가 이벤트 대상을 찾아갈 때는 가장 상위 window 객체부터 html, body 순으로 DOM 트리를 따라 내려간다. 결국 타겟까지 내려간다. 이를 캡처링 단계라고 한다.

- 캡처링 이벤트: 부모 DOM 에서 자식 DOM 으로 타고 내려가면서 이벤트를 찾아 나가는 과정

- 캡처링 과정에서 실행되는 이벤트를 캡처링 이벤트라고 한다.

- 캡처링 이벤트를 사용하는 방법은 콜백 함수 다음으로 true 를 주면 된다.

- 이벤트가 발생한 자식 DOM 에서 최상위 DOM 까지 타고 올라가는 과정을 버블링 이벤트 (돌고래 초음파 처럼 찾는다)

- 바다 속에서 방울이 보글보글 올라가는 이미지다.

- 리스너가 차례로 실행되는 것을 이벤트 전파(event propagation) 라고 한다.

- 버블링 이벤트를 사용하는 방식은 콜백 함수 다음으로 false 를 주면 된다. (default 값은 false, 버블링 이벤트)

이벤트 흐름

- window, document, parent 에 캡처링 이벤트가 달려있다. (true)

- btnFirst, parent, document, window 에 버블링 이벤트가 달려있다. (default === false)

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../reset.css">
    <style>
    </style>
</head>

<body>
    <article class="parent">
        <button class="btn" type="button">버튼</button>
    </article>

    <script>
        const parent = document.querySelector('.parent');
        const btnFirst = document.querySelector('.btn');
        btnFirst.addEventListener('click', (event) => {
            console.log("btn capture!");
        })

        window.addEventListener('click', () => {
            console.log("window capture!");
        }, true); // true : 캡처링 단계의 이벤트가 발생하도록 합니다.

        document.addEventListener('click', () => {
            console.log("document capture!");
        }, true);

        parent.addEventListener('click', () => {
            console.log("parent capture!");
        }, true);

        btnFirst.addEventListener('click', (event) => {
            console.log("btn bubble!");
        })

        parent.addEventListener('click', () => {
            console.log("parent bubble!");
        });

        document.addEventListener('click', () => {
            console.log("document bubble!");
        });

        window.addEventListener('click', () => {
            console.log("window bubble!");
        });
    </script>
</body>

</html>

- 첫번째 addEnventListener 는 default 값으로 bubling Event 이므로 캡처링 이벤트가 모두 실행 후 실행된다.

- 이벤트 흐름은 크롬이건 사파리건 똑같다.

- 하지만 form 내부의 button 의 경우 둘 사이의 관계는 강제되어 있다.
- form 태그를 캡쳐링으로 변경해도 버튼보다 늦게 실행되게 된다.
- 이것은 form 이 true 를 줌으로써 캡처링으로 변경해도 버블링으로 실행될 것이라고 본다.

<body>
    <form action="">
        <input type="text">
        <button>버튼!</button>
    </form>

    <script>
        const form = document.querySelector('form');
        const btn = document.querySelector('button');

        form.addEventListener('submit', (event) => {
            event.preventDefault(); // 새로 고침을 막는다.
            console.log('hello submit');
        }, true); // 캡처링 이벤트임에도 불구하고 button 보다 늦게 실행된다.
        // submit 은 무조건 버블링으로 실행될 것이다.

        btn.addEventListener('click', () => {
            console.log('hello button');
        });
    </script>
</body>

2. 잡담

2-1. 프로그래머란?

- 프로그래머란 기술자다.

- 여러분의 손으로 표현해야 한다.

- 이해하고 끝내서는 안된다. 이론적인 면에서 그치지 않고, 배운 내용을 바탕으로 계속 만들어 봐야한다.