Skip to content
This page has been auto-translated and may contain errors.View in English

이벤트

docs.scrimba.com

아무런 반응도 하지 않는 페이지는 그냥 문서일 뿐입니다. 사용자가 버튼을 클릭하거나, 검색창에 타이핑하거나, 을 제출했을 때 무언가가 일어나게 하고 싶은 순간, "이 일이 일어나면 이 코드를 실행하라"고 말할 방법이 필요합니다. 그것이 바로 이벤트입니다. 이 챕터에서는 사용자가 하는 행동을 감지하고 거기에 반응하는 방법을 다룹니다.

이벤트 감지하기

**이벤트(event)**란 클릭, 키 입력, 폼 전송처럼 페이지에서 일어나는 일을 말합니다. 이벤트에 반응하려면 엘리먼트를 선택하고, 관심 있는 이벤트의 이름을 지정한 다음, 그 일이 일어났을 때 실행할 함수를 넘겨주면 됩니다.

이 함수를 **핸들러(handler)**라고 부릅니다. 핸들러를 연결하는 메서드가 addEventListener입니다:

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", () => {
  console.log("Someone clicked buy");
});

왼쪽부터 순서대로 읽어보면, 이 버튼에서 "click" 이벤트를 기다리다가, 그 일이 일어나면 이 함수를 실행하라는 뜻입니다. 핸들러는 이벤트가 일어날 때마다 실행되며, 한 번만 실행되는 게 아닙니다. 세 번 클릭하면 세 번 실행됩니다.

**이벤트(event)**는 사용자가 클릭하거나, 타이핑하거나, 폼을 제출하거나, 페이지 로딩이 끝나는 것처럼 무언가가 일어났을 때 브라우저가 발생시키는 신호입니다. addEventListener로 핸들러를 등록해서 이벤트에 반응하는데, 이 메서드는 문자열로 된 이벤트 이름과 이벤트가 발생할 때마다 호출할 함수를 인자로 받습니다.

js
const button = document.querySelector(".buy-button");

const handleBuy = () => {
  console.log("Someone clicked buy");
};

button.addEventListener("click", handleBuy);

handleBuy가 괄호 없이 전달된다는 점에 주목하세요. 지금 함수를 호출하는 게 아니라, 나중에 브라우저가 호출할 함수 자체를 넘겨주는 것입니다. addEventListener("click", handleBuy())처럼 쓰면 즉시 함수를 호출해버리고 그 반환값을 등록하게 되는데, 초보자가 흔히 저지르는 실수입니다.

**이벤트(event)**는 브라우저가 관심을 등록한 모든 코드에 전달하는 메시지입니다. addEventListener(type, handler)로 등록하는데, type은 문자열로 된 이벤트 이름이고 handler는 해당 엘리먼트에서 이벤트가 발생할 때마다 브라우저가 호출하는 콜백입니다.

js
const button = document.querySelector(".buy-button");

const handleBuy = () => {
  console.log("Someone clicked buy");
};

button.addEventListener("click", handleBuy);

여기서 중요한 점은 두 가지입니다. 첫째, handleBuy()처럼 호출 결과가 아니라 handleBuy라는 함수 값 자체를 넘겨야 합니다. 둘째, addEventListener는 누적 방식으로 동작합니다. 서로 다른 두 함수로 두 번 호출하면 둘 다 실행되므로, 리스너는 덮어써지는 게 아니라 쌓입니다. button.onclick은 핸들러를 하나만 담을 수 있어서 이전 핸들러를 덮어써 버리는데, addEventListener가 그 대신 쓰이는 실질적인 이유가 바로 여기에 있습니다. 핸들러에 대한 이름 있는 참조를 유지하는 것도 중요한데, 나중에 이 핸들러를 제거하고 싶을 때 필요하기 때문입니다.

핸들러는 평범한 함수이고, 핸들러를 붙이는 엘리먼트는 DOM에서 가져옵니다. 엘리먼트를 선택하고 함수를 작성할 수 있다면, 사용자가 그 엘리먼트에 하는 행동에 반응할 수 있습니다.

