📚 What is TIL?

이벤트객체 그리고 이벤트흐름

이벤트에서 호출되는 핸들러에는 이벤트와 관련된 모든 정보를 가지고 있는 매개변수가 전송된다
여기서 핸들러는 이벤트 발생 시 실행되는 함수로, addEventListener 메서드의 콜백함수를 뜻한다

💡 그렇다면 이벤트 처리 과정에서 발생할 수 있는 문제는 무엇이고, 효율적인 이벤트 핸들링은 무엇일까?

 그것을 알기 위해선 이벤트 흐름을 이해해야한다


우선 브라우저 화면에서 이벤트가 발생하면 브라우저는 가장 먼저 이벤트 대상을 찾기 시작한다
그렇다면 어디부터 찾을까?

브라우저는 DOM 트리 구조의 최상위 요소인 window에서 시작해서 하위 요소로 이벤트 대상을 찾아내려간다
즉, window 객체부터 document, body 순으로 내려가는데 이것을 캡처링 단계라고 한다
이벤트 대상을 찾을 때까지 만나는 모든 캡처링 이벤트 리스터를 실행시키고
이벤트 대상을 찾으면 다시 DOM 트리를 따라 올라간다
내려갈때와는 다르게 역순으로 올라가며 모든 버블링 이벤트 리스너를 실행시키는데 이것을 버블링 단계 라고한다

element.addEventListener('eventType', functionName, [, useCapture]]);

🔎 addEventListener 메서드의 세 번째 매개변수를 통해 캡처링 등록과 버블링 등록이 가능하다
🔎 Default 값이 false 이기때문에 기본적으로 따로 true넣지 않는 이상 버블링으로 등록된다


<article class="parent">
  <button class="btn" type="button">버튼</button>
</article>
const parent = document.querySelector('.parent');
const btnFirst = document.querySelector('.btn');
btnFirst.addEventListener(
  'click',
  event => {
    console.log('btn capture!');
  },
  true,
);

window.addEventListener(
  'click',
  () => {
    console.log('window capture!');
  },
  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!');
});
// window capture!
// document capture!
// parent capture!
// btn capture!
// btn bubble!
// parent bubble!
// document bubble!
// window bubble!

✔️ DOM 트리 구조에 따라 하위요소로 내려가고 상위요소로 올라가는 것을 확인 할 수 있다
✔️ 이렇게 이벤트 리스너가 차례로 실행되는 것을 이벤트 전파라고 한다

💡 특정 요소에 이벤트를 달지않아도 이벤트 리스너가 있는 것처럼 사용할 수 있는 이유는?

 부모에서 자식으로가는 캡처링 단계, 자식에서 부모로가는 버블링 단계 즉, 이벤트 전파때문에 가능한 것이다
 따라서 요소 하나만으로 여러 하위요소들을 처리할 수 있는 장점이 있다
 CSS는 부모를 탐색할 수 없지만 JavaScript에서는 이벤트 흐름으로 부모요소를 탐색할 수 있어서
 유연한 웹 페이지를 구현 가능하다


target, currentTarget

부모부터 자식까지 일련의 요소를 모두 타고가며 진행되는 이러한 이벤트의 특징 덕분에
이벤트 객체에는 target, currentTarget 이라는 속성이 존재한다
target 속성은 이벤트가 발생한 진원지의 정보가 담겨 있다.
currentTarget 속성은 이벤트 리스너가 연결된 요소가 참조되어있다

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

🔎 click 이벤트를 예로들면 target 속성은 부모로부터 이벤트가 위임되어 발생하는 자식의 위치를 말하고,
🔎 currentTarget 속성은 이벤트가 부착된 부모의 위치를 말한다 따라서, target과 같은 요소를 참조할 수 있고, 다를 수도 있다
🔎 article 요소의 하위 요소인 button을 클릭했을 때 이벤트 핸들러가 등록된 article 요소에 접근하는 경우 currentTarget을 사용한다


