딥다이브 스터디 40 (이벤트)

김영현·2023년 12월 17일
1

이벤트

브라우저가 처리해야할 특정 사건. ex)마우스 이동, 키보드 입력, 클릭...등. 이를 감지하여 특정한 타입의 이벤트를 발생시킴.
이벤트에 반응하여 어떤 일을 하고싶다면 함수를 브라우저에게 알려 호출을 위임함.
이 함수를 이벤트 핸들러라고 하고 호출을 위임하는 것을 이벤트 핸들러 등록이라 한다.
=> 이벤트 핸들러는 콜백함수다. 즉, 개발자가 명시적으로 호출하는게 아니라 브라우저에게 위임한다.

이와같이 프로그램의 흐름을 이벤트 중심으로 제어하는 방식을 Event-driven programming이라 함.


이벤트 타입

사건은 종류가 있다. 이벤트도 마찬가지다. 약 200여가지의 타입이 있다. 모두 소개하기엔 너무 많으니, 몰랐거나 비슷하지만 다른점들만 짚어보았다.

마우스

  • dbclick : 더블클릭 했을 때
  • mousedown : 버튼을 눌렀을 때(mouseclick은 down-up의 과정을 전부 거쳐야 발생한다)
  • mouseup : 누르고 있던 버튼을 놓았을 때
  • mouseenter vs mouseover : 둘 다 커서를 HTML요소 안으로 이동했을 때. 전자는 버블링x
  • mouseleave vs mouseout : 둘 다 커서를 HTML 요소 밖으로 이동했을 때. 전자는 버블링x

버블링이 필요하다면 mouseover & mouseout 아니라면 반대

키보드 이벤트

  • kewdown : control, option, shift, tab, delete, enter, 방향키, 문자, 숫자 특수문자 눌렀을때 발생.
    단, 문자, 숫자, 특수문자, etner키를 눌렀을땐 연속적으로 이벤트가 발생함.

포커스 이벤트

  • focus vs focusin : HTML 요소가 포커스를 받았을 때. 전자는 버블링 x
  • blur vs focusout : HTML 요소가 포커스를 잃었을 때. 전자는 버블링 x

폼 이벤트

  • sumbit : form요소 내의 input(text, checkbox, radio), select 입력필드(textarea제외)에서 엔터키를 눌렀을때.
    form 요소 내의 submit버튼 (button, 타입이 submit인 input)를 클릭했을 때.
    => form요소 내의 button의 기본타입은 submit임.
  • reset : form요소 내의 reset버튼을 클릭했을때. 최근엔 사용하지 않는다.

값 변경 이벤트

  • input vs change : input(text, checkbox, radio), select, textarea 요소의 값이 입력되었을때. change는 HTML요소가 포커스를 잃어 입력이 종료되어야 발생한다.
  • readystatechange : HTML 문서 로드와 파싱 상태를 나타내는 document.readyState 프로퍼티 값(loading, interactive, complete)이 변경될때.
    => loaidng : 파싱 중, interactive : 파싱은 완료되었지만 async script, img, css등이 로딩 안된 상태 === DOMContentLoaded. complete : 모든게 완료.

DOM 뮤테이션 이벤트

  • DOMContentLoaded : HTML 파싱완료되어 DOM생성 완료되었을때.

뷰 이벤트

  • resize : 브라우저 윈도우 크기를 리사이즈할때 연속적으로 발생(window객체에서만)
  • scroll : 스크롤

리소스 이벤트

  • load : DOMContentLoad이벤트 발생 이후 모든 리소스의 로딩이 완료되었을때
  • unload : 리소스가 언로드될때 (주로 새로운 페이지 요청시)
  • abort : 리소스 로딩이 중단되었을때 (실패한건 아님). 미디어 리소스쪽에서 쓰인다고 한다.
  • error : 리소스 로딩실패

이벤트 핸들러 등록

브라우저에게 핸들러 호출 위임 = 등록
3가지 방식이있다.

어트리뷰트

<button onclick='console.log('hi');'>Hi</button>

자세히보면 호출하여 할당했다. 사실 이벤트 핸들러 어트리뷰트 값은 암묵적으로 생성 될 이벤트 핸들러의 함수 몸체를 의미함.
위의 코드는 사실...

function onclick(event){
	console.log("hi");
}

위와 같단 것.
그리고 이렇게 만들어진 함수 몸체를 onclick프로퍼티 에 다시 할당한다. => DOM 노드객체의 프로퍼티로 변환됨.

