[멋쟁이사자처럼] 프론트엔드 스쿨 7기 - 32일차 기록 및 복습 (DOM)
목차
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는 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. 프로그래머란?
- 프로그래머란 기술자다.
- 여러분의 손으로 표현해야 한다.
- 이해하고 끝내서는 안된다. 이론적인 면에서 그치지 않고, 배운 내용을 바탕으로 계속 만들어 봐야한다.