다른 언어로 콘솔 프로그램만 짜다 온 사람이 브라우저 자바스크립트에서 제일 낯설어하는 게 이벤트다. 콘솔 프로그램은 위에서 아래로 흐르다 끝나는데, 웹 페이지는 켜놓고 가만히 기다린다. 버튼을 누르든 글자를 치든 사용자가 뭔가 하기 전까지는 아무 일도 안 벌어진다. 이 기다렸다 반응하는 방식이 이벤트다. 나는 처음에 이걸 모르고 페이지가 뜨자마자 코드가 다 돌아버리게 짜놓고, 왜 버튼이 안 먹히냐고 한참을 헤맸다.


이번 편은 이벤트를 붙이고, 딸려오는 정보를 읽고, 기본 동작을 막고, 다시 떼는 흐름을 코드로 짚는다. 여기 나오는 코드는 브라우저에서 도는 것이라 콘솔에서 실행해 결과를 찍을 수는 없다. 그래서 결과는 실제로 눌렀을 때 무슨 일이 벌어지는지로 설명한다. 직접 확인하려면 간단한 HTML 파일에 버튼 하나 넣고 붙여 돌려보면 된다.


이벤트는 무언가 일어났다는 신호다. 클릭, 키 입력, 마우스 이동, 페이지 로딩 완료. 이 모든 게 이벤트다. 우리가 할 일은 어떤 요소에서 어떤 신호가 오면 무슨 함수를 실행할지 미리 등록해 두는 것이다. 그 등록 창구가 addEventListener다. 첫 인자로 이벤트 이름, 둘째 인자로 그때 실행할 함수를 넘긴다. 이 함수를 이벤트 핸들러(event handler, 이벤트 처리기)라 부른다.


addEventListener로 붙인다. 화면에 버튼이 하나 있고 그걸 누르면 숫자를 세고 싶다고 하자. 먼저 document.querySelector로 그 버튼 요소를 붙잡고, 거기에 클릭 핸들러를 등록한다.


const btn = document.querySelector("#buy");
let count = 0;
btn.addEventListener("click", function() {
count += 1;
console.log("눌린 횟수: " + count);
});


이제 버튼을 한 번 누를 때마다 콘솔에 "눌린 횟수: 1", "눌린 횟수: 2"가 차례로 찍힌다. 중요한 건 addEventListener를 부르는 순간엔 아무 일도 안 일어난다는 점이다. 그건 그냥 "클릭이 오면 이 함수를 불러줘"라고 예약해 두는 것뿐이다. 실제 실행은 사용자가 누르는 그 순간으로 미뤄진다. 이 시차를 못 붙잡으면 이벤트가 통째로 헷갈린다.


이벤트가 일어나면 정보가 딸려온다. 핸들러 함수는 인자를 하나 받을 수 있다. 이게 이벤트 객체다. 방금 무슨 일이 어디서 일어났는지가 다 담겨 있다. 흔히 eevent로 받는다. 그중 제일 많이 쓰는 게 e.target인데, 이벤트가 실제로 일어난 요소를 가리킨다.


btn.addEventListener("click", function(e) {
console.log(e.type);
console.log(e.target.textContent);
});


버튼을 누르면 e.type으로 "click"이 찍히고, e.target.textContent로 그 버튼 안에 적힌 글자가 찍힌다. 나는 이 e를 받는 걸 몰라서, 눌린 버튼이 어느 것인지 알아내려고 전역 변수를 여기저기 심는 삽질을 했다. 정보는 이미 핸들러 손에 들어와 있었는데 안 받고 있었던 거다.


preventDefault로 기본 동작을 막는다. 어떤 요소는 브라우저가 정해둔 기본 반응을 갖는다. 링크는 눌리면 주소로 이동하고, 폼은 제출되면 페이지를 새로 불러온다. 자바스크립트로 직접 처리하고 싶을 땐 이 기본 동작을 꺼야 한다. 이벤트 객체의 preventDefault가 그 스위치다.


const form = document.querySelector("#login");
form.addEventListener("submit", function(e) {
e.preventDefault();
console.log("직접 처리한다");
});