오래된 코드에서 이 방식을 사용하기에 알아둘 필요는 있지만, 권장x. HTML과 JS는 관심사가 다르므로 분리하는 것이 좋다(마크업과 로직).
하지만 React,Vue,Svelt 같은 프레임워크에서는 어트리뷰트 방식으로 처리한다.
=> HTML/CSS/JS를 뷰를 위한 구성요소로 보기에 관심사가 같다고 생각한다. (CSS in JS도 이 맥락에서 나왔다. 요즘은 CSS를 다시 분리하는게 추세지만...)

프로퍼티 방식

const $button = document.querySelector('button');
$button.onclick = function(){
	console.log('hi');
}

이벤트 핸들러 프로퍼티에 한달한다. 사실 어트리뷰트 등록방식과 똑같다.
다른점은 관심사의 분리와 타깃 당 하나의 이벤트 핸들러만 바인딩 가능하다는 것.
어트리뷰트 방식은 하나의 이벤트에 여러개의 핸들러 바인딩이 가능하다.

<button onclick='console.log('hi1'); console.log('hi2'); console.log('hi3');'>Hi</button>

addEventListener 방식

EventTarget.addEventListener('eventType', eventHandler, [useCapture?(boolean)]);

세번째 매개변수에 true를 전달하면 캡처링이 이루어지고 전달하지 않으면 기본적으로 버블링이 일어난다.

동일한 이벤트 타입에 여러번 핸들러를 바인딩할 수도 있다. 등록된 순서로 호출됨.

target.addEventListener('click', () => {conosle.log('hi1')})
target.addEventListener('click', () => {conosle.log('hi2')})
target.addEventListener('click', () => {conosle.log('hi3')})

참조가 동일한 핸들러를 중복등록하면 하나만 등록된다.

const handleClick = () => console.log('hi')
target.addEventListener('click', () => handleClick)
target.addEventListener('click', () => handleClick)

removeEventListener

addEventListener방식으로 등록한 이벤트 핸들러를 제거할 수 있다.

target.addEventListener('click', () => handleClick);
target.removeEventListener('click', () => handleClick);

참조가 동일해야함. 무명함수는 당연히 참조가 달라서 불가능하다.

여기서 바보같이 생각하고 있었단걸 깨달음.

const arrow = () => {...}

번외)
이렇게 되어있는 함수는 () => {...}가 함수 몸체인거고, const arrow = 는 그냥 arrow에 할당한다는 뜻이었다.
지금까지 막연히 const arrow = () => {...}할당문 전체가 함수라고 생각하고있었다....ㅎㅎ

프로퍼티 방식으로 할당한 핸들러는 그냥 null 할당으로 명시적으로 제거해주면된다.

$button.onclick = function(){
	console.log('hi');
}
$button.onclick = null;

이벤트 객체

이벤트가 발생하면 이벤트 관련 정보를 담고있는 이벤트 객체가 동적생성됨.
이 객체는 이벤트 핸들러의 첫번째 인수로 전달됨. 우리가 흔히 핸들러 매개변수에서 사용하는 e, event 등으로 받아오던 것.
중요한건 첫번째 인수다. 헷갈리지말자~

$button.onclick = function(e){
	console.log(e.target);
}

어트리뷰트 방식을 사용할땐 반드시 event라고 매개변수 이름을 적어주어야한다.

function onclick(event){
	console.log(event);
}

이처럼 event매개변수를 가진 함수가 암묵적으로 생성되기 때문이다.

상속 구조

이벤트 객체역시 객체다. 따라서 상속(위임)구조를 갖는다.

위의 Event, UIEvent, MOuseEvent등 모두 생성자 함수다.
=> 이벤트가 발생하면 암묵적으로 생성되는 이벤트 객체또한 생성자 함수에 의해 생성된다.(프로토타입 체인의 일원)

공통 프로퍼티

프로토타입 체인의 일원이 된다면 Event객체의 공통 프로퍼티 또한 사용할 수 있다.

  • type : 이벤트타입 (string)
  • target : 이벤트를 발생시킨 DOM 요소 (DOM 요소 노드)
  • currentTarget : 이벤트 핸들러가 바인딩된 DOM 요소 (DOM 요소 노드)
  • eventPhase : 이벤트 전파단계. 0: 없음, 1: 캡처링, 2: 타깃, 3: 버블링
  • bubbles : 이벤트 버블링 여부
  • cancleable : preventDefault()메서드로 기본동작을 취소할 수 있는지 여부.
    focus, blur, load, unload, abort, error, mouseenter, mouseleave, dbclick은 취소할수 없다.
  • defaultPrevented : 기본동작을 취소하였는지 여부
  • isTrusted : 예를들어 click메서드나 dispatchEvent처럼 인위적으로 발생시킨 이벤트의 경우는 false.이외는 true
  • timeStamp : 이벤트가 발생한 시각(1970/01/01/00:00:00 부터 경과한 밀리초)

