Event 객체 & 고급 이벤트 기법
JavaScript에서 프론트엔드에 자주 쓰이는 이벤트 객체와 고급 이벤트 기법 알아보기
JavaScript
🎪 Event 객체
Event
웹 페이지에서 마우스 클릭, 키 입력, 포커스 이동 등 어떤 사건을 발생시키는 것
이벤트 객체는 특정 이벤트가 발생했을 때, 해당 이벤트에 대한 상세 정보가 담긴 정보 꾸러미와 같다.
브라우저는 이벤트가 발생하면 이 정보 꾸러미(객체)를 자동으로 생성해서, 우리가 지정한 이벤트 리스너의 첫 번째 인자로 전달해 준다.
const myButton = document.getElementById("myButton");
myButton.addEventListener("click", (event) => {
console.log(event);
});
이 event 객체 안에는 "어떤 종류의 이벤트가", "어디서", "언제", "어떻게" 발생했는지 등 온갖 유용한 정보가 들어있다.
이벤트 객체의 핵심 속성과 메서드
객체 안에는 수많은 정보가 있지만, 실제 개발에서 자주 쓰이는 것들은 아래와 같다.
이벤트 발생 위치 정보
event.target: 실제로 이벤트가 시작된 요소event.currentTarget: 이벤트 리스너가 현재 연결되어 있는 요소
이벤트 종류 및 사용자 입력 정보
event.type: 발생한 이벤트의 종류 (String)event.key/event.code: 사용자가 누른 키에 대한 정보event.clientX/event.clientY: 브라우저 화면(Viewport) 기준으로 마우스 커서의 X, Y 좌표
이벤트 흐름 제어 메서드
event.preventDefault(): 브라우저의 기본 동작을 막는 매우 중요한 메서드- 예시 1:
<a>태그를 클릭했을 때 페이지 이동을 막고 다른 동작(e.g. 팝업 띄우기)을 시키고 싶을 때 - 예시 2:
<form>안에서submit버튼을 눌렀을 때, 페이지가 새로고침되는 것을 막고 HTTP 요청을 보내고 싶을 때
- 예시 1:
const myLink = document.getElementById("myLink");
myLink.addEventListener("click", (event) => {
event.preventDefault();
alert("링크의 기본 동작을 막았습니다.");
});
event.stopPropagation(): 이벤트가 상위 요소로 전파(버블링)되는 것을 막음- 예시: 카드 UI 컴포넌트가 있고, 카드 전체에 클릭 이벤트가, 카드 안의 '삭제' 버튼에도 별도의 클릭 이벤트가 있다고 가정
- '삭제' 버튼을 눌렀을 때, 카드 전체의 클릭 이벤트까지 실행되면 안되므로
stopPropagation()을 사용해 이벤트 전파를 차단
🚦 고급 이벤트 기법
이벤트 전파 (Propagation)
DOM Tree의 특정 요소에서 이벤트가 발생했을 때, 그 이벤트가 단순히 해당 요소에서 끝나지 않고 DOM 트리를 따라 다른 요소들에게 퍼져나가는 현상
이벤트 전파는 아래 두 가지 단계로 나뉜다.
비유하자면, 연못에 돌을 던졌을 때 돌이 수면까지 내려가는 과정이 캡처링, 돌이 닿은 지점부터 물결이 바깥으로 퍼져나가는 과정이 버블링과 같다.
캡처링
- 이벤트가 최상위 요소(
window)부터 시작하여 이벤트가 발생한 실제 요소(target)까지 내려가는 과정
버블링
- 이벤트가 실제 발생한 요소(
target)부터 시작하여 다시 최상위 요소(window)까지 올라가는 과정 addEventListener는 기본적으로 버블링 단계의 이벤트만 감지한다.- 세 번째 인자로
{ capture: true }옵션을 주면 캡처링 단계의 이벤트를 감지할 수 있다.
- 세 번째 인자로
이벤트 위임 (Delegation)
여러 자식 요소의 이벤트를 부모 요소 하나에서 처리하는 기법
버블링과
event.target속성을 활용하는 것이 핵심이다.
언제 사용할까?
- 목록(
ul,ol)처럼 동적으로 아이템이 추가되거나 삭제될 때 - 테이블의 각 셀(
td)이나 버튼이 많은 UI처럼 비슷한 요소에 동일한 이벤트를 적용해야 할 때
핵심 원리
이벤트는 버블링이라는 특성을 가진다.
특정 요소에서 이벤트가 발생하면, 그 이벤트를 처리한 후 부모 요소로, 또 그 부모 요소로 올라가며 이벤트를 전파한다.
이벤트 위임은 바로 이 버블링을 활용하는 것이다. 부모 요소에 "너의 자식 중 누군가에게 이벤트가 생기면 나에게 알려줘"라고 말해두는 것과 같다.
예시
<ul id="itemList">
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
<li>항목 4</li>
</ul>
const itemList = document.getElementById("itemList");
// 부모 요소에 이벤트 리스너를 하나만 답니다.
itemList.addEventListener("click", (event) => {
// 클릭된 요소가 LI 태그가 맞는지 확인합니다.
if (event.target && event.target.nodeName === "LI") {
console.log("클릭된 항목:", event.target.textContent);
// event.target은 실제 클릭된 <li> 요소를 가리킵니다!
}
});
li나 a 태그 각각에 이벤트 리스너를 추가하는 대신, 부모인 ul에 단 하나만 추가한다.
event.target을 통해 어떤 메뉴가 클릭되었는지 정확히 알 수 있다.
여기서 버블링과 연결고리를 지어본다면 아래와 같다.
- 사용자가 '항목 2'(
<li>)를 클릭한다. '항목 2'가 이벤트의target이 된다. - '항목 2'에서
click이벤트는 처리가 끝난 후, 이벤트 버블링 때문에 부모인<ul>로 전파됨 <ul>에 붙어 있던 이벤트 리스너가 자신의 자식 중 하나에서 클릭 이벤트가 버블링되어 올라왔다며 동작하기 시작- 리스너 함수 안에서
event.target을 확인하면, 비록 리스너는<ul>에 있지만target은 최초의 이벤트 발생지인 '항목 2'(<li>)를 정확히 가리키고 있음
결론적으로, 이벤트 버블링이라는 전파 현상이 없다면 자식 요소에서 발생한 이벤트가 부모 요소까지 전달될 수 없다. 그러면 이벤트 위임 패턴은 성립할 수 없게 된다.
디바운싱 (Debouncing)
연속적으로 발생하는 이벤트를 그룹화하여 특정 시간이 지난 후에 마지막 이벤트에 대해서만 콜백 함수를 실행하는 기법
언제 사용할까?
- 검색창 자동완성 (타이핑이 끝난 후 API 요청)
- 창 크기 조절(resize) 이벤트 처리 (크기 조절이 끝난 후 레이아웃 계산)
- 버튼 중복 클릭 방지
예시
<input type="text" id="searchBox" placeholder="검색어를 입력하세요..." />
const searchBox = document.getElementById("searchBox");
let debounceTimer; // 타이머 ID를 저장할 변수
function search(keyword) {
console.log(`API 요청 실행: ${keyword}`);
}
searchBox.addEventListener("input", function (event) {
// 1. 기존에 설정된 타이머가 있다면 취소합니다.
clearTimeout(debounceTimer);
const keyword = event.target.value;
// 2. 500ms(0.5초) 후에 search 함수를 실행하도록 새로운 타이머를 설정합니다.
// 만약 0.5초 안에 새로운 입력이 들어오면, 위에서 타이머가 취소되고 새 타이머가 설정됩니다.
debounceTimer = setTimeout(() => {
search(keyword);
}, 500);
});
쓰로틀링 (Throttling)
이벤트를 일정 시간 간격으로 딱 한 번만 실행하도록 제한하는 기법
언제 사용할까?
- 스크롤 이벤트에 기반한 애니메이션 (일정 간격으로만 위치 계산)
- 마우스 이동(mousemove) 이벤트 처리 (일정 간격으로만 커서 위치 추적)
예시
let throttleTimer; // 쓰로틀링 상태를 체크할 변수
function onScroll() {
console.log("스크롤 이벤트 발생! 하지만 쓰로틀링으로 조절됩니다.");
}
window.addEventListener("scroll", function () {
// 1. 현재 타이머가 설정되어 있다면(쓰로틀링 중이라면) 아무것도 하지 않고 함수를 종료합니다.
if (throttleTimer) {
return;
}
// 2. 타이머를 설정하고, 그 안에 실제 실행할 함수를 넣습니다.
// 이 함수는 즉시 실행됩니다.
throttleTimer = setTimeout(() => {
onScroll();
// 3. 200ms 후에 타이머를 null로 만들어 다음 이벤트가 실행될 수 있게 합니다.
throttleTimer = null;
}, 200);
});