이벤트 입문

김동현·2023년 2월 28일
0

자바스크립트

목록 보기
7/22

What is an event?

시스템은 이벤트가 발생할 때 어떤 종류의 신호를 생성하고 액션이 자동으로 수행될 수 있는 메커니즘을 제공한다.

이벤트는 브라우저 창 안에서 발생하며, 창 안에 있는 특정 항목에 연결되는 경향이 있다.

여기서의 특정 항목은 하나의 엘리먼트, 엘리먼트들의 집합, 현재 탭에 로드된 HTML 문서 또는 전체 브라우저 창일 수 있다.

MDN의 이벤트 레퍼런스➡️를 보면 이벤트가 많다는 것을 알 수 있다.

이벤트에 반응하기 위해서는 해당 특정 항목에 이벤트 핸들러를 붙여줘야 한다.

이벤트 핸들러는 이벤트가 발생했을때 실행되는 코드 블록이다.

코드 블록이 이벤트에 반응하여 실행되도록 정의하는 것을 이벤트 핸들러를 등록한다고 말한다.

이벤트 핸들러를 이벤트 리스너라고 부르기도 한다.
엄밀히 따지자면, 리스너는 이벤트 발생을 감지하는 걸 뜻하고, 핸들러는 발생에 대한 반응으로 실행되는 코드를 뜻한다.
하지만, 리스너와 핸들러는 같이 동작하기 때문에 이벤트 리스너와 이벤트 핸들러를 바꿔서 부르기도 한다.

웹 이벤트는 자바스크립트 언어의 일부가 아닌 브라우저에 내장된 API의 일부로 정의된다.


Using addEventListener()

addEventListener() 메서드는 이벤트 핸들러를 추가하는 데 권장되는 메커니즘이다.

<button>Change color</button>
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

function changeBackground() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.addEventListener("click", changeBackground);

버튼의 경우 클릭 이벤트 말고도 다른 여러 이벤트가 발생할 수 있다.

  • focus / blur
  • dblclick
  • mouseover / mouseout

Removing listeners

addEventListener() 메서드로 이벤트 핸들러를 등록했다면 removeEventListener() 메서드로 이벤트 핸들러를 제거할 수 있다.

btn.removeEventListener("click", changeBackground);

또는 이벤트 핸들러를 등록할 때 AbsortSignaladdEventListener() 메서드에 전달하면 나중에 AbsortSignal 을 소유하고 있는 컨트롤러에서 abort() 를 호출하면 이벤트 핸들러가 제거된다.

const controller = new AbortController();

btn.addEventListener(
  "click",
  () => {
    const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal }
); // pass an AbortSignal to this handler
controller.abort(); // removes any/all event handlers associated with this controller

간단하고 작은 프로그램일 경우엔 굳이 이벤트 핸들러를 제거하지 않아도 상관없지만, 크고 복잡한 프로그램일 경우엔 제거하면 효율성이 높아진다.

또한 이벤트 핸들러를 제거를 통해 같은 버튼을 통해 다른 상황에서 다른 작업을 수행하도록 만들 수 있다.

Adding multiple listeners for a single event

동일한 이벤트에 대해 여러개의 이벤트 핸들러를 등록할 수 있다.

myElement.addEventListener("click", functionA);
myElement.addEventListener("click", functionB);

Learn more

Other event listener mechanisms

이벤트 핸들러를 등록하는 다른 2가지 방법이 더 있다.

그러나 알아만 두고 사용하지는 말자.

Event handler properties

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

btn.onclick = () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
};

이 방법의 단점은 하나의 이벤트에 대해 2개 이상의 이
벤트 핸들러를 등록할 수 없다는 점이다.

element.onclick = function1;
element.onclick = function2;

코드만 봐도 알겠지만 덮어쓰기 되기 때문이다.

Inline event handlers — don't use these

<button onclick="bgChange()">Press me</button>
function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

이벤트 핸들러 어트리뷰트는 쉽게 사용할 수 있지만 관리가 힘들고 매우 비효율적이다.