마우스 정보 취득

MouseEvent타입 이벤트 객체는 다음과 같은 고유 프로퍼티를 갖는다.

  • 포인터의 좌표 정보
    • clientX : 뷰포트 기준(스크롤 무시 상단 0)
    • offsetX : 이벤트 대상 객체 기준
    • pageX : 스크롤 포함 문서기준
    • screenX : 모니터 기준
  • 버튼 정보
    • altKey, ctrlKey, shiftKey, button

알트같은 키보드 이벤트도 같이 들어가는 게 신기하다.

키보드 정보 취득

KeyboardEvent 타입 이벤트 객체는 다음과 같은 코유 프로퍼티를 갖는다.

  • ctrl, alt, meta(window) 키.
  • 위를 제외한 키.

심플하다. 참고로 소개하지 않은 프로퍼티인 keyCode는 폐지되었다.
또한 input요소에 한글을 입력하고 엔터 키를 누르면 keyup이벤트가 두번 호출되는 현상이 발생한다. keydown이벤트로 대체하자.


이벤트 전파

DOM트리상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 DOM트리를 통해 전파된다.
이를 이벤트 전파(event propagation)이라 한다.

<html>
  <body>
    <ul>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </body>
</html>

li를 클릭했다고 가정하면, 클릭이벤트가 발생된다. 이때 생성된 이벤트 객체는 이벤트를 발생시킨 타겟(event.target)을 중심으로 DOM트리를 통해 전파됨. 이를 단계로 나타내면 다음과 같다.

  1. Capturing : li요소를 클릭했을때, 이벤트가 발생하여 클릭 이벤트 객체가 생성되고 window > docuemnt > html > body > ul > li 처럼 위에서 아래로 전파된다.
  2. Target : 이벤트 객체가 li태그에 도달한 것이다. 타겟에 이벤트가 도달.
  3. Bubbling : 이벤트 객체가 타겟에서 시작하여 window까지 전파된다. 이것이 버블링 단계다.

https://domevents.dev/ 여기를 가보면 그림으로 볼수도 있다.ㅎㅎ


onclick을 기준으로 설명한 mdn문서다.
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Building_blocks/Events#%EC%9D%B4%EB%B2%A4%ED%8A%B8_%EB%B2%84%EB%B8%94%EB%A7%81%EA%B3%BC_%EC%BA%A1%EC%B2%98

이벤트 핸들러/어트리뷰트 방식으로 등록한 이벤트 핸들러는 캡처링단계를 캐치할 수 없다.addEventListener는 3번째 매개변수에서 설명함.

