"이벤트"는 우리가 웹 페이지에서 마우스를 클릭하거나 키보드를 누르는 것과 같은 사용자 행동, 또는 인풋 창에 포커스가 맞춰지거나 문서 로드가 완료되는 것과 같이 특정 시점에 발생하는 모든 행동을 의미한다.
이러한 이벤트가 발생했을 때 특정 함수를 실행하도록 할당하는 것을 "핸들러"라고 한다.
이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라 한다.
이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용을 할 수 있다. 이와 같이 이벤트 중심으로 제어하는 방식을 이벤트 드리븐 프로그래밍(event-driven programming)이라 한다.
사용자가 버튼을 "클릭"했을 때 메시지를 처리하고 싶다면,
개발자가 명시적으로 함수를 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것이다.
아래의 코드를 살펴보면 알겠지만 onclick 프로퍼티에 함수를 할당했다.
html과 js를 분리하지 않는 방식이다.
onclick 어트리뷰트로 하게되면 함수 호출문을 직접 할당하는 방식이라 잘 쓰진 않는다.
하지만 CBD 방식의 Angular/React/Svelte/Vue.js 같은 프레임워크에서는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다. CBD에서는 HTML, CSS, JS가 다른 개별 요소가 아닌, 뷰를 구성하기 위한 요소로 보기 때문에 관심사가 다르다고 생각하지 않는다.
<button onclick="alert('click')">클릭</button>
()를 붙여야 실행 된다.<button onclick="sayHello()">클릭</button>
<script>
function sayHello() {
alert("Hello");
}
</script>
onclick="sayHello()" 어트리뷰트는 파싱되어 다음과 같은 함수를 암묵적으로 생성하고, 이벤트 핸들러 어트리뷰트 이름과 동일한 키 onlick 이벤트 핸들러 프로퍼티에 할당한다.function onclick(event) {
sayHello();
}
<!-- 이벤트 핸들러에 인수를 전달하기는 x -->
<button onclick="sayHello">클릭</button>
html과 js를 분리하는 방식이다.
DOM 노드 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다.
이벤트 핸들러 프로퍼티 키는 어트리뷰트와 마찬가지로 on+이벤트 종류를 나타내는 이벤트 타입으로 이뤄져있다.
이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.
객체.핸들러 = 핸들러(함수) 할당<button id="btn">클릭</button>
<script>
const el = document.getElementById('btn')
el.onclick = sayHello;
</script>
addEventListener4-1. addEventListener(이벤트, 함수핸들러)
<button id="btn2">클릭</button>
<script>
function sayHello() {
alert("Hello");
}
const el = document.getElementById('btn2')
el.addEventListener("click", sayHello);
</script>
4-2. addEventListener(이벤트, 함수 직접 작성) - 익명함수로 등록
<button id="btn2">클릭</button>
<script>
const el = document.getElementById('btn2')
el.addEventListener("click", () => {
alert("Hello");
});
</script>
4-3. addEventListener 삭제
removeEventListener를 사용하여 할당된 핸들러를 삭제💡 이벤트 리스너 삭제할 때 주요포인트
<button id="btn2">클릭</button>
<script>
const el = document.getElementById('btn2');
// 익명 함수로 등록
el.addEventListener("click", () => {
alert("Hello");
});
// 아래 코드는 제거 불가능 (참조가 다르기 때문)
el.removeEventListener("click", () => {
alert("Hello");
});
</script>
removeEventListener는 반드시 addEventListener에서 등록한 동일한 함수 객체 참조를 써야 작동하기 때문이다. JS 엔진은 겉보기엔 코드가 같아도, "완전히 별개 함수"로 인식하기 때문에 나중에 제거할 가능성이 있으면 별도 함수 선언/변수에 담아서 등록해야 한다.<button id="btn2">클릭</button>
<script>
function sayHello() {
alert("Hello");
}
const el = document.getElementById('btn2');
// 이벤트 등록
el.addEventListener("click", sayHello);
// 3초 후에 이벤트 제거
setTimeout(() => {
// 함수 참조로 등록 → 제거 가능
el.removeEventListener("click", sayHello);
alert("이제 클릭해도 반응 안 함!");
}, 3000);
</script>
4-4. DOMContentLoaded처럼 문서 로드 완료 시 발생하는 이벤트의 경우, 반드시 addEventListener를 사용해야 한다.
document.onDOMContentLoaded = ... 이런 건 동작 Xload, beforeunload 같은 이벤트는 window.onload, window.onbeforeunload 같은 핸들러 속성도 제공된다.💡 팁 : 가급적 addEventListener로 통일하는 것이 좋다.
아래에서 DOMContentLoaded와 관련된 문서 로딩 시점의 이벤트제어를 정리했다.
공식 문서에 따르면 HTML 문서의 생명주기엔 다음과 같은 3가지 주요 이벤트가 관여한다고 한다.
<img>)이나 스타일시트 등의 기타 자원은 기다리지 않는다.따라서 위의 순서대로 생명주기가 실행된다. 각각의 시점 이벤트이기 때문에 이벤트를 주입해서 사용해주면 된다.
// only document
window.addEventListener("DOMContentLoaded", (event) => {
console.log("DOMContentLoaded");
});
// after resources (css, images)
window.addEventListener("onload", (event) => {
console.log("load");
});
// before unload
window.addEventListener("beforeunload", (event) => {
console.log("beforeunload");
});
// resource is being unloaded
window.addEventListener("unload", (event) => {
console.log("unload");
});
주로 어떤 상황에서 활용될까?
DOM이 준비된 것을 확인한 후 원하는 DOM 노드를 찾아 핸들러를 등록해 인터페이스를 초기화할 때 사용된다.
앞에서 언급했던 것처럼 이 이벤트를 다루려면 addEventListener를 사용해야 한다.
<script>
function ready() {
alert('DOM이 준비되었습니다!');
// 이미지가 로드되지 않은 상태이기 때문에 사이즈는 0x0입니다.
alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener("DOMContentLoaded", ready);
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
위 예시에서 DOMContentLoaded은 DOM이 로드되었을 때 실행된다.
따라서 밑에 위치한 img뿐만 아니라 모든 요소에 접근할 수 있다. 하지만 이미지 파일의 로딩은 기다리지 않기 때문에 alert 창에는 이미지 사이즈가 0으로 뜬다.
이와 같은 특징을 브라우저 렌더링 과정에 대입해서 살펴보자.

💡 "렌더링 과정 중 DOM 트리가 완성되면 DOMContentLoaded 이벤트가 발생한다"
여기서 말하는 DOM 트리가 완성되는 단계를 브라우저가 동기적으로, 즉 위에서 아래로 순차적으로 실행되는 관점에서 좀 더 자세히 살펴보자.
이 과정에서 HTML을 파싱하면서 자바스크립트와 CSS를 만났을 때 DOMContentLoaded 이벤트처리는 어떻게 되는가?
1. HTML 문서를 처리하는 도중에 <script> 태그를 만났을 때
<script>를 실행한다. <script>가 끝나면 다시 HTML 문서 처리를 재개한다.<script> 안에 있는 스크립트가 처리되고 난 후에 발생한다.<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM이 준비되었습니다!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script>
alert("라이브러리 로딩이 끝나고 인라인 스크립트가 실행되었습니다.");
</script>
2. HTML 문서를 처리하는 도중에 외부 스타일시트를 만났을 때
HTML 파싱 중에 <link rel="stylesheet">를 만나면 CSS 파싱은 비동기적으로 진행될 수 있다. 즉, 외부 스타일시트는 DOM에 영향을 주지 않기 때문에 DOMContentLoaded는 외부 스타일시트가 로드되기를 기다리지 않는다.
따라서 DOMContentLoaded 이벤트가 발생할 때 CSSOM이 모두 준비됐다는 보장은 없다.
보통은 CSS 파일이 아직 다운로드 중이어도 DOMContentLoaded가 발생할 수 있다.
💥 그런데 한가지 예외가 있다. 스타일시트를 불러오는 태그 바로 다음에 스크립트가 위치하면 이 스크립트는 스타일시트가 로드되기 전까지 실행되지 않는다.
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 이 스크립트는 위 스타일시트가 로드될 때까지 실행되지 않습니다.
// 스크립트에서 요소의 좌표 정보를 사용하고 있다.
alert(getComputedStyle(document.body).marginTop);
</script>
<link>와 <script> 사이에 다른 태그가 끼면, 스크립트 실행이 그 CSSOM을 보장해서 기다리지는 않는다.DOMContentLoaded 이벤트는 DOM만 보장했다면,
load 이벤트는 DOM + CSSOM + 이미지/폰트 같은 모든 리소스까지 준비 완료되었을 때 실행된다.
<script>
window.onload = function() { // window.addEventListener('load', (event) => {와 동일합니다.
alert('페이지 전체가 로드되었습니다.');
// 이번엔 이미지가 제대로 불러와 진 후에 실행됩니다.
alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
};
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
사용자가 페이지를 떠날 때 추가 확인을 요청할 수 있는데, 이는 beforeunload를 통해 제어해주면 된다.
// use addEventListener beforeunload
window.addEventListener("beforeunload", (event) => {
// 표준에 따라 기본 동작 방지
event.preventDefault();
// Chrome에서는 returnValue 설정이 필요함
event.returnValue = "";
});

unload 페이지가 완전히 종료될 때 마지막으로 할 수 있는 작업 이벤트이다.
페이지 종료 시 마지막 정리 작업이나 통계, 로그 전송 같은 목적에만 쓸 수 있다.
만약, 사용자와 상호작용(경고창 등)을 하려면 반드시 beforeunload를 써야 한다.
unload 이벤트는 사용자가 페이지를 떠날 때 발생하므로 unload 이벤트에서 분석 정보를 서버로 보낼 수도 있을 것이다.
세션 종료 로그 보내기
window.addEventListener("unload", () => {
// 사용자가 페이지를 떠날 때 서버로 로그 전송
navigator.sendBeacon("/log", JSON.stringify({ action: "exit", time: Date.now() }));
});
Analytics / 페이지 사용 통계 기록
window.addEventListener("unload", () => {
// 페이지를 떠날 때 사용자가 머문 시간을 서버에 기록
const timeSpent = Date.now() - performance.timing.navigationStart;
navigator.sendBeacon("/analytics", JSON.stringify({ timeSpent }));
});
navigator.sendBeacon()를 쓰면 비동기 HTTP 요청을 안전하게 보낼 수 있다.다양한 이벤트 타입이 있지만, 자주 사용되는 몇 가지를 중심으로 살펴보자.
click : 요소를 클릭할 때 발생dblclick : 더블 클릭할 떄 발생이벤트 객체를 인수로 받아, 어떤 키가 눌렸는지(event.key), 현재 이벤트 타입(event.type) 등 다양한 정보를 활용할 수 있다.
keyup: 누른 키에서 손을 뗄 때 실행keydown: 키보드를 누를 때 실행. 키를 누르고 있을 때 단 한번만 실행keypress(deprecated): 키보드를 누를 때 실행. 키를 누르고 있을 때 계속 실행 keyup<body>
<input id="text" type="text" />
<script>
const input = document.getElementById("text");
input.addEventListener("keyup", (event) => {
console.log("현재 입력값:", event, event.key);
});
</script>
</body>
"텍스트" : 입력한 특정 텍스트가 콘솔창에 출력됨event : 해당 키보드 이벤트 관련된 모든 정보가 콘솔창에 출력됨event.key : 오직 키보드 값만 콘솔창에 출력됨
keydowndocument.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
console.log("ESC 눌렀다!");
}
});
3️⃣ keypress (⚠️ 현재는 표준에서 deprecated, 대부분 keydown으로 대체)
키를 누르면 keydown 이벤트가 발생, 이어서 바로 keypress 이벤트가 발생. 그런 다음 키가 해제되면 keyup 이벤트가 생성된다.
💡 keydown > keypress > keyup 순으로 이벤트 진행
focus : 포커스가 맞춰질 때blur : 포커스를 잃을 때 <body>
<input id="text" type="text" />
<script>
const input = document.getElementById("text");
input.addEventListener("focus", () => {
input.style.backgroundColor = "lightblue";
});
input.addEventListener("blur", () => {
input.style.backgroundColor = null;
});
</script>
</body>
mousemove : 마우스를 움직일 때마다 발생한다. event.clientX와 event.clientY를 사용하여 마우스 포인터의 현재 위치를 파악할 수 있다. <body>
<div
id="box"
style="
position: relative;
width: 100px;
height: 100px;
border: 2px solid lightblue;
"
></div>
<div
id="circle"
style="
position: absolute;
width: 10px;
height: 10px;
background-color: lightpink;
border-radius: 50%;
"
></div>
<script>
const box = document.getElementById("box");
const circle = document.getElementById("circle");
box.addEventListener("mousemove", (event) => {
circle.style.top = `${event.clientY}px`;
circle.style.left = `${event.clientX}px`;
});
</script>
</body>
resize : 윈도우 창의 크기가 변경될 때 발생한다. <script>
window.addEventListener("resize", () => {
document.body.innerText = `현재 창 크기는 ${window.innerWidth} x ${window.innerHeight}`;
});
</script>
input: 입력 필드의 값이 어떤 방식으로든 변경되었을 때마다 발생<input type="text" id="input"> oninput: <span id="result"></span>
<script>
input.oninput = function() {
result.innerHTML = input.value;
};
</script>
change: 입력 필드에서 포커스가 해제된 후에 값이 변경되었을 때 발생<input type="text" onchange="alert(this.value)">
<input type="button" value="버튼">
즉, 사용자가 입력을 하고 있을 때는 input 이벤트가 발생하고, 사용자 입력이 종료되어 값이 변경되면 change 이벤트가 발생한다.
좋은 글이네요. 잘 봤습니다.