이벤트 전파를 방지하는 방법

🔎 Event.stopPropagation() - 메서드를 사용하는 순간 버블링을 막아서 이벤트 전파 중지
🔎 Event.stopImmediatePropagation() - 이벤트 전파 중지 + 형제 이벤트 실행 중지
🔎 ​Event.preventDefault() - 이벤트 전파 중지 + 형제 이벤트 실행 중지 + 이벤트 기본 동작 중지
🔎 ​Event.target - 직접 조건식을 지정해서 이벤트 핸들러를 컨트롤


이벤트 위임의 고려성

이벤트 위임으로 li에 이벤트를 붙여놨다고 가정해보자
누군가가 봤을때 아래와 같은 코드를 처음 접했다면,
이벤트가 당연히 button이 있겠지 하다가 헤매게될 수 있다

<ol>
    <li><button type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
</ol>

💡 이벤트위임은 효율적이고 편리한 방법이지만, 특정 상황에 적합하지 않을 수 있다

 1) 이벤트 핸들러를 전파할 필요가 없는 경우
 2) 상위요소에서 이벤트를 막아야 하는 경우
 3) 이벤트가 발생한 요소가 중요한 경우

 이벤트 핸들러가 전파되는 특징은 큰 이점이지만
 이벤트 핸들러를 전파할 필요가 없는데 사용할 경우 불필요한 오버헤드를 발생시킬 수 있다
 또한, 복잡한 상황에서 이벤트 전파를 끊어야하는경우 작동해야할 것이 작동하지 않을 수 있다
 따라서 이벤트가 특정 요소에서만 처리되어야 하는 경우 직접 이벤트 핸들러를 바인딩하는(서로 묶어주는) 것이 좋다


DOM 요소/속성 제어하기

style 객체의 속성 식별자 규칙

const target = document.querySelector('p');
const txtColor = target.style.color; // 현재 스타일 정보를 가져온다
target.style.color = 'red'; // 현재 스타일 정보를 변경한다
target.style.fontWeight = 'bold'; // 현재 스타일 정보에 font-weight 속성이 없다면 추가한다
target.style.color = null; // 현재 스타일 정보를 제거(초기화)한다

🔎 CSS 에서 사용하는 속성 이름 그대로를 사용하면 된다
🔎 속성 이름에 대쉬(-)가 포함되어 있는 경우 카멜케이스로 사용한다 ( font-weight > fontWeight )
🔎 float 같은 속성의 경우 이미 자바스크립트의 예약어로 존재하기 때문에 cssFloat으로 사용된다

⚠️ style 객체를 통해 설정된 스타일은 CSS inline 스타일과 동일한 가중치를 가진다
 따라서, CSS를 통해 수정의 여지가 있는 스타일에는 사용하지 않는 것이 좋다
 이럴경우 classList를 이용한 클래스 제어를 하자

더보기
document.createElement(target)
 target 요소를 생성한다

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

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

element.removeChild(target)
 element의 target 자식 요소를 제거한다

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

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

node.insertBefore(target, location)
 target요소를 parentElement의 자식인 location 위치 앞으로 이동한다

Element.insertAdjacentHTML(beforebegin|beforeend|afterbegin|afterend,text)
 요소 노드를 대상의 인접한 주변에 배치한다

node.firstElementChild
 첫번째 자식을 찾는다

node.lastElementChild
 마지막 자식을 찾는다

node.nextElementSibling
 다음 형제요소를 찾는다

node.previousSibling
 이전 형제노드를 찾는다

node.children
 모든 직계자식을 찾는다

node.parentElement
 부모 요소를 찾는다

Element.closest(selectors)
 자기 자신부터 찾기 시작해(자기자신도 탐색 대상) 부모로 타고 올라가며 가장 가까운 cont 클래스 요소를 찾는다
 단, 형제요소는 찾지 않는다