여러 파일일 경우 HTML과 자바스크립트를 별도로 유지하는 것이 관리차원에서 효율적이다.

단일 파일에서도 이벤트 핸들러 어트리뷰트는 좋지않다.

한 개의 버튼은 괜찮겠지만 100개의 버튼이 있다면 100개의 이벤트 핸들러 어트리뷰트를 추가해야 한다.

애초에 자바스크립트에서 관리하면 반복문으로 끝낼 수 있다.

어차피 보안때문에 많은 서버에서 인라인 자바스크립트를 허용하지 않는다.

Event objects

이벤트 핸들러 내에서 event, evt, e와 같은 매개변수를 볼 수 있다.

이를 이벤트 객체라고 하며 추가 기능과 정보를 제공하기 위해 이벤트 핸들러로 자동으로 전달된다.

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

function bgChange(e) {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener("click", bgChange);

Extra properties of event objects

대부분의 이벤트 객체에는 사용할 수 있는 표준 속성 및 메서드들이 포함되어 있다.

이벤트 객체 레퍼런스 보러가기➡️

일부 이벤트 객체는 특정 유형의 이벤트와 관련된 속성이 추가되어 있다.

예를 들어 keydown 이벤트는 사용자가 키를 누를 때 발생하는 이벤트인데 이 이벤트의 이벤트 객체는 KeyboardEvent 로, 어떤 키를 눌렀는지 알려주는 key 속성을 가지고 있다.

<input id="textBox" type="text" />
<div id="output"></div>
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");
textBox.addEventListener(
  "keydown",
  (event) => (output.textContent = `You pressed "${event.key}".`)
);

a키를 누른 이미지


Preventing default behavior

때로는 이벤트가 기본적으로 수행하는 작업을 중단해야 하는 상황이 발생할 수 있다.

event.preventDefault() 로 할 수 있다.

가장 일반적인 예로 Form을 들 수 있다.

기본적으로, 내용을 작성하고 제출 버튼을 클릭하면 데이터를 처리하기위해 서버의 지정된 페이지로 데이터를 전달한 후에 redirected 페이지로 이동한다.

문제는 사용자가 데이터를 올바르게 제출하지 않았을 때 발생한다.

개발자는 올바르게 작성되지 않은 데이터를 서버에 제출하지 못하도록 막아야 한다.

일부 브라우저는 자동 form 데이터 유효성 검사 기능을 지원하지만, 대부분의 브라우저는 그렇지 않기 때문에 이러한 기능에 의존하지 말고 자체적으로 유효성 검사를 구현하는 것이 좋다.

간단한 예를 보자.

<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text" />
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text" />
  </div>
  <div>
    <input id="submit" type="submit" />
  </div>
</form>
<p></p>
const form = document.querySelector("form");
const fname = document.getElementById("fname");
const lname = document.getElementById("lname");
const para = document.querySelector("p");

form.addEventListener("submit", (e) => {
  if (fname.value === "" || lname.value === "") {
    e.preventDefault();
    para.textContent = "You need to fill in both names!";
  }
});

아무것도 입력하지 않고 제출버튼을 누른 이미지

폼 양식에 아무 값도 입력하지 않은 채 제출버튼을 누르면 이벤트의 기본동작이 중지된다.

Event bubbling

이벤트 버블링은 브라우저가 엘리먼트를 대상으로 이벤트를 처리하는 방법을 설명한다.

Setting a listener on a parent element

<div id="container">
  <button>Click me!</button>
</div>
<pre id="output"></pre>

<div> 엘리먼트는 <button> 엘리먼트의 부모 엘리먼트이다.
만약 부모에 클릭 이벤트 핸들러를 추가하고 버튼을 클릭하면 어떻게 될까?

const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
container.addEventListener("click", handleClick);

버튼을 클릭한 이미지

버튼을 클릭하면 DIV 엘리먼트를 클릭했다고 나온다.

버튼은 <div> 안에 있기 때문에 버튼을 클릭할 때 암시적으로 <div> 엘리먼트도 클릭한다는 것을 의미한다.

이번엔 <div> 뿐만 아니라 버튼 및 상위 엘리먼트들 모두에게 이벤트 핸들러를 등록해보자.

const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);

