JavaScript - Events

2024. 1. 7. 15:41개인노트-개인공부

목차

  • 이벤트 추가 및 제거
  • 이벤트 객체
  • 기본 동작 방지
  • 버블링과 캡처링
  • 이벤트 옵션
  • 이벤트 위임
  • 키보드 이벤트
  • 양식과 포커스 이벤트
  • 커스텀 이벤트와 디스패치

 

# 이벤트 추가 및 제거

  • .addEventListener()로 이벤트를 등록합니다.
    • 대상에 이벤트 청취(Listen)를 등록합니다.
    • 대상에 지정한 이벤트가 발생했을 때 지정한 함수(Handler)가 호출됩니다.
  • .removeEventListener()로 등록된 이벤트를 제거합니다.
    • 대상에 등록했던 이벤트 청취(Listen)를 제거합니다.
    • 메모리 관리를 위해 등록한 이벤트를 제거하는 과정이 필요할 수 있습니다.

## 예시 코드

<!-- index.html -->
<body>
  <div class='parent'>
    <div class='child'>
      <a href='https://www.naver.com' target='_blank'>
        click
      </a>
    </div>
  </div>
</body>

### .addEventListener()의 예시코드

// main.js

// .addEventListener()
// 대상에 이벤트 청취(Listen)를 등록합니다.
// 대상에 지정한 이벤트가 발생했을 때 지정한 함수(Handler)가 호출됩니다.

const parentEl = document.querySelector('.parent');
const childEl = document.querySelector('.child');

parentEl.addEventListener('click', () => {
  console.log('Parent!');
});
childEl.addEventListener('click', () => {
  console.log('Child!');
});

클릭 결과

parent엘리먼트를 클릭하면 'Parent!'가 출력되고,

child엘리먼트를 클릭하면 'Child!', 'Parent!' 2개가 출력됩니다. child 엘리먼트의 영역은 child 영역이면서 parent영역이기도 하기 때문입니다.

### .removeEventListener()의 예시코드

// main.js

// .removeEventListener()
// 대상에 등록했던 이벤트 청취(Listen)를 제거합니다.
// 메모리 관리를 위해 등록한 이벤트를 제거하는 과정이 필요할 수 있습니다.

const parentEl = document.querySelector('.parent');
const childEl = document.querySelector('.child');

const handler = () => {
  console.log('Parent!');
};

parentEl.addEventListener('click', handler);
childEl.addEventListener('click', () => {
  parentEl.removeEventListener('click', handler);
});

## 정리

  • .addEventListener()로 이벤트를 등록합니다.
  • .removeEventListener()로 등록된 이벤트를 제거합니다.
  • child 요소를 '클릭'하면 그 조상인 parent의 '클릭' 이벤트도 동작합니다.

 

# 이벤트 객체

  • 이벤트 객체는 대상에서 발생한 이벤트 정보를 담고 있습니다.
  • 이벤트 리스너의 콜백함수 매개변수로 이벤트 객체가 전달됩니다.

## 예시 코드

// main.js

// 이벤트 객체
// 이벤트 객체는 대상에서 발생한 이벤트 정보를 담고 있습니다.

const parentEl = document.querySelector('.parent');

parentEl.addEventListener('click', event => {
  console.log(event.target, event.currentTarget);
});
childEl.addEventListener('wheel', event => {
  console.log(event);
});

 

클릭 결과

console.log(event.target, event.currentTarget)의 결과

parent 요소를 클릭했을 경우
child요소를 클릭했을 경우

target : 이벤트가 발생한 해당 요소

currentTarget : 이벤트가 등록된 요소

## 정리

  • 이벤트 객체에서는 대상에서 발생한 다양한 이벤트 정보를 담고 있습니다.

 

# 기본 동작 방지

  • event.preventDefault() : 이벤트의 기본 동작을 막을 수 있습니다.
    • 예 : a 태그에서의 페이지 이동

## 예시 코드

// main.js

// 마우스 휠의 스크롤 동작 방지!
const parentEl = document.querySelector('.parent');
parentEl.addEventListener('wheel', event => {
  event.preventDefault();
  console.log('Wheel!');
  // console.log에 'Wheel!' 이라는 문자는 나오지만, 화면에서 Wheel은 동작하지 않습니다.
});

// <a> 태그에서 페이지 이동 방지!
const anchorEl = document.querySelector('a');
childEl.addEventListener('click', event => {
  event.preventDefault();
  console.log('Click!');
  // console.log에 'Click!' 이라는 문자는 나오지만, 페이지가 이동되지는 않습니다.
});

## 정리

  • 이벤트의 기본 동작이 필요하지 않는 경우가 있을 수 있습니다. 그런 경우 이벤트 콜백 메소드 안에 event.preventDefault()함수를 호출하여 이벤트의 기본 동작을 막을 수 있습니다.

 