이처럼 이벤트는 이벤트를 발생시킨 타깃은 물론 상위 DOM요소에서도 제어할 수 있음.
즉, DOM트리를 통하여 전파되는 이벤트는 currentTarget과 이벤트가 통과하는 DOM트리상의 경로에 위치한 모든 DOM요소에서 캐치할 수 있다.

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`캡쳐링: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`버블링: ${elem.tagName}`));
  }
</script>

https://ko.javascript.info/bubbling-and-capturing

이 코드를 실행해보면 HTML > Body > Form > Div > P > Div...순으로 나온다.


이벤트 위임(event delegation)

버블링개념을 이용하면 보다 쉽게 이벤트 핸들러를 등록, 관리할 수 있다.

    <ul>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
    </ul>
<script>
	const $ul = document.querySelector(ul);
  	ul.addEventListener('click', (e)=>{
  	if(e.target.tagName === 'LI"){
  		...dosomething()  
  }
  })
</script>

모든 li태그에 같은 이벤트 핸들러를 등록한다고 했을 때, li태그의 수가 늘어날수록 핸들러를 계속 등록해야한다.
하지만 이벤트 위임을 사용한다면 ul 하나에 등록하여 조건문으로 버블링 단계중 li태그 일때 실행하도록 사용할 수 있다.


DOM 요소의 기본동작 중단

preventDefault()

DOM 요소는 저마다 기본동작 존재. a태그는 클릭하면 href속성에 지정된 링크로 이동하고 checkbox, radio요소를 클릭하면 체크 || 해제됨. 이를 막는 메서드가 이벤트 객체 메서드 중 하나인 preventDefault().

stopPropagation()

이벤트 전파(캡처링, 버블링)를 중지시킨다.


이벤트 핸들러 내부의 this

이벤트 핸들러는 콜백함수다. 즉, this를 명시적으로 지정해주지 않으면 window를 가리키게된다.

어트리뷰트 방식

암묵적으로 생성되는 이벤트 핸들러의 문이다. 따라서 이벤트 핸들러에 의해 일반함수로 호출되는 어트리뷰트로 전달된 함수는 window를 가리키게된다.단, 호출할때 인수로 전달한 this는 이벤트를 바인딩한 DOM이다.

<button onclick="handleClick(t)"></button>
<script>
	function handleClick(){
		console.log(this) // window  
  	}
</script>

<!-- 아래는 DOM -->

<button onclick="handleClick(this)"></button>
<script>
	function handleClick(params){
		console.log(params) // 이벤트가 바인딩된 button 
  	}
</script>

프로퍼티 방식, addEventListener방식

둘 다 this는 이벤트가 바인딩 된 DOM요소를 가리킴.

const $el = document.querySelector(button);
$el.onclick = function (e) {
	console.log(this) //버튼
}

$el.addEventListener('click', function (e){
	console.log(this) //버튼
})

결국 currentTarget이 곧 this다. 화살표 함수로 전달하면 바로 상위코프의 this를 가리키기에 window를 가리키게된다.

클래스에서 이벤트핸들러를 바인딩하는 경우 this에 유의해야한다. 이벤트핸들러 내부의 this는 이벤트가 바인딩 된DOM 요소를 가리키게된다.

clas App {
    constructor(){
    	this.$button = document.querySelector(button);
      	this.$button.onclick = this.incease; // 이벤트 핸들러로 메서드를 등록.
    }
    increase(){
    	this.$button.textContent = 'hi'   //이렇게 사용하면 오류가난다.

    }
}
// bind로 수정
clas App {
    constructor(){
    	this.$button = document.querySelector(button);
      	this.$button.onclick = this.incease.bind(this); // bind메서드로 this를 명시적으로 전달
    }
    increase(){
    	this.$button.textContent = 'hi'   //오류없음

    }
}
// 클래스필드에 화살표 함수 할당
clas App {
    constructor(){
    	this.$button = document.querySelector(button);
      	this.$button.onclick = this.incease;
    }
    increase() => this.$button.textContent = 'hi'   //상위스코프인 App을 가리킴
}

프로퍼티 방식이지만 addEventListener도 똑같은 방식이니 유의하자


이벤트 핸들러에 인수 전달

함수를 매개변수로 받는 함수에 전달할때, 인수 전달이 어려울 수 있음.
이때 그냥 함수로 한번 감싸준 뒤 호출하면서 인수로 전달해주면 됨.

$input.onblur = myFunc(parameter) //이렇게 호출하면 안된다

//이렇게 해야함
$input.onblur = () => {
	myFunc(parameter)
}

아니면 이벤트 핸들러를 반환하는 함수를 만들면 된다.

const myFunc = (text) = (e) => {
  $msg.textContent = text;
}
//위의 함수는 아래와 같다.
function myFunc(text) {
  return function(e) {
    $msg.textContent = text;
  };
}
$input.onblur = myFunc('hi');

커링 기법이 여기서 등장할줄이야


커스텀 이벤트

historyAPI강의를 배울때 나온 과제에서 요긴하게 써먹었던 커스텀 이벤트다.
이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 타입지정이 가능하다.]

const keyboardEvent = new KeyboardEvent('keyup');
console.log(keyboardEvent.type) // keyup

//커스텀 이벤트 생성
const customEvent = new CustomEvent('foooo');
console.log(customEvent.type);// foooo

커스텀 객체는 버블링되지 않으며 prevetDefault로 취소가 불가능하다. 만약 가능하게 하고 싶다면

const customEvent = new CustomEvent('foooo',{
	bubbles: true,
  	cancleable: true,
});

요래 두번째 인자로 전달해준다.

dispatch

생성한 커스텀 이벤트를 일으키는메서드다. 이는 동기적으로 실행된다.
=> 커스텀이벤트를 처리할 이벤트 핸들러를 먼저 등록해놓아야함.

$button.addEventListener('foooo', (e) => {
	...doSomething()
})

const customEvent = new CustomEvent('foooo');

$button.dispatchEvent(customEvent, {
	detail: {...이벤트와 함께 전달하고싶은 정보}
})

이벤트에 정보를 담아 보내야 한다면 두번째 인자로 정보를 담은 detail프로퍼티를 포함하는 객체를 전달하면 된다.

참고로 커스텀 이벤트는 addEventListener로만 등록할 수 있다. 프로퍼티 방식의 이벤트는 모두 on으로 시작하는 이벤트기 때문이다.
onclick, onfocus, onblur....

profile
모르는 것을 모른다고 하기

0개의 댓글