모두에게 이벤트 핸들러를 등록한 후 버튼을 클릭한 이미지

클릭 이벤트가 <button> => <div> => <body> 순으로 발생한다.

이런 현상을 "가장 안쪽 엘리먼트에서부터 이벤트가 bubble up 한다"라고 말한다.

버블링은 유용하지만 때로는 예상치 못한 문제를 일으킬 수 있다.

다음의 예제를 보자.

<button>Display video</button>

<div class="hidden">
  <video>
    <source
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
      type="video/webm" />
    <p>
      Your browser doesn't support HTML video. Here is a
      <a href="rabbit320.mp4">link to the video</a> instead.
    </p>
  </video>
</div>
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));
  • hidden 클래스로 인해 기본적으로 영상은 화면에 보이지 않는다.

  • 버튼을 누르면 hidden 클래스가 제거되고 영상이 보이게된다.

  • 영상이 보이는 상태에서 영상을 클릭하면 영상이 재생이 된다.

  • <video> 의 부모태그인 <div> 박스를 클릭하면 hidden 클래스가 다시 추가 되어서 영상이 다시 보이지 않게 된다.

버튼을 클릭하면 영상이 나타난다.

그러나 영상을 클릭하면 재생이 되기 시작하지만 다시 숨겨진다.

이러한 오류는 <video><div> 엘리먼트 안에 있기때문에 두 개의 이벤트 핸들러가 모두 실행됨으로써 발생한다.

Fixing the problem with stopPropagation()

이렇듯 이벤트 버블링은 때때로 문제를 일으킬 수 있지만 이를 방지할 수 있는 방법이 있다.

이벤트 객체에는 stopPropagation() 이라는 메소드가 있다.

이 메소드는 이벤트 핸들러에서 호출될 때 이벤트가 다른 엘리먼트로 bubble up 되는 현상을 막는다.

위의 코드를 stopPropagation() 메소드를 이용해 수정해보자.

const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));

video.addEventListener("click", (event) => {
  event.stopPropagation();
  video.play();
});

box.addEventListener("click", () => box.classList.add("hidden"));



preventDefault() vs stopPropagation()

헷갈릴 수 있는 부분이 있다.

바로 이벤트 전파와 기본동작이다.

다음의 예제를 보자.

<a href="https://www.naver.com">
  <img src="이미지url" />
  <button>삭제</button>
</a>

예제를 위해 사용했지만 a 엘리먼트 안에 button 엘리먼트를 넣는 건 표준에 어긋난다.

const btn = document.querySelector("button");
btn.addEventListener("click", function(e){
  e.stopPropagation();
  e.currentTarget.closest("a").remove();
}

버튼을 누르면 a 엘리먼트가 삭제되도록 만든 코드이다.

이 버튼을 누르면 클릭 이벤트가 부모 엘리먼트로 전파되는걸 막은 뒤, 부모 엘리먼트인 a 엘리먼트를 삭제한다.

그러나 버튼을 눌러보면 네이버로 이동한다.

즉, a 엘리먼트의 기본동작이 실행된 것이다.

이를 막기위해서는 preventDefault() 를 호출해야만 한다.

여기서 a 태그는 button 태그의 부모 요소이며, 클릭 이벤트가 button에서 발생한다.

이 경우, button에서 preventDefault() 를 호출하면 button의 기본 동작이 중지되고,
stopPropagation() 를 호출하면 이벤트 버블링도 중지된다.

그러나 여기서 이해해야 할 점은 클릭 이벤트가 button에서 발생하더라도 a 태그의 기본 동작으로 여전히 링크 이동이 발생한다는 것이다.

이는 브라우저의 기본 동작으로 발생하며, button태그 내부의 클릭 이벤트가 결국 a태그의 기본 동작을 활성화하기 때문이다.

이 경우 버블링이 발생하지 않더라도 기본 동작이 실행된다.

따라서, preventDefault() 를 호출하는 것은 button이 발생시킨 클릭 이벤트에 대한 a 태그의 기본 동작(링크 이동)을 막기 위한 것이다.

a 태그의 기본 동작이 링크 이동이기 때문에, button에서 preventDefault() 를 호출하면 이벤트 버블링과 무관하게 a 태그의 기본 동작이 막아지는 것이다.

즉, button을 클릭했을 때 발생하는 클릭 이벤트에 대한 a 태그의 기본 동작을 막으려면 preventDefault() 를 사용해야 한다.

결과적으로, preventDefault()stopPropagation() 은 각기 다른 목적을 가진 도구이며, 각각 기본 동작의 방지와 이벤트 전파의 방지를 목적으로 사용된다.

Event capture

이벤트 전파의 또 다른 형태는 이벤트 캡쳐이다.

이벤트 캡쳐는 이벤트 버블링과 순서만 반대일뿐 동일하다.

이벤트 버블링은 가장 안쪽 엘리먼트(타킷)에서 가장 바깥쪽 엘리먼트로 이벤트가 전파되지만 이벤트 캡처는 가장 바깥쪽 엘리먼트에서 가장 안쪽 엘리먼트(타깃)로 이벤트가 전파된다.

이벤트 캡처는 기본적으로 비활성화되어 있다.

활성화하려면 addEventListener() 에 캡처 옵션을 전달해야 한다.

다음 예제를 보자.

<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);