Juno이벤트 감지하기 사용자에게 반응하려면 엘리먼트를 선택하고, "click"처럼 문자열로 이벤트 이름을 지정한 다음, 실행할 함수를 addEventListener에 넘기면 됩니다. 이 함수가 핸들러이며, 한 번만 실행되는 게 아니라 이벤트가 일어날 때마다 매번 실행됩니다. 페이지가 사용자에게 반응하는 거의 모든 동작이 이 메서드 하나에서 시작됩니다.
Juno이벤트 감지하기addEventListener는 이벤트 이름과 함수를 받아서, 이벤트가 발생할 때마다 그 함수를 호출합니다. 함수는 handleBuy()가 아니라 handleBuy처럼 괄호 없이 이름만 전달하세요. 괄호를 붙이면 클릭 시점이 아니라 그 자리에서 즉시 호출되는데, 거의 모두가 한 번쯤은 겪는 실수입니다.
Juno이벤트 감지하기addEventListener는 조건에 맞는 이벤트가 발생할 때마다 브라우저가 호출하는 콜백을 등록하며, 덮어쓰지 않고 쌓이기 때문에 onclick보다 이 방식을 선호합니다. 호출 결과가 아니라 함수 값 자체를 넘겨야 하므로, handleBuy()가 아니라 항상 handleBuy를 사용하세요. 나중에 리스너를 제거할 때 다시 필요하니 이름 있는 참조도 계속 유지해 두세요.

이벤트 객체

브라우저가 핸들러를 실행할 때, 무슨 일이 일어났는지를 설명하는 정보 하나를 함께 넘겨줍니다. 이것이 바로 **이벤트 객체(event object)**입니다. 보통 event라는 이름의 매개변수를 핸들러에 추가해서 이 값을 받습니다:

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", (event) => {
  console.log(event.target); // 클릭된 엘리먼트
});

가장 유용한 부분은 event.target, 즉 이벤트가 일어난 엘리먼트입니다. 버튼을 클릭했다면 event.target은 바로 그 버튼입니다. 다시 선택할 필요 없이, 이벤트가 이미 어디에서 시작됐는지 알려줍니다.

모든 핸들러는 인자를 하나 받는데, 바로 무슨 일이 일어났는지에 대한 정보를 담고 있는 **이벤트 객체(event object)**입니다. 관례적으로 event(또는 e)라는 이름의 매개변수로 이 값을 받습니다:

js
const search = document.querySelector(".search-input");

search.addEventListener("input", (event) => {
  console.log(event.target.value); // 사용자가 지금까지 입력한 값
});

event.target은 이벤트가 발생한 엘리먼트입니다. 폼 필드에서는 특히 핵심적인 역할을 하는데, event.target.value가 입력 필드의 현재 텍스트를 알려주기 때문입니다. input 이벤트와 event.target.value를 함께 쓰면, 사용자가 입력하는 순간마다 그 내용을 읽을 수 있습니다.

브라우저는 핸들러에 인자를 하나 전달하는데, 바로 무슨 일이 일어났는지를 담은 스냅숏인 **이벤트 객체(event object)**입니다. 어떤 엘리먼트인지, 어떤 키인지, 마우스 위치는 어디인지, 그리고 이벤트의 기본 동작을 제어하는 메서드까지 담고 있습니다. 관례적으로 event라는 매개변수로 이 값을 받습니다.

js
const search = document.querySelector(".search-input");

search.addEventListener("input", (event) => {
  console.log(event.target.value); // 필드의 현재 텍스트
});

event.target은 이벤트가 시작된 엘리먼트이며, 리스너를 붙인 엘리먼트와 항상 같지는 않습니다(그건 event.currentTarget이며, 이 둘의 차이 덕분에 아래에서 다룰 위임(delegation)이 동작합니다). 폼 컨트롤에서는 event.target.value가 현재 값을 문자열로 반환하는데, HTML이 텍스트로 값을 담고 있는 만큼 여기서도 텍스트로 돌려받는 것입니다. 숫자가 필요하다면 Number(event.target.value)로 직접 변환해야 합니다.

Juno이벤트 객체 핸들러는 인자를 하나 받는데, 바로 이벤트 객체입니다. 이를 받으려면 event라는 이름의 매개변수를 지정하세요. 가장 자주 쓰게 될 부분은 이벤트가 일어난 엘리먼트인 event.target입니다. 다시 선택할 필요 없이, 이벤트가 이미 어디에서 시작됐는지 알고 있습니다.
Juno이벤트 객체event라는 매개변수를 지정하면 브라우저가 무슨 일이 일어났는지에 대한 정보를 채워 넣습니다. event.target은 이벤트가 발생한 엘리먼트이고, input 요소에서는 event.target.value가 필드에 현재 입력된 텍스트입니다. input 이벤트와 event.target.value를 함께 쓰면 사용자가 입력하는 순간마다 그 내용을 읽을 수 있습니다.
Juno이벤트 객체 이벤트 객체는 무슨 일이 일어났는지를 담은 스냅숏이며, event.target은 이벤트가 시작된 지점입니다. 이는 리스너를 붙인 지점과 항상 같지는 않은데, 그건 event.currentTarget입니다. 폼 필드에서 event.target.value는 문자열로 반환되므로, 숫자가 필요하면 Number(...)로 감싸주세요. target과 currentTarget의 차이를 기억해 두세요, 위임(delegation)이 바로 이 차이에 의존하기 때문입니다.