preventDefault를 부르면 폼을 제출해도 페이지가 새로고침되지 않고, 그 아래 우리가 짠 코드가 대신 돈다. 이 한 줄을 빼먹으면 입력값을 읽기도 전에 페이지가 통째로 다시 뜨면서 콘솔 로그가 눈앞에서 사라진다. 폼을 다루는데 값이 이상하게 자꾸 초기화된다면 십중팔구 이 줄을 안 넣은 거다.


입력값은 e.target.value로 읽는다. 텍스트 입력칸은 글자가 바뀔 때마다 input 이벤트를 쏜다. 그 순간의 입력값은 e.target.value에 들어 있다.


const box = document.querySelector("#search");
box.addEventListener("input", function(e) {
console.log("지금 값: " + e.target.value);
});


입력칸에 "사"를 치면 "지금 값: 사", 이어서 "과"를 치면 "지금 값: 사과"가 찍힌다. 글자 하나 바뀔 때마다 핸들러가 불린다. 검색어 자동완성 같은 게 이 흐름으로 돈다. value는 항상 문자열이라, 숫자 입력칸이라도 "3"처럼 문자열로 온다. 계산에 쓰려면 숫자로 바꿔야 한다는 걸 자주 까먹는다.


키보드 이벤트는 e.key를 본다. 어떤 키가 눌렸는지는 keydown 이벤트에서 e.key로 읽는다. 엔터로 검색을 실행하는 흔한 경우를 보자.


box.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
console.log("검색 실행");
}
});


입력칸에서 엔터를 누르면 "검색 실행"이 찍히고, 다른 키는 조건에 안 걸려 조용히 넘어간다. e.key는 "Enter", "Escape", "a" 처럼 사람이 읽을 수 있는 이름으로 온다. 예전엔 키 번호를 외워 e.keyCode === 13 같은 걸 썼는데, 숫자를 외우는 건 사람이 할 짓이 아니라서 지금은 e.key로 이름을 비교한다.


떼려면 붙일 때 그 함수여야 한다. 등록한 핸들러는 removeEventListener로 뗀다. 그런데 여기 지독한 함정이 있다. 뗄 때 넘기는 함수가 붙일 때 넘긴 그 함수와 같은 참조여야 한다. 익명 함수를 그때그때 새로 써서 붙였다 떼려 하면, 생김새는 같아도 서로 다른 함수라 안 떨어진다.


function onClick() { console.log("클릭"); }
btn.addEventListener("click", onClick);
btn.removeEventListener("click", onClick);


이렇게 이름 붙은 함수를 변수에 담아 양쪽에 같은 걸 넘기면 깔끔하게 떨어진다. 나는 이걸 모르고 addEventListener("click", function(){...})로 붙인 걸 똑같이 생긴 익명 함수로 떼려다 실패했고, 핸들러가 안 떨어져서 클릭 한 번에 같은 코드가 여러 번 도는 유령 버그를 만들었다. 딱 한 번만 실행되면 되는 핸들러라면 아예 세 번째 인자로 { once: true }를 넘기면 된다. 한 번 불리고 알아서 떨어진다.


인라인 onclick 속성은 피한다. HTML 태그 안에 onclick="..."처럼 직접 박는 옛날 방식도 있다. 되긴 되는데 문제가 많다. 한 요소에 핸들러를 하나밖에 못 달고, HTML과 자바스크립트가 뒤엉켜서 나중에 고치기가 힘들다. addEventListener는 같은 이벤트에 핸들러를 여러 개 겹쳐 달 수 있고, 로직이 자바스크립트 파일 한 곳에 모인다. 특별한 이유가 없으면 addEventListener로 붙이는 게 정답이다.


이벤트를 다루는 골격은 늘 같다. 요소를 붙잡고, addEventListener로 핸들러를 예약하고, 핸들러 안에서 딸려온 이벤트 객체로 무슨 일이 어디서 났는지 읽는다. 기본 동작이 방해되면 preventDefault로 끄고, 값은 e.target.value로, 키는 e.key로 읽는다. 이 흐름이 손에 붙으면 화면 위 거의 모든 상호작용을 같은 방식으로 짤 수 있다.