# 버블링과 캡쳐링

  • 캡쳐링 : 이벤트가 하위 요소로 전파되는 단계.
    • addEventListener의 세번째 인수로 {capture: true}를 적용하면 그 이벤트는 버블링 단계가 아닌 캡쳐링 단계에서 동작합니다.
  • 버블링 : 이벤트 버블링은 타깃 이벤트에서 시작해서 <html> 요소를 거쳐 document 객체를 만날 때까지 상위요소로 전파되어 각 노드에서 모두 이벤트가 발생하는 현상입니다.
    • event.stopPropagation()으로 버블링을 정지 시킬 수 있습니다.

## 예시 코드

### 버블링 실습 예시

<!-- index.html -->
<body>
  <div class='parent'>
    <div class='child'>
      <a href='https://www.naver.com' target='_blank'>
        click
      </a>
    </div>
  </div>
</body>
// main.js

// 이벤트 전파(버블) 정지

const parentEl = document.querySelector('.parent');
const childEl = document.querySelector('.child');
const anchorEl = document.querySelector('a');

window.addEventListener('click', event => {
  console.log('Window!');
});
document.body.addEventListener('click', event => {
  console.log('Body!');
});
parentEl.addEventListener('click', event => {
  console.log('Parent!');
  event.stopPropagation(); // 버블링 정지!
});
childEl.addEventListener('click', event => {
  console.log('Child!');
});
anchorEl.addEventListener('click', event => {
  console.log('Anchor!');
});

 

 

이벤트 버블링

stopPropagation()을 주석처리하고, 최하위 자식인 Anchor를 클릭하면 최상위 조상까지 이벤트가 전파됩니다.
stopPropagation()을 사용하면 그 보다 상위 요소에는 이벤트가 전파되지 않습니다.

### 캡쳐링 실습 예시

// main.js

// 캡쳐링
// addEventListener의 세번째 인수로 {capture: true}를 전달합니다.
// {capture: true}이면 그 이벤트는 캡쳐링 단계에서 동작합니다.

const parentEl = document.querySelector('.parent');
const childEl = document.querySelector('.child');
const anchorEl = document.querySelector('a');

window.addEventListener('click', event => {
  console.log('Window!');
});
document.body.addEventListener('click', event => {
  console.log('Body!');
}, {capture: true});
parentEl.addEventListener('click', event => {
  console.log('Parent!');
});
childEl.addEventListener('click', event => {
  console.log('Child!');
});
anchorEl.addEventListener('click', event => {
  console.log('Anchor!');
});

 

이벤트 캡쳐링

{capture: true} 가 적용된 EventListener는 버블링 단계에서 동작하지 않고, 캡쳐링 단계에서 동작합니다.

이벤트 동작 순서 : 상위(window)에서 하위요소(타겟)로..(캡쳐링) -> 이벤트 타겟 -> 하위요소(타겟)에서 상위(window)로..(버블링)

Body에 capture 옵션을 적용하고, Child를 클릭하면 적용된 Body가 먼저 console에 출력됩니다. 나머지 요소의 순서는 버블링 순서와 같습니다.

 

## 정리

 

# 이벤트 옵션

## once: true

  • { once: true } : 이벤트가 단 한번만 동작합니다.
// 핸들러를 한 번만 실행

const parentEl = document.querySelector('.parent');

parentEl.addEventListener('click', event => {
  console.log('Parent!');
}, {
  once: true
});

## passive: true

  • { passive: true } : touch, wheel 등 일부 이벤트에서 동작을 최적화하여 스크롤 성능을 대폭 향상시킬 수 있는 웹 표준 기능입니다.
// 기본 동작과 핸들러 실행 분리

const parentEl = document.querySelector('.parent');

parentEl.addEventListener('sheel', event => {
  for(let i = 0; i < 10000; i += 1) {
    console.log(i);
  }
}, {
  passive: true
});

위의 for 문의 실행으로 인해서 화면에서의 wheel 동작이 버벅일 수 있습니다. passive: true로 화면이 버벅이는 현상을 줄일 수 있습니다.

 

# 이벤트 위임

  • 이벤트 위임(Delegation)
  • 비슷한 패턴의 여러 요소에서 이벤트를 핸들링해야 하는 경우,
  • 단일 조상 요소에서 제어하는 이벤트 위임 패턴을 사용할 수 있습니다.

## 예시 코드

### 이벤트 위임없이 이벤트 등록

아래 코드는 child에 총 4번의 이벤트를 각각 등록하여 사용합니다.

<!-- index.html -->
<!-- ... -->
<body>
  <div class='parent'>
    <div class='child'>1</div>
    <div class='child'>2</div>
    <div class='child'>3</div>
    <div class='child'>4</div>
  </div>