자주 쓰는 이벤트

여러분이 만들 대부분의 기능은 몇 가지 이벤트만으로 충분히 처리됩니다:

  • click: 사용자가 무언가를 클릭하거나 탭할 때.
  • input: 텍스트 필드의 값이 입력 중에 바뀔 때.
  • submit: 폼이 전송될 때.
  • keydown: 키가 눌릴 때.

까다로운 것은 submit입니다. 기본적으로 폼을 제출하면 페이지가 새로고침되어, 자바스크립트가 하던 작업이 모두 사라져 버립니다. 이를 막으려면 핸들러 안에서 event.preventDefault()를 호출하세요:

js
const form = document.querySelector("form");

form.addEventListener("submit", (event) => {
  event.preventDefault(); // 페이지가 새로고침되는 것을 막습니다
  console.log("Form submitted, page stays put");
});

대부분의 페이지는 소수의 이벤트에 의존합니다. 버튼에는 click, 값이 바뀌는 텍스트 필드에는 input, 폼에는 submit, 키보드 단축키에는 keydown을 사용합니다. 이 네 가지만 익혀도 대부분의 인터랙션을 만들 수 있습니다.

사람들이 자주 헷갈리는 것은 submit입니다. 폼의 기본 동작은 페이지를 새로고침하거나 다른 페이지로 이동하는 것인데, 이 때문에 코드가 처리하던 작업이 모두 사라집니다. event.preventDefault()를 호출해서 페이지를 그대로 유지하고 직접 처리를 이어받으세요:

js
const form = document.querySelector("form");

const handleSubmit = (event) => {
  event.preventDefault();
  const email = form.querySelector(".email-input").value;
  console.log(`Signing up ${email}`);
};

form.addEventListener("submit", handleSubmit);

버튼의 click이 아니라 폼 자체의 submit을 리스닝해야, Enter 키를 누르는 키보드 사용자도 놓치지 않고 처리할 수 있습니다.

대부분의 인터랙션은 네 가지 이벤트로 처리됩니다: click, input(blur를 기다리는 change와 달리 값이 바뀔 때마다 발생), submit, keydown입니다. 이들을 올바른 엘리먼트에 연결하면 대부분의 UI가 자연스럽게 만들어집니다.

preventDefault가 진짜 제 역할을 하는 곳은 submit 이벤트입니다. 폼의 기본 동작은 페이지 전체를 이동시키는 것이라, 페이지 내부 상태가 모두 사라집니다. event.preventDefault()를 호출해 이를 취소하면 데이터를 직접 처리할 수 있습니다:

js
const form = document.querySelector("form");

const handleSubmit = (event) => {
  event.preventDefault();
  const email = form.querySelector(".email-input").value;
  console.log(`Signing up ${email}`);
  // 서버로 전송하는 부분은 다음 챕터에서 다룹니다
};

form.addEventListener("submit", handleSubmit);

버튼의 click이 아니라 폼의 submit에 연결하세요. submit은 Enter 키, 버튼 클릭, 코드로 직접 제출하는 경우 모두에서 발생하므로 올바른 지점입니다. 값을 확보한 뒤 어딘가로 전송하는 부분은 비동기(async) 코드가 담당하며, 이는 다음 챕터에서 다룹니다.

버블링과 위임

엘리먼트를 클릭하면 이벤트는 거기서 끝나지 않습니다. 이벤트는 **버블링(bubbling)**됩니다. 클릭된 엘리먼트에서 먼저 발생한 뒤, 그 부모, 그 부모의 부모로 계속 위로 전파됩니다. 그래서 부모에 붙인 리스너가 자식의 클릭도 감지할 수 있는 것입니다.