innerHTML innerText textContent 차이

innerHTML

요소 안에 포함된 문자열 내에서 HTML 마크업을 인지하고 값을 할당할 때,
마크업으로 변환할 수 있는 문자열이 있다면 마크업으로 만들어 보여준다

innerText

렌더링된 텍스트 콘텐츠를 다룬다 문자열에 문법적으로 처리가 가능한 텍스트가 있다면
처리가 끝난 결과물을 텍스트로 전달한다(렌더링 된것이 아니다)

textContent

노드의 텍스트 콘텐츠를 표현한다. 컨텐츠를 텍스트로만 다룬다
즉, HTML로 파싱하지 않고 원시 텍스트로 삽입한다

.hidden {
  display: none;
}
<ul>
  <li>
    <figure>
      <img src="" alt="잭필드" />
      <figcaption>
        상품 가격 : 39,990원
        <span class="hidden">(주의) VIP만 받을 수 있는 혜택입니다.</span>
      </figcaption>
    </figure>
  </li>
</ul>
const info = document.querySelector('figcaption');
console.log(info.textContent);
console.log(info.innerText);
console.log(info.innerHTML);
// 상품 가격 : 39,990원
// (주의) VIP만 받을 수 있는 혜택

//상품 가격 : 39,990원

//상품 가격 : 39,990원
//<span class="hidden">(주의) VIP만 받을 수 있는 혜택</span>

💡 textContent 말고 innerHTML 만 써도될까?

 innerHTML을 사용해도 되지만 텍스트만 표현하기위한 용도로는 textContent를 쓰는게 성능적으로도 빠르다

⚠️ innerHTML를 textContent 처럼 웹페이지에 텍스트를 삽입하는데 사용되는 것은 보안이슈가 있을 수 있다


innerHTML 이슈

const name = 'John';
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

🔎 HTML5 명세서를 보면 script 태그 를 사용하여 삽입된 요소는 innerHTML 삽입될 때 실행하지 않는다고 나와있다
🔎 하지만 예외가 있다

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

🔎 이미지에 대한 경로가 있지만 잘못된 경로로 이미지는 렌더링에 실패할 것이다
🔎 그럼 렌더링이 실패할때 실행될 속성인 onerror가 실행이 되게된다

⚠️ 일반 텍스트를 삽입 할 때는 innerHTML 대신 textContent를 사용하자
⚠️ 악성유저를 고려해서 innerHTML로 입력을 받지 않으면 된다 사용자에게는 textContent로 받자
⚠️ 프로젝트가 보안 점검을 거치는 경우 innerHTML 를 사용하면 잠재적인 보안 위험성때문에
 코드가 거부될 가능성이 높다고 한다


DOM으로 설문지 구현해보기

* {
  box-sizing: border-box;
}

body {
  color: #fff;
}

#mainForm {
  width: 460px;
  margin: 0 auto;
  padding: 20px;
  border-radius: 10px;
  background-color: #ff8787;
}

#mainForm fieldset {
  border: none;
}

#mainForm legend {
  font-size: 1.2em;
  text-align: center;
}

#mainForm label {
  display: block;
}

#mainForm input {
  display: block;
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: none;
  border-radius: 5px;
  /* 그림자의 수평거리, 수직거리, 그림자 테두리 크기, 그림자의 위치(inset: 내부로)  */
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
  transition: all 0.3s;
}

/* 가상클래스 valid. input에 입력받은 값이 유효하다면을 의미한다 */
#mainForm input:valid {
  background-color: #e5ebb2;
}

#mainForm button {
  /* fieldset은 BFC를 생성하기 때문에 해제가 필요 없다 https://html.spec.whatwg.org/multipage/rendering.html#the-fieldset-and-legend-elements */
  float: right;
  padding: 10px;
  margin-left: 10px;
  background-color: #f8c4b4;
  cursor: pointer;
  border-radius: 5px;
  border: none;
}