</body>
<!-- ... -->
// main.js

const parentEl = document.querySelector('.parent');
const childEls = document.querySelectorAll('.child');

// 모든 대상 요소에 이벤트 등록
childEls.forEach(el => {
  el.addEventListener('click', event => {
    console.log(event.target.textContent);
  });
});

### 이벤트 위임을 사용한 패턴

// main.js

const parentEl = document.querySelector('.parent');
const childEls = document.querySelectorAll('.child');

// 조상 요소에 이벤트 위임
parentEl.addEventListener('click', event => {
  const childEl = event.target.closest('.child');
  if (childEl) {
    console.log(childEl.textContent);
  }
});

target : 실제로 이벤트가 발생한 요소.

closest 메소드 : 대상 요소의 선택자와 일치하는 (대상요소를 포함한)가장 가까운 조상요소를 찾습니다.

## 정리

  • 엘리먼트가 많아지면 많아질 수록 이벤트 핸들러를 등록하는 것을 절약할 수 있습니다.
  • 코드를 한 번만 작성하기 때문에 효율적으로 어플리케이션을 관리할 수 있습니다.

# 키보드 이벤트

  • 키보드로 한글을 입력하면 브라우저는 입력된 한글을 처리하는 과정이 필요합니다.
  • 특히, input요소에서 엔터키나 탭키 처럼 입력을 완료하는 키를 누르거나 혹은 keydown, keyup의 이벤트를 발생시키면 그 이벤트가 두 번 처리되는 현상이 나타납니다.
  • 이는 한글 뿐만이 아니라 중국어나 일본어에서도 발생합니다.
  • 이러한 CJK문자(한글,중국,일본어)는 브라우저에서 처리하는 과정이 한 단계가 더 필요하기 때문에 두 번의 결과가 출력됩니다.
  • 이런 중간 처리과정을 event.isComposing 을 통하여 알 수 있습니다.

한글을 입력하고 엔터를 입력하면 '안녕하세요'가 두 번 출력됩니다.

## 예시 코드

// main.js

// Keyboard Events

// keydown : 키를 누를 때
// keyup : 키를 땔 때

const inputEl = document.querySelector('input');

inputEl.addEventListener('keydown', event => {
  if (event.key === 'Enter') {
    console.log(event.isComposing);
    console.log(event.target.value); // 한글을 입력하면 두 번 처리됩니다.
  }
});

위 코드 결과. true는 처리중. false는 처리끝을 의미. 아래쪽의 안녕하세요를 사용하면 됩니다.

// 더 나은 코드
inputEl.addEventListener('keydown', event => {
  if (event.key === 'Enter' && !event.isComposing) {
    console.log(event.isComposing);
    console.log(event.target.value); // 이제 한글을 입력해도 한 번만 처리 됩니다.
  }
});

위 코드 결과. 이제 한 번만 출력됩니다.


## 정리

  • 키보드 이벤트를 다룰 때, 엔터나 탭키처럼 입력을 완료하는 코드과 한글 입력을 같이 사용한다면, 이벤트의 isComposing 속성을 같이 로직으로 처리하는 것이 필요합니다.

# 양식과 포커스 이벤트

  • focus : 요소가 포커스를 얻었을 때
  • blur : 요소가 포커스를 잃었을 때
  • input : 값이 변경되었을 때
  • change : 상태가 변경되었을 때
  • submit : 제출 버튼을 선택했을 때
  • reset : 리셋 버튼을 선택했을 때

## 예시 코드

  <!-- ... -->
  <style>
    form {
      padding: 10px;
      border: 4px solid transparent;
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
    }
    form.active {
      border-color: orange;
    }
  </style>
</head>
<body>
  <form>
    <input type="text" placeholder="ID" />
    <input type="password" placeholder="PW" />
    <button type="submit">제출</button>
    <button type="reset">초기화</button>
  </form>
</body>
<!-- ... -->
// main.js
// Focus & Form Events

const formEl = document.querySelector('form');
const inputEls = document.querySelectorAll('input');

inputEls.forEach(el => {
  el.addEventListener('focus', () => {
    formEl.classList.add('active');
  });
  el.addEventListener('blur', () => {
    formEl.classList.remove('active');
  });
  el.addEventListener('change', event => {
    console.log(event.target.value);
  });
});

formEl.addEventListener('submit', event => {
  event.preventDefault();
  const data = {
    id: event.target[0].value,
    pw: event.target[1].value,
  }
  console.log('제출!', data);
});

formEl.addEventListener('reset', event => {
  console.log('리셋!');
});