이 동작이 바로 **위임(delegation)**을 가능하게 합니다. 부모에 리스너를 하나만 붙이고 event.target으로 실제로 클릭된 자식이 무엇인지 확인하는 방식입니다. 항목마다 리스너를 붙이는 대신, 모든 항목을 커버하는 리스너 하나만 있으면 됩니다.

js
const list = document.querySelector(".todo-list");

list.addEventListener("click", (event) => {
  if (event.target.matches(".delete")) {
    event.target.closest("li").remove();
  }
});

이 방식의 이점은, 나중에 목록에 추가된 항목까지도 처리할 수 있다는 것입니다. 항목별 리스너는 지금 존재하는 엘리먼트에만 연결되지만, 부모에 붙인 위임 리스너는 코드가 실행될 당시 존재하지 않았던 자식의 클릭도 감지합니다. 이벤트가 여전히 리스닝 중인 부모까지 버블링되기 때문입니다.

리스너 제거하기

removeEventListener는 핸들러를 제거하는데, 등록할 때 사용한 것과 정확히 같은 함수 참조를 넘겨야만 동작합니다:

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", handleBuy);
button.removeEventListener("click", handleBuy); // 동작함: 동일한 참조

그래서 익명 핸들러는 제거할 수 없습니다. addEventListener("click", () => {...})처럼 인라인 화살표 함수를 쓰면 매번 새로운 함수가 만들어지기 때문에, removeEventListener에 넘길 참조가 없어서 리스너를 제거할 방법이 없습니다. 나중에 제거할 계획이 있다면 이름 있는 핸들러를 사용해서 이를 피하세요. 다루던 엘리먼트보다 더 오래 살아남는 리스너는 실제로 **메모리 누수(memory leak)**의 원인이 되는데, 리스너가 여전히 참조를 붙잡고 있어서 페이지가 더는 필요하지 않은 객체를 회수하지 못하기 때문입니다.

성능과 관련해 한 가지 더: scroll이나 touch 리스너에 세 번째 인자로 { passive: true }를 추가하면, preventDefault를 호출하지 않겠다는 것을 약속하는 셈이 되어, 브라우저가 핸들러를 기다리지 않고 바로 스크롤할 수 있어서 스크롤이 매끄럽게 유지됩니다.

Juno자주 쓰는 이벤트 가장 많이 쓰게 될 이벤트는 click, input, submit, keydown입니다. 꼭 기억할 것은 submit인데, 폼은 기본적으로 페이지를 새로고침하므로, 이를 막고 제어권을 유지하려면 핸들러에서 event.preventDefault()를 호출해야 합니다. 이를 빠뜨리면 페이지가 갑자기 새로고침되는데, 처음에는 당황스러울 수 있습니다.
Juno자주 쓰는 이벤트click, input, submit, keydown이 대부분의 상황을 처리합니다. 버튼의 click이 아니라 폼의 submit을 리스닝해야 키보드 사용자를 위한 Enter 키도 동작합니다. 그리고 submit 핸들러에서 event.preventDefault()를 호출하지 않으면 페이지가 새로고침되면서 상태가 사라집니다.
Juno자주 쓰는 이벤트 이벤트는 부모로 버블링되므로, 위임 리스너 하나와 event.target만으로 나중에 추가된 자식까지 포함해서 여러 자식을 모두 처리할 수 있습니다. removeEventListener는 등록할 때와 동일한 함수 참조가 필요하기 때문에, 익명 핸들러는 제거할 수 없고 메모리 누수를 일으킬 수 있습니다. 그리고 submit은 Enter 키에도 반응하므로 버튼의 click보다 낫습니다. 새로고침을 막으려면 event.preventDefault()를 꼭 넣어두세요.

이벤트, 그 다음은?

이벤트는 페이지가 무언가를 감지하는 방법입니다. 엘리먼트를 선택하고, 관심 있는 대상을 지정한 다음, 그 일이 일어나면 함수를 실행하면서 이벤트 객체에서 필요한 정보를 읽어옵니다. 거의 모든 인터랙티브 기능이 이 흐름 위에서 만들어집니다.

자연스러운 다음 단계는 폼을 서버로 전송하거나 새 데이터를 불러오는 것처럼, 페이지를 멈추지 않고도 핸들러 안에서 시간이 걸리는 작업을 처리하는 것입니다. 이것이 바로 비동기(async) 코드가 하는 일이며, 다음 챕터에서 다룹니다. 객체(objects)에 담아둔 항목 목록처럼 구조화된 데이터를 다루는 핸들러도 여러분이 이미 알고 있는 내용에 의존합니다.