이벤트 캡처 이미지

왜 캡처와 버블링 둘 다 있는 걸까?

브라우저의 호환성이 지금보다 훨씬 떨어졌던 옛날에, 넷 스케이프는 이벤트 캡처만을 사용했고 익스플로러는 이벤트 버블만을 사용했다.

W3C가 동작을 표준화하고 합의할 때 이 두 가지를 모두 포함하도록 정했기 때문에 모던 브라우저들은 이 두 가지를 모두 포함하는 시스템을 갖게 된 것이다.

하지만 대부분 이벤트 핸들러는 기본적으로 이벤트 버블링 단계에 등록되어 있으며 이벤트 버블링만으로도 충분하다.

Event delegation

이벤트 버블링의 장점으로는 이벤트 위임이 있다.

이벤트 핸들러를 등록하는건 가벼운 작업이 아니다.

그러므로 많은 엘리먼트들에 개별적으로 이벤트 핸들러를 등록하는건 비효율적이다.

다음 예제를 보자.

<div id="container">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>
.tile {
  height: 100px;
  width: 25%;
  float: left;
}

tile 박스를 클릭하면 그 박스의 색깔이 변하는 코드를 만들고자 한다.

각각의 tile 박스에 이벤트 핸들러를 등록하는건 매우 비효율 적이다.

반복문을 사용하면 구현은 쉽겠지만 결국 많은 이벤트 핸들러를 등록한다는 사실은 변함이 없다.

클릭 이벤트는 bubble up 되기 때문에 각각의 tile 박스대신 container 박스에 이벤트 핸들러를 등록하는게 훨씬 효율적이다.

function random(number) {
  return Math.floor(Math.random() * number);
}

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  return rndCol;
}

const container = document.querySelector("#container");

container.addEventListener(
  "click",
  (event) => (event.target.style.backgroundColor = bgChange())
);

클릭하면 색깔이 달라지는 박스

이 예제에서는 event.target 을 사용하여 클릭 대상이었던 엘리먼트(즉 tile)를 가져온다.
이벤트 핸들러가 등록된 엘리먼트에 접근하려면 event.currentTarget을 사용하면 된다.


It's not just web pages

이벤트란 것은 자바스크립트에만 있는 것이 아니다.

대부분의 프로그래밍 언어는 일종의 이벤트 모델을 가지고 있으며 작동하는 방식도 자바스크립트와는 다르다.

또한 웹페이지용 자바스크립트 이벤트 모델과 다른 환경에서 사용하는 자바스크립트 이벤트 모델도 서로 다르다.

[참고] : MDN

profile
프론트에_가까운_풀스택_개발자

0개의 댓글