#mainForm progress {
  width: 100%;
}
<form id="mainForm">
  <fieldset>
    <legend>JS 스터디 모집 설문조사</legend>
    <!-- progress : 작업의 완료 정도를 나타내는 요소입니다. -->
    <progress max="100" value="0" class="bar-progress"></progress>
    <p class="msg-notice">설문지를 작성하세요!</p>

    <label for="q1">질문 1. 무엇을 가장 배우고 싶으세요?</label>
    <input type="text" id="q1" required />
    <label for="q2">질문 2. 시간은 언제가 적당한가요?</label>
    <input type="text" id="q2" required />
    <label for="q3">질문 3. 몇 시간정도 예상하세요?</label>
    <input type="text" id="q3" required />
    <label for="q4">질문 4. 난이도를 조정한다면?</label>
    <input type="text" id="q4" required />
    <label for="q5">질문 5. 끝으로 하고싶은 말이 있나요?</label>
    <input type="text" id="q5" required />

    <button type="submit">제출</button>
    <button type="reset">초기화</button>
  </fieldset>
</form>
const mainForm = document.querySelector('#mainForm');
const message = document.querySelector('.msg-notice');
const info = [
  '설문지를 작성하세요!',
  '시작했습니다!',
  '다음으로 가볼까요!',
  '오 절반지났습니다!',
  '거의 끝났어요!',
  '고생하셨습니다~ 제출해주세요!',
];
let count = 0;

mainForm.addEventListener('input', e => {
  count = 0;
  if (e.target.value === '') {
    e.target.classList.remove('write');
  } else {
    e.target.classList.add('write');
  }
  count = mainForm.querySelectorAll('.write').length;
  message.textContent = info[count];
  bar(count);
});
mainForm.addEventListener('click', e => {
  const input = mainForm.querySelectorAll('input');
  if (e.target.type == 'submit') {
    count == input.length ? bar(0) : (message.textContent = `아직남은 설문이 ${input.length - count}개 남았습니다`);
  } else if (e.target.type == 'reset') {
    bar(0);
    message.textContent = info[0];
    input.forEach(e => {
      e.classList.remove('write');
    });
  }
});

function bar(count) {
  mainForm.querySelector('.bar-progress').setAttribute('value', `${count * 20}`);
}

🔎 상태 게이지를 간단하게 계산하는 좋은 방법도 있었다 validInputs.length / inputs.length \* 100
🔎 최대한 if문을 줄이고 querySelectorAll을 이용해서 구현하는 방법을 선택했다



🔗 TIP

전역과 블록내부에서 선언할때의 차이

const container = document.querySelector('span');
const text = document.querySelector('p');

container.addEventListener('click', e => {
  // const text = e.querySelector('p')
});

🔎 text를 listener 함수 내에서 선언하는 게 전역으로 선언하는 것보다 더 좋을까?
🔎 전역을 오염시키는 것은 코드양이 많아질 것을 생각하면 유지보수 측면에서 안 좋을 수 있지만
🔎 만약 짧은 코드의 경우 listener 함수 안쪽에 쓰면 매번 건들기 때문에 오히려 메모리낭비가 될 수 있다

💡 문서를 탐색한다는 것 자체가 비용이 많이 들기 때문에 최대한 문서를 최소한으로 탐색하는 것이 좋다
💡 querySelector로 요소를 찾는다는 것은 비용이 많이든다는 것만 인지하자


책 추천

책을 사야하는 이유

🔎 구글링은 정보를 한번 찾는다고 끝이나는 게 아니라 한번 더 검증해볼 시간이 필요하다
🔎 책에는 검증된 이야기만 실려있고 목차를 보며 원하는 정보를 빨리 찾을 수도 있다
🔎 공부할때 레퍼런스가 될만한 책을 가지고 있자 한가지에 대해 깊게 다룬 책을 사는 것도 좋다

카테고리:

업데이트:

댓글남기기