thenullpage.com
화면에 있는 버튼이 눌리는 순간을 코드가 알아채는 방법은 이벤트 하나뿐이다. 문법은 줄 하나면 끝나는데 나는 이 줄 하나에서 참 여러 번 넘어졌다. 대부분 자바스크립트가 이상해서가 아니라, 함수를 넘긴다는 게 무슨 뜻인지 내가 흐릿하게 알고 있어서 생긴 일이었다.
페이지를 열자마자 저장이 됐다. 아무도 버튼을 안 눌렀는데 콘솔에 저장 로그가 찍혀 있었다.
function save() {
console.log("저장됨");
}
const btn = document.querySelector("#save");
btn.addEventListener("click", save()); // 열자마자 저장됨이 찍힌다
save()는 함수가 아니라 함수를 지금 실행한 결과다. 저장 로그는 그때 찍혔고, 실행 결과인 undefined가 리스너 자리에 등록됐다. 그래서 정작 버튼을 눌러도 아무 일이 없다. 괄호를 빼고 이름만 넘겨야 브라우저가 나중에 대신 불러준다. 인자를 같이 넘기고 싶으면 그때만 감싼다.
btn.addEventListener("click", save); // 이름만 넘긴다
btn.addEventListener("click", () => save(3)); // 인자가 필요하면 감싼다
핸들러를 두 개 붙였는데 하나만 살아 있었다. 옛날 방식인 onclick으로 붙인 게 문제였다.
btn.onclick = () => console.log("A");
btn.onclick = () => console.log("B");
// 클릭하면 B만 찍힌다
btn.addEventListener("click", () => console.log("A"));
btn.addEventListener("click", () => console.log("B"));
// 클릭하면 A와 B가 등록한 순서대로 찍힌다
onclick은 요소가 가진 칸 하나짜리 속성이라 나중에 넣은 값이 앞의 것을 덮는다. 남이 짜둔 코드와 내 코드가 같은 버튼을 건드리면 한쪽이 소리 없이 사라진다. addEventListener는 등록한 만큼 차곡차곡 쌓인다.
리스너를 뗐는데도 계속 돌았다. 모달을 닫은 뒤에도 마우스 좌표가 콘솔에 쏟아지고 있었다.
box.addEventListener("mousemove", (e) => console.log(e.clientX));
box.removeEventListener("mousemove", (e) => console.log(e.clientX)); // 안 떨어진다
두 줄의 화살표 함수는 생김새만 같을 뿐 각각 새로 만들어진 다른 함수다. 브라우저는 등록할 때 받은 그 함수와 같은 것을 줘야만 떼어낸다. 이름을 붙여 변수에 담아두면 해결된다.
function onMove(e) {
console.log(e.clientX);
}
box.addEventListener("mousemove", onMove);
box.removeEventListener("mousemove", onMove); // 이제 떨어진다
어차피 한 번만 실행할 거라면 뗄 걱정을 할 필요도 없다. 세 번째 인자로 옵션을 주면 브라우저가 알아서 정리한다.
btn.addEventListener("click", () => console.log("최초 1회"), { once: true });
// 두 번째 클릭부터는 아무 반응이 없다
핸들러의 첫 번째 인자에 사건 정보가 들어온다. 무슨 일이 어디서 났는지는 전부 이 객체에 담겨 있다.
const input = document.querySelector("#nick");
input.addEventListener("input", (e) => {
console.log(e.type); // input
console.log(e.target.value); // 한 글자 칠 때마다 현재 값
});
input.addEventListener("change", (e) => {
console.log("확정", e.target.value); // 다 치고 칸 밖을 눌렀을 때 한 번
});
글자 수를 실시간으로 세는 자리에 change를 걸어놨다가 숫자가 안 움직인다고 한참 들여다본 적이 있다. 타이핑 중에는 input, 값이 확정될 때는 change다.
엔터가 두 번 눌린 것처럼 검색됐다. 한글을 치고 엔터를 눌렀을 때만 그랬다.
const search = document.querySelector("#q");
search.addEventListener("keydown", (e) => {
console.log(e.key); // ㄱ, a, Enter, Escape, ArrowUp 처럼 그대로 찍힌다
if (e.isComposing) return; // 한글 조합 중이면 무시
if (e.key === "Enter") console.log("검색 실행", search.value);
});
한글은 자음과 모음을 합치는 중간 상태가 있어서, 조합이 끝나는 엔터와 진짜 엔터가 연달아 들어온다. e.isComposing이 그 조합 중인 상태를 알려준다. 예전 코드에서 흔히 보이는 e.keyCode는 숫자를 외워야 하는 데다 이제 안 쓰기로 한 속성이라, 지금은 e.key만 쓴다.
로그인 폼에서 엔터를 쳤더니 화면이 새로고침됐다. 콘솔 로그도 같이 지워져서 원인을 못 찾고 있었다.
const form = document.querySelector("#login");
form.addEventListener("submit", (e) => {
e.preventDefault(); // 브라우저 기본 동작인 페이지 전송을 막는다
const data = new FormData(form);
console.log(Object.fromEntries(data)); // { id: "user01", pw: "1234" }
});
폼 전송이나 링크 이동처럼 브라우저가 원래 하기로 되어 있는 동작이 있다. 내가 직접 처리하겠다면 preventDefault로 그걸 먼저 취소해야 한다. 반대로 href="#"를 눌렀을 때 주소창 끝에 샵이 붙는 것도 같은 방법으로 막는다.
스크롤 하나에 페이지가 뚝뚝 끊겼다. 손가락을 한 번 움직이는 동안 핸들러가 수십 번 불린다는 걸 그때 알았다.
window.addEventListener("scroll", () => {
console.log(window.scrollY);
}, { passive: true });
passive는 이 안에서 preventDefault를 쓰지 않겠다고 미리 알려주는 옵션이다. 브라우저는 핸들러가 스크롤을 막을지 지켜볼 필요가 없어져서 화면을 바로 굴린다. 호출 횟수 자체를 줄이려면 타이머로 묶는 방법이 있다.
let timer;
window.addEventListener("resize", () => {
clearTimeout(timer);
timer = setTimeout(() => console.log("계산", window.innerWidth), 200);
});
// 창을 붙잡고 흔들어도 멈춘 뒤 한 번만 찍힌다
돌아보면 이벤트에서 난 사고는 두 갈래였다. 하나는 함수를 넘긴다는 걸 함수를 실행한다로 착각해서, 하나는 브라우저가 원래 하려던 일을 내가 모르고 있어서 났다. 그래서 지금은 addEventListener를 쓸 때 괄호가 붙었는지부터 보고, 뭔가 이상하면 핸들러 첫 줄에 console.log(e.type, e.target)을 찍어 이게 진짜 불리기는 하는지 확인한다. 불리지도 않는 함수를 붙잡고 고치느라 날린 시간이 아까워서 생긴 습관이다.