티스토리 뷰
이벤트
브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트를 발생시킨다.
이벤트 핸들러 : 이벤트가 발생했을 때 호출될 함수
이벤트 핸들러 등록 : 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것
이벤트 드리븐 프로그래밍 : 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식
이벤트 타입 : 이벤트의 종류를 나타내는 문자열
마우스 이벤트
click, dbclick, mousedown(눌렀을때), mouseup(땟을때), mousemove,
mouseenter(마우스 커서를 HTML 요소 안으로 이동했을때(버블링 x))
mouseover(마우스 커서를 HTML 요소 안으로 이동했을때(버블링 o)
mouseleave(마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링 x))
mouseout(마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링 o))
키보드 이벤트
keydown, keypress(눌렀을 때 연속적으로 발생), keyup
포커스 이벤트
focus(HTML 요소가 포커스를 받았을 때(버블링x))
blur(HTML 요소가 포커스를 잃었을 대(버블링x))
focusin(HTML 요소가 포커스를 받았을 때(버블링 o))
focusout(HTML 요소가 포커스를 잃었을 때(버블링o))
폼 이벤트
submit, reset
값 변경 이벤트
input(input, select, textarea 요소의 값이 입력되었을 때)
change(~~값이 변경되었을 때, 입력할때는 input이벤트, 포커스를 잃을때 change 이벤트 react랑 달라)
readystatechange(HTML 문서의 로드와 파싱 상태를 나타내는 readyState 프로퍼티 값('loading', interactive', complete')이 변경될때
DOM 뮤테이션 이벤트
DOMContentLoaded(HTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때)
뷰 이벤트
resize(브라우저 윈도우 크기를 리사이즈 할때 연속으로 발생), scroll
리소스 이벤트
load(DOMContentLoaded가 발생한 후, 모든 리소스의 로딩이 완료되었을 때), unload(주로 새로운 웹페이지 요청할때), abort(리소스 로딩이 중단되었을때), error(리소스 로딩 실패)
이벤트 핸들러 등록
1. 이벤트 핸들러 어트리뷰트 방식
<button onclick="sayHi('Lee')">Click me!</button>
<!-- Angular -->
<button (click)="handleClick($event)">Save</button>
{ /* React */ }
<button onClick={handleClick}>Save</button>
<!-- Svelte -->
<button on:click={handleClick}>Save</button>
<!-- Vue.js -->
<button v-on:click="handleClick($event)">Save</button>
2. 이벤트 핸들러 프로퍼티 방식(재할당하면 마지막 할당한 이벤트가 실행)
$button.onclick = function () { console.log('button click'); };
3. addEventListener(여러 이벤트를 등록할 수 있다.)
EventTarget.prototype.addEventListener('eventType', functionName [, useCapture])
첫번째 매개변수에는 이벤트 타입(on제외)
두번째 매개변수에는 이벤트 핸들러
마지막 매개변수에는 이벤트를 캐치할 전파 단계를 지정.
생략하거나 false를 지정하면 버블링 단계에서 이벤트를 캐치
true를 지정하면 캡처링 단계에서 이벤트를 캐치
$button.addEventListener('click', function () { console.log('button click'); });
이벤트 핸들러 제거
addEventListener로 만들었다면 removeEventListner로 제거
onClick으로 만들었다면 onClick=null로 제거
이벤트 객체
생성된 이벤트 객체는 이벤트 핸들러의 첫번째 인수로 전달
이벤트 핸들러 어트리뷰트의 방식에서는 매개변수를 event라고 명시해야 한다.
이벤트 객체의 공통 프로퍼티
type, target, currentTarget, eventPhase(이벤트 전파단계, 0:이벤트없음, 1:캡처링, 2:타깃, 3:버블링),
bubbles(이벤트를 버블링으로 전파하는지 여부),
cancelable(preventDefault 메서드를 호출하여 이벤트의 기본 동작을 취소할 수 있는지 여부),
defaultPrevented(preventDefault 메서드를 호출하여 이벤트를 취소했는지 여부),
isTrusted(사용자의 행위에 의해 발생한 이벤트인지 여부, 인위라면 false),
timeStamp(이벤트가 발생한 시각(1970~~)부터 경과한 밀리초)
마우스 정보
마우스 포인터의 좌표 정보 : screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY
버튼 정보 : altKey, ctrlKey, shiftKey, button
screen : 모니터기준, client : 웹 페이지 기준, offset : 이벤트 대상 기준, page : 전체 화면기준(스크롤 포함)
도형 옮기기 예제
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: #fff700;
border: 5px solid orange;
cursor: pointer;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
// 드래그 대상 요소
const $box = document.querySelector('.box');
// 드래그 시작 시점의 마우스 포인터 위치
const initialMousePos = { x: 0, y: 0 };
// 오프셋: 이동할 거리
const offset = { x: 0, y: 0 };
// mousemove 이벤트 핸들러
const move = (e) => {
// 오프셋 = 현재(드래그하고 있는 시점) 마우스 포인터 좌표 - 드래그 시작 시점의 마우스 포인터 좌표
offset.x = e.clientX - initialMousePos.x;
offset.y = e.clientY - initialMousePos.y;
// translate3d는 GPU를 사용하므로 absolute의 top, left를 사용하는 것보다 빠르다.
// top, left는 레이아웃에 영향을 준다.
$box.style.transform = `translate3d(${offset.x}px, ${offset.y}px, 0)`;
};
// mousedown 이벤트가 발생하면 드래그 시작 시점의 마우스 포인터 좌표를 저장한다.
$box.addEventListener('mousedown', (e) => {
// 이동 거리를 계산하기 위해 mousedown 이벤트가 발생(드래그를 시작)하면
// 드래그 시작 시점의 마우스 포인터 좌표(e.clientX/e.clientY: 뷰포트 상에서 현재
// 마우스의 포인터 좌표)를 저장해 둔다. 한번 이상 드래그로 이동한 경우 move에서
// translate3d(${offset.x}px, ${offset.y}px, 0)으로 이동한 상태이므로
// offset.x와 offset.y를 빼주어야 한다.
initialMousePos.x = e.clientX - offset.x;
initialMousePos.y = e.clientY - offset.y;
// mousedown 이벤트가 발생한 상태에서 mousemove 이벤트가 발생하면
// box 요소를 이동시킨다.
document.addEventListener('mousemove', move);
});
// mouseup 이벤트가 발생하면 mousemove 이벤트를 제거해 이동을 멈춘다.
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', move);
});
</script>
</body>
</html>
키보드 정보
altKey, ctrlKey, shiftKey, metaKey, key, keyCode
이벤트 전파
DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다. 이를 이벤트 전파라고 함
요소를 클릭하면 클릭 이벤트가 발생하고 이때 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.
캡처링 단계 : 이벤트가 상위 요소에서 하위 요소 방향으로 전파
타깃 단계 : 이벤트가 이벤트 타깃에 도달
버블링 단계 : 이벤트가 하위 요소에서 상위 요소 방향으로 전파
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
const $banana = document.getElementById('banana');
// #fruits 요소의 하위 요소인 li 요소를 클릭한 경우
// 캡처링 단계의 이벤트를 캐치한다.
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 1: 캡처링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
}, true);
// 타깃 단계의 이벤트를 캐치한다.
$banana.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 2: 타깃 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
});
// 버블링 단계의 이벤트를 캐치한다.
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
});
</script>
</body>
</html>
이벤트 위임
이벤트 위임을 통해 상위 요소에 이벤트 핸들러를 등록
e.preventDefault() : DOM 요소의 기본 동작을 중단
e.stopPropagation : 이벤트 전파를 중지
이벤트 핸들러 내부의 this
이벤트 핸들러 어트리뷰트는 window
이벤트 핸들러 프로퍼티와 addEventListener는 이벤트를 바인딩한 DOM 요소
잘못된 예시
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// increase 메서드를 이벤트 핸들러로 등록
this.$button.onclick = this.increase;
}
increase() {
// 이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다.
// 따라서 this.$button은 this.$button.$button과 같다.
this.$button.textContent = ++this.count;
// -> TypeError: Cannot set property 'textContent' of undefined
}
}
new App();
bind 사용
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// increase 메서드를 이벤트 핸들러로 등록
// this.$button.onclick = this.increase;
// increase 메서드 내부의 this가 인스턴스를 가리키도록 한다.
this.$button.onclick = this.increase.bind(this);
}
increase() {
this.$button.textContent = ++this.count;
}
}
new App();
화살표 함수와 인스턴스 메서드 사용
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// 화살표 함수인 increase를 이벤트 핸들러로 등록
this.$button.onclick = this.increase;
}
// 클래스 필드 정의
// increase는 인스턴스 메서드이며 내부의 this는 인스턴스를 가리킨다.
increase = () => this.$button.textContent = ++this.count;
}
new App();
이벤트 핸들러에 인수 전달
//1
const checkUserNameLength = min => {
$msg.textContent
= $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : '';
};
// 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
$input.onblur = () => {
checkUserNameLength(MIN_USER_NAME_LENGTH);
};
//2
// 이벤트 핸들러를 반환하는 함수
const checkUserNameLength = min => e => {
$msg.textContent
= $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : '';
};
// 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달한다.
$input.onblur = checkUserNameLength(MIN_USER_NAME_LENGTH);
커스텀 이벤트
타이머
자바스크립트는 타이머를 생성할 수 있는 타이머 함수 setTimeout, setInterval, 타이머를 제거할 수 있는 clearTimeout과 clearInterval을 제공한다.
자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖기 때문에 두 가지 이상의 테스크를 동시에 실행할 수 없다. 즉, 자바스크립트 엔진은 싱글 스레드로 동작한다. 이런 이유로 타이머 함수는 비동기 처리 방식으로 동작한다.
setTimeout(func|code[, delay, param1, param2, ...]); , setInterval(~~)
func : 타이머가 만료된 뒤 호출될 콜백함수
delay : 타이머 만료시간(밀리초) 1000이 1초(4ms 이하인 경우에는 최소 지연 시간 4ms가 지정된다.)
param : 콜백 함수에 인수가 존재하면 세번째 이후의 인수로 전달
(콜백 함수는 정확히 지연 시간 후에 호출된다는 보장은 없다. 지연 시간 이후에 콜백 함수가 테스크 큐에 푸시되어 대기하지 되지만 콜 스택이 비어야 호출되므로 약간의 시간차가 발생할 수 있기 때문이다.)
-- 다음 글에서 테스크 큐, 콜스택 확인
clearTimeout, clearInterval(콜백함수) : 콜백함수의 타이머 취소
디바운스와 스로틀
<!DOCTYPE html>
<html>
<body>
<button>click me</button>
<pre>일반 클릭 이벤트 카운터 <span class="normal-msg">0</span></pre>
<pre>디바운스 클릭 이벤트 카운터 <span class="debounce-msg">0</span></pre>
<pre>스로틀 클릭 이벤트 카운터 <span class="throttle-msg">0</span></pre>
<script>
const $button = document.querySelector('button');
const $normalMsg = document.querySelector('.normal-msg');
const $debounceMsg = document.querySelector('.debounce-msg');
const $throttleMsg = document.querySelector('.throttle-msg');
const debounce = (callback, delay) => {
let timerId;
return (event) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(callback, delay, event);
};
};
const throttle = (callback, delay) => {
let timerId;
return (event) => {
if (timerId) return;
timerId = setTimeout(
() => {
callback(event);
timerId = null;
},
delay,
event
);
};
};
$button.addEventListener('click', () => {
$normalMsg.textContent = +$normalMsg.textContent + 1;
});
$button.addEventListener(
'click',
debounce(() => {
$debounceMsg.textContent = +$debounceMsg.textContent + 1;
}, 500)
);
$button.addEventListener(
'click',
throttle(() => {
$throttleMsg.textContent = +$throttleMsg.textContent + 1;
}, 500)
);
</script>
</body>
</html>
디바운스 : 짧은 시간 간격으로 이벤트가 연속해서 발생하면 이벤트 핸들러를 호출하지 않다가 일정 시간이 경과간 이후에 이벤트 핸들러가 한 번만 호출되도록 한다. 즉, 디바운스는 짧은 시간 간격으로 발생하는 이벤트를 그룹화해서 마지막에 한 번만 이벤트 핸들러가 호출되도록 한다.(리덕스 미들웨어에도 이 기능 존재)
스로틀 : 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한번만 호출되도록 한다. 즉 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 일정시간 단위로 이벤트 핸들러가 호출되도록 호출 주기를 만든다.
( 실무에서는 Underscore의 throttle 함수나 Lodash의 throttle 함수를 권장)
'책 > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
모던자바스크립트 12(45장~46장) (프로미스, 제너레이터와 async/await) (0) | 2021.05.23 |
---|---|
모던자바스크립트 11(42장~44장) (비동기, Ajax, REST API) (0) | 2021.05.21 |
모던자바스크립트 10(38장~39장) (브라우저의 렌더링, DOM) (0) | 2021.05.18 |
모던자바스크립트 9(34장~37장) (0) | 2021.05.15 |
모던자바스크립트 8(28장~33장) (0) | 2021.05.11 |