자바스크립트의 이벤트
자바스크립트의 이벤트에 대해서 자세히 알아봅니다.
서론
이벤트는 사용자와의 상호작용을 처리하기 위해 없어서는 안 될 중요한 요소입니다.
현대의 웹 서비스를 이용하면서 내부적으로 수 많은 이벤트가 발생합니다.
단순히 버튼을 클릭할 때 발생하는 click 이벤트부터
브라우저를 닫는 순간 발생하는 beforeunload 이벤트까지,
개발자는 사전에 정의한 동작을 다양한 이벤트들을 통해 원하는 타이밍에 호출할 수 있습니다.
이번 포스트에서는 이러한 이벤트들에 대해 자세히 알아보겠습니다! 😁
이벤트란?
이벤트는 사용자의 행동이나 웹 브라우저에서 발생하는 특정 사건을 의미합니다.
브라우저는 발생된 특정 사건, 즉 이벤트를 감지하여 등록된 함수를 호출함으로써 이벤트에 대한 처리를 수행합니다.
왜 이벤트를 처리할 때, 함수를 등록해서 사용할까?
이벤트가 언제 발생할 지 정확한 시점을 알 수가 없기 때문입니다.
때문에 이벤트 타입과 함께 실행할 함수를 등록하고,
등록한 함수의 호출 시점을 브라우저에게 위임하는 비동기적인 방식으로 처리합니다.
이벤트 핸들러(Event Handler)와 이벤트 리스너(Event Listener)
발생된 이벤트를 감지하여 의도한 동작을 수행하려면, 두 가지가 필요합니다.
- 이벤트가 발생했을 때, 어떤 동작을 수행할 지 정의한 함수
- DOM 요소에 감지할 이벤트 타입 및 함수 등록
이 때, 1번에서 정의한 함수를 이벤트 핸들러라고 합니다.
그리고 2번과 같은 과정을 이벤트 핸들러를 등록 한다고 표현합니다.
그렇다면 이벤트 리스너는 무엇일까요?
이벤트 리스너란 DOM 요소에 이벤트 핸들러를 등록한 뒤, 이벤트가 발생했을 때 등록된 이벤트 핸들러를 호출하여 실행하는 일련의 메커니즘 자체를 의미합니다. 보통은 이벤트 핸들러와 공통적인 의미로 사용하는 것 같습니다.
이벤트 핸들러
이벤트가 발생했을 때, 호출되어 실행되는 함수를 의미
이벤트 리스너
DOM 요소에 등록된 이벤트 핸들러를 관리
특정 이벤트가 발생했을 때 해당 핸들러를 호출하는 메커니즘
이벤트 핸들러를 등록 하는 방법 3가지
이벤트 핸들러를 등록하는 방법으로는 아래와 같이 3가지 방법이 존재합니다.
- HTML 어트리뷰트 등록
- 이벤트 핸들러 프로퍼티 등록
addEventListener메서드를 사용한 등록
각각 하나씩 살펴보겠습니다.
1. HTML 어트리뷰트 등록
HTML 어트리뷰트 방식으로 이벤트를 등록하기 위해서 아래와 같은 과정으로 등록할 수 있습니다.
- 자바스크립트로 호출할 함수를 미리 정의
- 이벤트를 호출할 HTML에
on + 이벤트타입어트리뷰트의 값으로, 1번에 정의한 함수를 문자열로 작성
1
2
3
function handler1() {
console.log('HTML 어트리뷰트 이벤트가 호출되었습니다.');
}
1
2
3
<button onclick="handler1()">
HTML 어트리뷰트 이벤트 호출
</button>
하지만 이 방식은 권장되지 않습니다.
- 많은 수의 요소에 동일한 이벤트 핸들러를 등록 한 뒤, 이벤트 핸들러명이 수정된다면 많은 수의 값을 일일이 수정해야 합니다.
1 2 3 4 5 6 7 8 9 ... <button onclick="inlineHandler()">버튼1</button> <button onclick="inlineHandler()">버튼2</button> <button onclick="inlineHandler()">버튼3</button> <button onclick="inlineHandler()">버튼4</button> <button onclick="inlineHandler()">버튼5</button> <button onclick="inlineHandler()">버튼6</button> <button onclick="inlineHandler()">버튼7</button> ...
1 2 3 function inlineHandler() { // ... }- HTML과 자바스크립트가 분리되어 있지 않습니다. 이는 HTML과 자바스크립트를 반복적으로 번갈아 보아야 하는 수고로움을 더하게 됩니다.
때문에 이런 방법이 있다는 것만 알아 둡시다! 😅
2. 이벤트 핸들러 프로퍼티 방식
이벤트 핸들러 프로퍼티는 자바스크립트 코드 내에서 이벤트를 등록하는 방법 중 하나로,
DOM 요소를 선택한 뒤, 등록할 이벤트 프로퍼티에 이벤트 핸들러를 직접 할당하는 방식으로 등록할 수 있습니다.
1
2
3
<button id="prop-event-btn">
이벤트 핸들러 프로퍼티 호출
</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const $button = document.querySelector('#prop-event-btn');
// 1.
$button.onclick = function () {
console.log('이벤트 핸들러 프로퍼티가 호출되었습니다.');
}
// 2.
$button.onclick = () => {
console.log('이벤트 핸들러 프로퍼티가 호출되었습니다.');
}
// 3.
function handler() {
console.log('이벤트 핸들러 프로퍼티가 호출되었습니다.');
}
$button.onclick = handler;
하지만 이 방식에도 단점은 있습니다.
한 요소에 하나의 이벤트만을 등록할 수 있습니다.
개인적으로는 두 개 이상의 이벤트를 등록하는 경우가 거의 없어 큰 단점은 아니라고 생각합니다.
하지만 다음에 설명할 방식에서 다양한 부가 기능을 활용할 수 있다는 점과 비교하면 단점이라고 할 수 있겠습니다.
3. addEventListener 메서드를 사용한 이벤트 등록
가장 많이 사용되는 방식입니다.
node.addEventListener(event_type, callback[, use_capture]) |
addEventListener 메서드의 각 인수로 아래와 같은 값을 전달할 수 있습니다.
event_type이벤트 타입 (
ex) click, keyup ...)callback이벤트 발생 시 호출할 콜백 함수
use_capture이벤트 감지 시점을 결정
true (캡쳐링) | false (버블링, default)
1
2
3
<button id="method-event-btn">
addEventListener 메서드 이벤트 호출
</button>
1
2
3
4
5
const $button = document.querySelector('#method-event-btn');
$button.addEventListener('click', () => {
console.log('addEventListener 이벤트가 호출되었습니다.');
});
2번 이벤트 핸들러 프로퍼티 방식과는 다르게 addEventListener 메서드를 사용한다면,
하나의 DOM에 이벤트 리스너를 여러 번 등록할 수 있습니다.
이렇게 여러 번 등록했던 이벤트가 발생하면, 등록된 순서대로 이벤트 핸들러를 호출합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
$button.addEventListener('click', () => {
console.log('event 1');
});
$button.addEventListener('click', () => {
console.log('event 2');
});
$button.addEventListener('click', () => {
console.log('event 3');
});
$button.click();
1
2
3
event 1
event 2
event 3
하지만 동일한 참조를 가지는 함수를 여러 번 등록하는 경우에는
나중에 등록되는 이벤트 핸들러가 기존의 이벤트 핸들러를 덮어 씌우기 때문에
마지막 하나만 등록됩니다.
1
2
3
4
5
6
7
8
9
function eventHandler() {
console.log('event 1');
}
$button.addEventListener('click', eventHandler);
$button.addEventListener('click', eventHandler);
$button.addEventListener('click', eventHandler);
$button.click();
1
2
event 1
<!-- 콘솔에는 1번만 출력됩니다. -->
이벤트 감지
DOM 트리에는 수많은 DOM 노드가 존재합니다.
그중 작성된 HTML에서 특정 DOM 노드에서 이벤트가 발생했는지 어떻게 확인할 수 있을까요?
브라우저는 아래의 순서대로 이벤트를 감지하고 이벤트 핸들러를 실행합니다.
- 이벤트 트리거 (Event Triggering)
- 이벤트 객체 생성 (Event Object Creation)
- 이벤트 전파 (Event Propation)
순서대로 그 과정을 알아보겠습니다.
1. 이벤트 트리거 (Event Triggering)
개발자가 특정 DOM 요소에 이벤트 리스너를 등록 하면,
브라우저는 이벤트 리스너 정보를 내부적으로 저장합니다.
브라우저는 지속적으로 사용자의 상호 작용 (클릭, 키보드 입력 등)을 모니터링 하고,
해당하는 상호 작용이 발생하면 이벤트를 트리거 합니다.
2. 이벤트 객체 생성 (Event Object Creation)
발생한 이벤트대 대한 정보를 가지는 이벤트 객체를 생성합니다.
이 정보에는 이벤트 타입(event.type), 타겟 요소(event.target), 이벤트 발생 시점 등을 포함합니다.
이렇게 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타겟을 중심으로 DOM 트리를 통해 전파됩니다.
3. 이벤트 전파 (Event Propagation)
이벤트 전파는 이벤트 객체가 전파되는 방향에 따라
각각 캡쳐링 단계, 타겟 단계, 버블링 단계로 구분할 수 있습니다.
- 캡쳐링 단계 (Capturing Phase)
이벤트가 상위 요소에서 하위 요소 방향으로 전파
- 타겟 단계 (Target Phase)
이벤트가 이벤트 타겟에 도달
- 버블링 단계 (Bubbling Phase)
이벤트가 하위 요소에서 상위 요소 방향으로 전파
이벤드 핸들러 등록 방식에 따라 감지할 수 있는 단계에 차이가 있습니다.
HTML 어트리튜브, 프로퍼티 방식
타겟 단게, 버블링 단계 이벤트만 감지
addEventListener메서드 방식
타겟 단계는 물론, 버블링 단계와 캡쳐링 단계도 선택적으로 감지
addEventListener메서드의 3번째 인수에
true전달 - 캡쳐링 단계 감지
false를 전달(하거나 아무것도 전달하지 않음) - 버블링 단계 감지 (default)
이벤트 캡쳐링과 이벤트 버블링
관련해서 과거에도 이런 이벤트 전파 방식을 사용했는지 궁금해서 조금 알아보게 되었습니다.
초기 브라우저들은 지원하는 이벤트 전파방식이 각각 달랐습니다.
- Netscape Navigator - 이벤트 캡쳐링 방식
- Internet Explorer - 이벤트 버블링 방식
두 이벤트 전파방식이 각각의 장단점이 있었기 때문에 서로 다른 방식을 채택했을 겁니다.
먼저 이벤트 캡쳐링의 장단점에 대해서 알아보면,
| 이벤트 캡쳐링의 장점 | 이벤트 캡쳐링의 단점 |
|---|---|
| 선제적 이벤트 처리 | 성능 문제 |
| 전역적 이벤트 관리 | 유연성 부족 |
| 직관성 부족 | |
| 동적 요소 처리 어려움 |
다음은 이벤트 버블링의 장단점 입니다.
| 이벤트 버블링의 장점 | 이벤트 버블링의 단점 |
|---|---|
| 직관적 이벤트 흐름 | 의도치 않은 이벤트 전파 |
| 이벤트 위임 처리 가능 | 전파 중지의 필요성 |
| 코드 간소화 | 성능 고려사항 |
| 메모리 효율성 | 특정 이벤트 제한 |
| 동적 요소 처리 용이 | 복잡한 이벤트 처리 로직 |
이렇게 각각의 이벤트 전파 방식의 장단점과 그 사용용도가 명확하여
브라우저마다의 지원방식이 달랐을 겁니다.
하지만 2000년도, W3C에서 표준화 된 동작을 정의하기 위해 두 방식을 모두 포함하기로 합니다.
이로써 개발자는 상황에 맞는 적절한 이벤트 전파 방식을 선택할 수 있게 되었고,
이벤트 전파 방식에 대한 브라우저 간 호환성을 고민할 필요가 없게 되었습니다.
이벤트 객체
이벤트 객체는 동록된 이벤트 핸들러에서 지정한 타입의 이벤트가 발생했을 때,
이벤트 핸들러의 파라미터로 전달받는 객체입니다.
이는 발생한 이벤트에 대한 모든 정보를 담고 있으며,
이벤트의 타입에 따라 확장된 이벤트 객체를 제공받게 됩니다.
마우스 클릭 이벤트라면 좌표 관련 속성들이,
키보드와 관련된 이벤트라면 입력된 키의 값 등을 포함합니다.
공통적으로 가지고 있는 이벤트 객체의 속성들은
type: 이벤트의 종류 (ex: ‘click’, ‘keydown’, ‘input’ 등)target: 이벤트가 실제로 발생한 DOM 요소currentTarget: 이벤트 핸들러가 등록된 DOM 요소eventPhase: 이벤트가 어느 단계인지 나타내는 속성
등이 있고, 메서드는 아래와 같습니다.
stopPropagation(): 이벤트가 더 이상 버블링되거나 캡쳐링 되지 않도록 중지stopImmediatePropagation(): 동일한 요소에 연결된 다른 이벤트 핸들러의 실행까지 중지preventDefault(): 이벤트의 기본 동작 (ex: 링크 클릭 시 페이지 이동 등)을 막음
이러한 속성들을 활용해 이벤트 위임 패턴을 구현할 수 있습니다.
targetvscurrentTarget이 두 속성은 보통 동일한 DOM을 가리키지만, 달라지는 경우가 있음을 인지해야 합니다.
위 설명과 같이 이벤트 핸들러가 등록된 DOM과 이벤트가 발생한 DOM이 다른 경우에
처리할 로직이 있다면 유용합니다.
1 2 3 4 5 6 7 ... <div id="parent"> <div id="child"> ... </div> </div> ...
1 2 3 4 5 6 7 const $parent = document.querySelector('#parent'); const $child = document.querySelector('#child'); $parent.addEventListener('click', (e) => { console.log(`parent target: ${e.target.id}`); console.log(`parent currentTarget: ${e.currentTarget.id}`); });
$parent내부의$child영역을 클릭하게 되면, 아래와 같이 출력됩니다.
1 2parent target: child parent currentTarget: parent
이벤트 위임 (Event Delegation)
이벤트 위임이란 이벤트 전파(보통 이벤트 버블링)를 적극적으로 활용하여
부모 요소에 이벤트를 등록하여 하위 요소들의 이벤트를 효율적으로 관리하는 구현 패턴입니다.
우선 예를 하나 들어보겠습니다.
아래 코드에서 점심 메뉴 중 하나를 클릭하여 선택하는 기능을 구현한다고 해보겠습니다.
1
2
3
4
5
6
7
8
9
10
...
<ul id="lunchmenu">
<li>
<button id="kimchijjigae">김치찌개</button>
</li>
<li>
<button id="deonjangjjigae">된장찌개</button>
</li>
</ul>
...
목록은 준비되어 있으니, 이벤트 핸들러를 등록하여 구현을 할 겁니다.
1
2
3
4
5
6
7
8
9
10
const $kimchijjigae = document.querySelector('#kimchijjigae');
const $deonjangjjigae = document.querySelector('#deonjangjjigae');
$kimchijjigae.addEventListener('click', () => {
confirmMenu('kimchijjigae');
});
$deonjangjjigae.addEventListener('click', () => {
confirmMenu('deonjangjjigae');
});
자! 이제 각 메뉴를 클릭하게 되면, 필요한 로직을 잘 처리할 겁니다.
하지만 고민해봐야 할 부분은 지금은 메뉴가 두 가지 뿐이라는 것 입니다.
점심 메뉴는 김치찌개, 된장찌개만 있는 것이 아니죠.
제육볶음, 국밥 등 수 많은 메뉴들이 추가될 수 있을 텐데
그 때마다 하나씩 이벤트 핸들러르 등록하기에는 힘들고 번거로운 일 일겁니다.
이런 경우에 이벤트 위임 패턴을 사용해 훨씬 효율적이고,
아무리 많은 DOM이 추가되더라도 추가 작업이 필요 없도록 구현할 수 있습니다.
1
2
3
4
5
6
const $lunchmenu = document.querySelector('click', (e) => {
if(e.target.tagName === 'button') {
const clickedMenu = e.target.id;
confirmMenu(clickedMenu);
}
});
하나의 이벤트 핸들러 등록 만으로 원하는 로직을 구현할 수 있습니다.
메뉴가 얼마나 추가되어도 button 태그에 id 속성만 알맞게 작성하면
더 이상 코드를 추가하거나 수정할 필요가 없습니다.
커스텀 이벤트
마지막으로 커스텀 이벤트에 대해서 알아보겠습니다.
커스텀 이벤트라는 이름처럼 브라우저에 기본적으로 내장되어 있는 이벤트가 아닌,
개발자가 직접 필요한 이벤트를 만들수가 있습니다.
이는 CustomEvent 생성자를 통해 이벤트를 생성하여 만들 수 있습니다.
eventType: 개발자가 원하는 이벤트 이름eventInitalDict: 이벤트가 발생했을 때, 이벤트 객체와 함께 전달할 데이터
| Custem(eventType, eventInitalDict) |
1
2
3
4
5
6
7
8
const $button = document.querySelector('button');
const customEvent = new CustomEvent('custom', { test: 'Custom Event is evoked!' });
$button.addEventListener('custom', (e) => {
console.log(e.test);
});
$button.dispatchEvent(customEvent); // Custom Event is evoked!
이처럼 개발자가 필요에 따라 이벤트를 만들어 발생시켜
이벤트 전파를 통해 필요한 로직을 원하는 시점에 발생시키거나 데이터를 전달 할 수 있습니다.
하지만 복잡한 로직에서 이러한 커스텀 이벤트를 과도하게 사용하면,
중복 정의 등의 문제가 발생할 수도 있으니 별도의 관리가 필요합니다.
또한 특정 동작의 트리거 지점을 명확히 이해하지 않으면,
이벤트 발생 순서나 의도치 않은 동작으로 이어질 가능성이 있습니다.
정리
이렇게 이벤트와 관련된 다양한 내용들을 살펴봤습니다.
이벤트란 무엇인지부터 이벤트 핸들러를 등록하는 방법,
이벤트 전파에 대한 내용과 커스텀 이벤트까지 정리해봤는데,
이를 통해 한층 더 이벤트를 더 잘 활용할 수 있게 됐다고 생각합니다.
특히 커스텀 이벤트로 원하는 위치에서 필요한 로직과 데이터를 실행하고 전달받을 수 있으니
잘 활용하면 유용할 수 있겠다는 생각이 들었습니다.
긴 글 읽어주셔서 감사드리고, 다음에 또 다른 글로 찾아오겠습니다. 😉
Reference
잘못된 내용이 있다면, 댓글이나 메일 등으로 알려주시면 반영 하겠습니다. 😊