## 정리

  • 이벤트
    • input : 값이 변경되었을 때(키보드를 입력할 때마다 동작합니다.)
    • change : 상태가 변경되었을 때(모든 값 입력을 끝내고 다른 요소로 포커스를 이동했을 때 동작합니다). 즉, 엘리먼트의 상태가 변경되었을 때 동작합니다.
  • 제출 버튼(type='submit'인 버튼)을 클릭하면 form에 submit이라는 이벤트가 발생합니다. 
  • form요소에서는 submit 이벤트가 발생하면 페이작 새로고침되는 것이 기본동작입니다.
    • 페이지를 새로고침하는 기본 동작을 막기위해 event.preventDefault();를 사용합니다.

 

# 커스텀 이벤트와 디스패치

디스패치

  • 화면에서 실제로 이벤트를 발생시키지 않아도 dispatchEvent 메소드를 사용하여 강제로 이벤트를 발생시킬 수 있습니다.
    • 인수로는 이벤트 인스턴스를 넘겨줍니다. 예 : new Event('click')

커스텀 이벤트

  • javascript에는 존재하지 않는 이벤트라도 dispatchEvent 메소드를 사용하면 이벤트를 강제로 발생시킬 수 있습니다.

## 예시 코드

### 디스패치 예시

<body>
  <div class="parent">
    <div class="child">1</div>
    <div class="child">2</div>
  </div>
</body>
// 디스패치

const child1 = document.querySelector('.child:nth-child(1)');
const child2 = document.querySelector('.child:nth-child(2)');

child1.addEventListener('click', event => {
  // 강제로 이벤트 발생!
  child2.dispatchEvent(new Event('click'));
  child2.dispatchEvent(new Event('wheel'));
  child2.dispatchEvent(new Event('keydown'));
});
child2.addEventListener('click', event => {
  console.log('Child2 Click!');
});
child2.addEventListener('wheel', event => {
  console.log('Child2 Wheel!');
});
child2.addEventListener('keydown', event => {
  console.log('Child2 Keydown!');
});

결과

 

1을 클릭하게 되면, 2의 세가지 이벤트가 발생하게 됩니다.

### 커스텀 이벤트 예시

// 커스텀 이벤트

const child1 = document.querySelector('.child:nth-child(1)');
const child2 = document.querySelector('.child:nth-child(2)');

// addEventListener에 javascript에는 존재하지 않는 'hello-world' 라는 커스텀 이벤트를 연결했습니다.
// 당연하게도 hello-world 이벤트는 어떠한 경우에도 발생하지 않습니다.
child1.addEventListener('hello-world', event => {
  console.log('커스텀 이벤트 발생!');
  console.log(event.detail);
});

// 하지만, dispatchEvent를 이용하여 이벤트를 강제로 발생시키면
// 임의의 이름으로 만든 이벤트를 동작시킬 수 있습니다.
child2.addEventListener('click', () => {
  child1.dispatchEvent(new Event('hello-world'));
});

결과

결국&nbsp; 위 코드대로 child2를 클릭하게 되면, child1의 'hello-world'라는 이벤트가 발생합니다.

 

참고로 event 객체에는 detail이라는 속성이 없으므로 undefined가 출력됩니다.

 

### 커스텀 이벤트 예시2

Event 생성자 함수 뿐만 아니라 CustomEvent 생성자 함수라는 것도 존재합니다.

// 커스텀 이벤트

const child1 = document.querySelector('.child:nth-child(1)');
const child2 = document.querySelector('.child:nth-child(2)');

child1.addEventListener('hello-world', event => {
  console.log('커스텀 이벤트 발생!');
  console.log(event.detail);
});

// Event 대신 CustomEvent 생성자 함수를 사용하고, 두번째 인수로 객체를 넘겨줍니다.
// 
child2.addEventListener('click', () => {
  child1.dispatchEvent(new CustomEvent('hello-world'), {detail: 123});
});

결과

CustomEvent 생성자 함수에 두번째 인수로 detail 속성을 가진 객체 데이터를 넘겨주면, event 객체에서 detail 속성에 내가 만든 데이터를 확인할 수 있습니다.

javascript에 존재하지 않는 custom한 이벤트를 만들 수도 있고 실행할 수도 있는데, 만약 실행 할 때 어떤 데이터를 같이 전달하고 싶으면 두번째 인수로 detail 속성에 데이터를 추가하고 객체로 넘겨주면 됩니다.

detail 속성을 이용하여 데이터를 넘겨주고 싶다면 Event 생성자 함수가 아닌 CustomEvent 생성자 함수를 사용해야 합니다.

'개인노트-개인공부' 카테고리의 다른 글

Node vs Element  (2) 2024.01.28
javascript - 클로저  (1) 2024.01.21
TailWindCSS  (2) 2024.01.02
Vite란?  (1) 2023.12.25
Zustand  (2) 2023.12.17