
이벤트 핸들러 · 이벤트 객체 · 전파 · 위임 · 폼 이벤트
브라우저는 클릭, 키보드 입력, 마우스 이동 같은 동작을 감지해서 이벤트 객체를 만들고, 등록해 둔 이벤트 핸들러(콜백 함수) 를 호출한다.
이 핸들러를 브라우저에 넘겨두는 과정을 이벤트 핸들러 등록이라고 부른다.
HTML 태그 안에 직접 onXXX 속성으로 작성하는 방식.
<button onclick="alert('클릭했네?'); console.log('클릭했네?');">
클릭해보세요
</button>
<button onmouseover="hello('수강생');">마우스를 올려보세요</button>
<script>
function hello(name) {
alert(`${name}씨, 마우스 올리지마세요!`);
}
</script>
특징
onclick="함수호출문" 처럼 함수 호출문을 문자열로 넣는다DOM 객체의 onXXX 프로퍼티에 함수를 직접 할당.
<button id="btn">클릭해보세요</button>
<script>
const $button = document.querySelector('#btn');
$button.onclick = function () {
alert('DOM 프로퍼티 방식으로 이벤트 핸들러 등록 완료!');
};
// 이전 핸들러는 덮어써진다
$button.onclick = () => alert('이벤트 덮어쓰기!');
</script>
특징
가장 많이 쓰는 방식.
<button id="btn">클릭해보세요</button>
<script>
const $button = document.querySelector('#btn');
$button.addEventListener('click', function () {
alert('클릭했네?');
});
// 프로퍼티 방식과 함께 사용 가능
$button.onclick = function () {
console.log('프로퍼티 방식 핸들러');
};
// 같은 이벤트에 여러 개 핸들러 등록 가능
$button.addEventListener('click', function () {
console.log('addEventListener 핸들러 1');
});
$button.addEventListener('click', function () {
console.log('addEventListener 핸들러 2');
});
// 동일한 함수 참조는 중복 등록되지 않는다
const handleMouseover = () => console.log('button mouseover');
$button.addEventListener('mouseover', handleMouseover);
$button.addEventListener('mouseover', handleMouseover);
</script>
형식
element.addEventListener('이벤트타입', 핸들러함수, useCapture옵션);
true 면 캡처링 단계에서, 기본값 false 면 버블링 단계에서 동작 (전파에서 다시)addEventListener 로 등록한 핸들러만 removeEventListener 로 제거 가능.
<button id="btn">클릭해보세요</button>
<script>
const $button = document.querySelector('#btn');
const handleClick = () => alert('클릭했대요~');
$button.addEventListener('click', handleClick);
$button.removeEventListener('click', handleClick); // 제거
// 이런 식의 익명 함수는 나중에 제거 불가능
$button.addEventListener('mouseover', () => alert('마우스 올렸대요~'));
</script>
조건
addEventListener 때와 완전히 같아야 제거된다이벤트가 발생하면 브라우저가 이벤트 객체를 만들어서
이벤트 핸들러의 첫 번째 인수로 넘긴다.
<h1 class="message">아무곳이나 클릭해보세요. 클릭 좌표를 알려준다.</h1>
<script>
const $msg = document.querySelector('.message');
function showCoords(e) {
console.log(e); // 이벤트 객체
$msg.textContent = `clientX : ${e.clientX}, clientY : ${e.clientY}`;
}
document.onclick = showCoords;
</script>
이벤트 어트리뷰트 방식을 쓴다면 매개변수 이름은 반드시 event 를 써야 한다.
<div class="area" onclick="showDivCoords(event)">
안쪽을 클릭해보세요. 좌표를 알려준다.
</div>
<script>
const $area = document.querySelector('.area');
function showDivCoords(e) {
console.log(e);
$area.textContent = `clientX : ${e.clientX}, clientY : ${e.clientY}`;
}
</script>
<button onclick="handleClick1()">클릭1</button>
<button onclick="handleClick2(this)">클릭2</button>
<script>
function handleClick1() {
console.log(this); // 전역 객체(window)
}
function handleClick2(button) {
console.log(button); // 클릭한 버튼 요소
}
</script>
this 는 일반 함수 규칙대로 전역 객체가 된다this 를 인수로 넘겨서 요소를 직접 받을 수 있다<button id="btn1">버튼1</button>
<button id="btn2">버튼2</button>
<script>
const $btn1 = document.querySelector('#btn1');
const $btn2 = document.querySelector('#btn2');
$btn1.onclick = function (e) {
console.log(this); // 이벤트를 바인딩한 요소
console.log(e.currentTarget); // 동일
console.log(this === e.currentTarget); // true
};
$btn2.addEventListener('click', function (e) {
console.log(this); // 이벤트를 바인딩한 요소
console.log(e.currentTarget); // 동일
console.log(this === e.currentTarget); // true
});
</script>
핵심
this === e.currentTarget === 이벤트를 바인딩한 요소<button id="btn3">버튼3</button>
<button id="btn4">버튼4</button>
<script>
const $btn3 = document.querySelector('#btn3');
const $btn4 = document.querySelector('#btn4');
$btn3.onclick = e => {
console.log(this); // 상위 스코프의 this (window 등)
console.log(e.currentTarget); // 버튼 요소
};
$btn4.addEventListener('click', e => {
console.log(this); // 상위 스코프의 this
console.log(e.currentTarget); // 버튼 요소
});
</script>
this 를 쓰고 싶다면 일반 함수를 쓰는 게 안전하다이벤트가 발생하면 이벤트 객체는 DOM 트리를 따라 이동한다.
<ul id="drink">
<li>커피</li>
<li>콜라</li>
<li>우유</li>
</ul>
<script>
const $drink = document.querySelector('#drink');
$drink.addEventListener('click', e => {
console.log(e.eventPhase); // 2 또는 3
console.log(e.target); // 실제 클릭한 요소(li 또는 ul)
console.log(e.currentTarget); // 항상 ul
});
</script>
addEventListener 는 세 번째 인수를 활용하면 캡처링 단계도 잡을 수 있다<ul id="food">
<li>김치찌개</li>
<li>된장찌개</li>
<li>고등어구이</li>
</ul>
<script>
const $food = document.querySelector('#food');
// 캡처링 단계
$food.addEventListener(
'click',
e => {
console.log('캡처링', e.eventPhase, e.target, e.currentTarget);
},
true
);
// 버블링 단계
$food.addEventListener('click', e => {
console.log('버블링', e.eventPhase, e.target, e.currentTarget);
});
</script>
비슷한 역할의 여러 요소에 각각 핸들러를 다는 대신,
상위 공통 요소에 단 한 번만 핸들러를 등록하는 패턴.
<ul id="drink">
<li>커피</li>
<li>콜라</li>
<li>우유</li>
</ul>
const $drink = document.querySelector('#drink');
$drink.addEventListener('click', e => {
// ul 자체 클릭은 무시하고, li 에서만 반응
if (e.target.matches('li')) highlight(e.target);
});
function highlight(li) {
li.classList.toggle('highlight');
}
장점
링크 이동, 체크박스 체크 같은 브라우저 기본 동작을 막고 싶을 때 사용.
<a href="https://www.google.com">클릭해도 이동 안 되는 링크</a>
<input type="checkbox"> 클릭해도 체크 안 되는 체크박스
<script>
document.querySelector('a').addEventListener('click', e => {
e.preventDefault();
});
document
.querySelector('input[type=checkbox]')
.addEventListener('click', e => {
e.preventDefault();
});
</script>
이벤트가 상위/하위로 전파되는 것을 막는다.
<ul id="drink">
<li>커피</li>
<li>콜라</li>
<li>우유</li>
</ul>
<script>
const $drink = document.querySelector('#drink');
// 이벤트 위임
$drink.addEventListener('click', e => {
if (e.target.matches('li')) e.target.style.color = 'red';
});
// 첫 번째 li 에 별도 핸들러
document.querySelector('li').addEventListener('click', e => {
e.stopPropagation(); // ul 로 전파되지 않음
e.target.style.color = 'blue';
});
</script>
주요 타입
mousedown / mouseupmouseover / mouseoutmousemoveclick / dblclickcontextmenu (우클릭 메뉴)<button>클릭해보세요</button>
<script>
const $btn = document.querySelector('button');
// e.button: 0=왼쪽, 1=가운데, 2=오른쪽
$btn.addEventListener('mousedown', e =>
console.log(`mousedown button=${e.button}`)
);
$btn.addEventListener('mouseup', e =>
console.log(`mouseup button=${e.button}`)
);
$btn.addEventListener('mouseover', e =>
console.log(`mouseover button=${e.button}`)
);
$btn.addEventListener('mouseout', e =>
console.log(`mouseout button=${e.button}`)
);
$btn.addEventListener('click', e =>
console.log(`click button=${e.button}`)
);
$btn.addEventListener('dblclick', e =>
console.log(`dblclick button=${e.button}`)
);
$btn.addEventListener('contextmenu', e => {
e.preventDefault(); // 우클릭 메뉴 막기
console.log(`contextmenu button=${e.button}`);
});
</script>
주요 타입
keydown: 키를 눌렀을 때keyup: 키에서 손을 뗐을 때주요 프로퍼티
event.key: 실제 문자 값 (예: "a", "A", "Enter")event.code: 키 위치 코드 (예: "KeyA", "Enter")<input type="text" placeholder="아무 키나 입력">
<script>
const $input = document.querySelector('input[type="text"]');
$input.addEventListener('keydown', e => {
console.log(`keydown key=${e.key}, code=${e.code}`);
});
$input.addEventListener('keyup', e => {
console.log(`keyup key=${e.key}, code=${e.code}`);
});
</script>
<form name="memberjoin">
<label for="username">이름</label>
<input type="text" name="username" id="username" value="홍길동">
<br><br>
<label>성별</label>
<input type="radio" name="gender" id="male" value="m" checked>
<label for="male">남자</label>
<input type="radio" name="gender" id="female" value="f">
<label for="female">여자</label>
<br><br>
<label>나이</label>
<select id="age" name="age">
<option value="10">10대 이하</option>
<option value="20">20대</option>
<option value="30">30대</option>
<option value="40">40대</option>
<option value="50">50대</option>
<option value="60">60대 이상</option>
</select>
<br><br>
<label>자기소개</label>
<textarea
name="introduce"
id="introduce"
rows="5"
cols="30"
style="resize: none"
>저를 소개합니다!</textarea>
<span>0</span>/5000자
<br><br>
<input type="submit" value="제출">
</form>
<script>
console.log(document.forms); // 모든 form
const $form = document.forms.memberjoin;
console.log($form.elements); // 폼 필드들
const $username = $form.username;
console.log($username.value);
$username.value = '유관순';
const $gender = $form.gender;
console.log($gender[1].checked);
$gender[1].checked = true;
const $age = $form.age;
console.log($age.options);
$age.options[2].selected = true;
$age.selectedIndex = 3;
$age.value = '50';
const $introduce = $form.introduce;
console.log($introduce.value);
$introduce.value = 'value';
</script>
focus: 요소에 포커스가 들어올 때 (버블링 X)blur: 요소에서 포커스가 빠져나갈 때 (버블링 X)focusin, focusout: 포커스 이벤트가 버블링되는 버전const $username = document.forms.memberjoin.username;
$username.onfocus = e => {
e.target.classList.toggle('lightgray');
};
$username.onblur = e => {
e.target.classList.toggle('lightgray');
};
// 폼 전체에 포커스 스타일 주고 싶을 때
const $form = document.forms.memberjoin;
$form.addEventListener('focusin', e => e.target.classList.add('focused'));
$form.addEventListener('focusout', e => e.target.classList.remove('focused'));
change
input
const $form = document.forms.memberjoin;
const $age = $form.age;
const $introduce = $form.introduce;
$age.addEventListener('change', () => alert('age change!'));
$introduce.addEventListener('change', () => alert('introduce change!'));
$introduce.addEventListener('input', e => {
const len = e.target.value.trim().length;
$form.querySelector('span').textContent = len;
});
submit 은 폼이 서버로 전송되기 직전에 발생
이 시점에 유효성 검사 → 통과 못하면 전송 취소 패턴을 많이 쓴다
폼 전송 트리거
<button type="submit"> / <input type="submit"><form
method="GET"
action="https://search.naver.com/search.naver"
name="search"
>
<input type="text" name="query" placeholder="검색할 키워드를 입력하세요">
<button type="submit">네이버 검색하기</button>
</form>
<script>
const $form = document.forms.search;
$form.onsubmit = function () {
const keyword = this.querySelector('input[name=query]').value;
if (!keyword) {
alert('검색어를 입력하지 않았다!');
return false; // 기본 제출 동작 취소
// 혹은 e.preventDefault() 사용도 가능
}
};
</script>
핸들러 등록
onclick="..."element.onclick = handler (한 개만 가능)element.addEventListener(type, handler) (여러 개 가능)이벤트 객체
addEventListener 방식에서만 자유로운 변수명 사용this
this === e.currentTarget전파 / 위임
e.target 으로 하위 요소를 처리하는 패턴 = 이벤트 위임기본 동작 제어
e.preventDefault() 로 링크 이동, 폼 전송 등 막기e.stopPropagation() 으로 위/아래 전파 차단폼 이벤트