[REAL Deep Dive into JS] 40. 이벤트

young_pallete·2022년 10월 21일
1

REAL JavaScript Deep Dive

목록 보기
41/46
post-custom-banner

🚦 본론

이벤트는 매우 중요한 개념입니다.
우리가 렌더링하는 그 순간부터, 이미 이벤트는 호출이 되어집니다.

이번 파트에서는 이러한 이벤트들을 통해 사용자와 애플리케이션이 어떻게 서로 인터렉티브하게 상호작용할 수 있는지를 알아보겠습니다.

아! 제 글은 이벤트 타입을 다 일일이 작성하지 않습니다.
이벤트라고 해서 모든 것을 다 공부하기보다는, 상황에 따라 하나하나 알아가는 게 더 좋은 것 같아요. (실제로 이벤트를 모두 사용할 일도 없기 때문이죠!)
이 부분은, 필요한 상황이 있다면 해당 상황에 응용 가능한 이벤트가 있을지 책을 보며 살펴보시는 것을 추천드려요.

이벤트를 전달하는 3가지 방법

간단 명료하게 먼저 이벤트 전달을 위한 방법을 알아보죠.
표준으로 정해진 이벤트를 호출하는 방법은 크게 3가지가 있습니다.

  • HTML Attribute로 설정
  • window, Document, HTMLElement 타입의 property로 설정
  • window, Document, HTMLElement 타입의 addEventListener 메서드로 설정

이에 대해서는 3번째 것이 가장 확장성이 높은데요.
이유는, 다음과 같습니다.

1번째 경우에는, 파라미터를 주는 것이 어렵기 때문에 잘 사용하지 않아요.
2번째 경우에는 이벤트를 오직 하나만 추가 가능하다는 점이 매우 아쉽습니다.

그러나, 3번째는 위의 문제점들을 모두 해결해줄 수 있죠.
따라서 가급적이면, addEventListener을 사용하는 것이 바람직합니다.

addEventListener(EventType, FunctionName, [, useCapture])

그렇다면, addEventListener 메서드를 알아볼까요?
이 메서드는 3개의 인자를 받습니다. 다만 마지막 인자는 기본값이 false로, 일반적인 경우가 아니라면, 생략해도 무방합니다. (이 옵션은 좀이따 설명이 나옵니다.)

1번째 인자는, 이벤트 타입이 무엇인지를 작성합니다. (string)
2번째 인자는, 어떤 콜백을 실행할 것인지(이벤트가 트리거될 때 실행할 함수)를 작성합니다.

removeEventListener

우리가 이벤트를 설정한다면, 계속해서 메모리가 누수될 가능성을 야기합니다.

예컨대, 전역 객체에 설정되어 있으면, 전역 객체는 계속해서 이벤트를 가지고 있을 가능성이 크죠. 특히 이게 전역 모듈에 추가된 것이라면 말이죠.

따라서 제때 이벤트를 제거하는 것 역시 필요한데요.
이는, addEventListener에 할당한 모든 인자를 똑같이 쓰되, 콜백 함수만 따로 변수로 캐싱하여 다시 재할당해주면 가능합니다.

const sayHi = () => {
	console.log('hi!');
}

document.body.addEventListner('click', sayHi)

// 삭제 시에도 똑같은 핸들러 변수를 넣어주어야 합니다.
// Good
document.body.removeEventListner('click', sayHi)

// Bad
document.body.removeEventListner('click', () => {
	console.log('hi!');
})

이벤트 객체

이벤트가 트리거되면, 이 이벤트를 담고 있는 객체가 콜백함수의 첫 번째 인자로 할당됩니다.
이를 통해, 이벤트의 타입에 따라 원하는 정보를 습득할 수 있게 됩니다.

document.body.removeEventListner('click', (e) => {
	console.log(e); // PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, …}
})

이벤트 객체의 구조

이벤트 객체 역시 여러 상속을 거치게 됩니다.
이는 그리기가 마땅치 않아, 해당 블로그의 이벤트 객체 상속 구조도로 대신하고자 합니다.

결국, 포인트는 이벤트 역시 여러 종류가 있으며, 우리가 원하는 정보가 특정 이벤트에 존재하지 않을 수 있다는 점에 유의해야 합니다.

이벤트 객체의 공통 프로퍼티

이정도까지는 이해하는 게 좋을 것 같아요.
이벤트 위임이 빈번한 프로젝트의 경우 에러가 발생하면 이 원인을 추적하기 어렵습니다.
이럴 때, 이 프로퍼티들을 기억하여 디버깅하는 것 역시 좋은 방법인 것 같아요!

  • target: 현재 이벤트가 트리거된 요소
  • currentTarget: 이벤트 핸들러가 바인딩된 요소
  • bubbles: 이벤트 버블링 전파 여부
  • cancelable: 이벤트 기본 동작 취소 가능 여부
  • defaultPrevented: preventDefault()를 통한 이벤트 취소 여부
  • isTrusted: 사용자 행위로 인한 이벤트 동작 여부
  • timeStamp: 이벤트 발생 시각

이벤트 전파

정~말 중요하면서도, 디버깅하거나 구현 시 가끔 헷갈리는 문제입니다.
바로 이벤트 전파인데요. 여기서 DOM 때 배운 것이 유용하게 작용합니다.

DOM은 계층적 구조이다.

브라우저는 렌더링할 시 DOM으로 노드 객체를 만들게 돼요.
이때, 노드 객체는 트리 구조이며, 트리 구조는 계층적 구조이죠.
따라서 이벤트가 호출될 때에는,

  1. 상위 노드에서 하위 노드로 발생 지점까지 계속해서 내려가게 됩니다.
  2. 타겟을 발견하게 됩니다.
  3. 발견했음을 상위 객체에 계속해서 전달하게 됩니다.

즉, 이벤트의 호출과정은 재귀 함수 호출 형식으로 동작한다는 것을 눈치챌 수 있습니다.

이벤트 전파도 결국 이에서 착안된다.

우리는 이러한 현상에 대해 '이름'을 붙이며, 이를 일반화시키는 과정을 거칩니다.
따라서 위의 단계를 우리는 이름을 붙이기로 했습니다.

  1. 이벤트 캡쳐링 단계
  2. 타겟 단계
  3. 이벤트 버블링 단계

가 그것입니다.

그리고 일반적으로, 우리가 흔히 호출되는 이벤트는 이벤트 버블링 단계에서 발생합니다.

응용

간혹, 우리가 토이 프로젝트를 하다 보면 이런 경우가 있습니다.

카드를 만들고 싶어. 그런데, 카드를 클릭할 때가 아니라, '좋아요'를 눌렀을 때만 함수를 동작하고 싶어.

그런 경우에는 어떻게 해야 할까요?

간단한 상황 설계

codepen을 통해 주어진 상황을 간단히 구현해봅시다.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div class="card">
    <div class="card__inner">
      <div class="card__details">
        <div class="card__title">[REAL Deep Dive into JS] 40. 이벤트 </div>
        <div class="card__content">이벤트 전파에 대해 알아보자.</div>
      </div>
      <div class="card__like-btn">좋아요</div>
    </div>
  </div>
</body>
</html>

CSS

.card {
  position: relative;
  
  width: 300px;
  height: 120px;
  border-radius: 20px;
  padding: 1rem;
  box-shadow: 0px 2px 2px 2px rgba(0, 0, 0, 0.2)
}

.card__inner {
  height: 100%;
}

.card__details {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.card__title {
  font-weight: 700;
  font-size: 1.5rem;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.card__content {
  display: flex;
  align-items: center;
  height: 100%;
  width: 100%;
  flex-grow: 1;
}

.card__like-btn {
  position: absolute;
  right: 1rem;
  bottom: 1rem;
}

JS

document.body.addEventListener('click', (e) => {
  console.log('body! ', e)
})

const card = document.querySelector('.card');
card.addEventListener('click', (e) => {
  console.log('card! ', e)
})

const likeButton = card.querySelector('.card__like-btn');
likeButton.addEventListener('click', (e) => {
  likeButton.classList.toggle('card__like-btn--active');
  
  console.log('likeButton! ', e)
})

결과

제법 그럴싸~하죠? (그렇다고 해주세요... 😭)
그럼 이제, 한 번 좋아요를 클릭해봅시다.

네, 빨간색으로 잘 active 되었습니다.
그렇다면 출력 결과 순서를 볼까요?

호오! 타깃까지 잘 들어간 후, 상위 요소로 올라가는 동안 호출이 잘 되었습니다.

그렇다면, 우리, 만약 좋아요 버튼을 누를 때, card의 이벤트만 호출하고 싶다면 어떻게 할까요?

2가지를 해야 합니다.

  1. capture: trueaddEventListener의 3번째 인자를 설정해주는 겁니다. 그렇다면 캡쳐링 단계에서 이벤트의 콜백 함수를 우선하여 호출합니다.

  2. 이후에는 전파가 진행되면서 다른 이벤트들이 출력될 것입니다.
    이를 방지하기 위해 stopPropagation 메서드를 호출하면 됩니다.

const card = document.querySelector('.card');
card.addEventListener('click', (e) => {
  e.stopPropagation();
  
  console.log('card! ', e)
}, true)

그렇다면, 또 응용을 해봅시다.
만약 bodycard만 호출하고 싶다면 어떻게 할까요?
이제는 간단합니다. 기존 위의 코드에서 body만 캡쳐링을 바꿔주면 됩니다.

그렇다면 버블링 단계까지 넘어가지 않게 되고, 결론적으로 cardbody만 이벤트가 호출되겠죠!

이때 주의할 게 있습니다. capture에서는 상위에서 하위로 탐색하는 순으로 이벤트가 호출됩니다.

그렇다면 마지막으로, 좋아요 버튼!만 호출되게 하고 싶다면 어떻게 하면 될까요?

버블링 단계에서 좋아요 버튼이 제일 먼저 되니, 이후 전파만 막아버리면 됩니다.
따라서 다음 코드처럼 하시면 됩니다.

document.body.addEventListener('click', (e) => {
  console.log('body! ', e)
})

const card = document.querySelector('.card');
card.addEventListener('click', (e) => {
  console.log('card! ', e)
})

const likeButton = card.querySelector('.card__like-btn');
likeButton.addEventListener('click', (e) => {
  likeButton.classList.toggle('card__like-btn--active');
  e.stopPropagation(); // 더이상 버블링을 못하도록 한다.
  
  console.log('likeButton! ', e)
}, true)

더이상 좋아요 버튼 이벤트 외에 아무것도 나오지 않습니다.

이벤트 위임

이제 이벤트의 꽃입니다. 한 번 이를 살피기 위해, 다음 상황을 가정하겠습니다.

"카드만 클릭해도 좋아요가 눌러지게 해주세요.

이벤트 다는 건 쉽습니다. 하지만 모든 엘리먼트에 이벤트를 단다는 것은 꽤나 고려해야 하는 요소가 많은 일입니다.
계속해서 브라우저에서 이벤트를 위해 대기해야 하기 때문에 이벤트에 대한 자원이 소요된다는 점이 매우 치명적인데요.

카드 같은 경우에는, 벨로그만 해도 무~지하게 많습니다.
제가 만약 스크롤을 많이 한다면 10000개의 카드가 나올 수도 있겠습니다.

그럼, 우리는 어썸한 개발자이니 이런 생각을 할 수 있습니다.

아니, 그냥 다른 걸 하는데도 10000개의 카드에서 계속 메모리를 쓴다니. 너무 억울한 걸?

따라서 똑똑한 개발자들은 해당 문제를 단 한 번의 이벤트로 해결합니다.
그것이 바로 이벤트 위임입니다.

실제 응용

백문불여일견입니다.
직접 한 번 응용을 해보겠습니다.

아까 만든 카드를 HTML 가지고 4개를 만들겠습니다.

자. 이제 우리는 단 한 번의 이벤트를 가지고 원하는 카드를 클릭할 겁니다.
이는 매우 간단합니다. 책에선 forEach를 썼지만, 저는 ES6에서 나온, HTMLElement.prototype.closest 메서드를 사용하겠습니다.

card.parentNode.addEventListener('click', (e) => {
  const $card = e.target.closest('.card');
  if (!$card) return;
  
  $card.querySelector('.card__like-btn').classList.toggle('card__like-btn--active');
})

이벤트 위임의 원리는 간단합니다.
결국 상위 노드에서 이벤트를 달아서, 일치하는 타겟만 바꿔주자!는 것입니다.
closest는 클릭한 게 뭐든지간에, 이 상위 노드에 일치하는 게 있는지를 찾습니다.
만약 찾게 되면, 하위의 좋아요 버튼을 찾아서, 토글해주는 꼴이군요.

자, 2번째 카드를 클릭하겠습니다.

잘 클릭되었군요.
제가 closest를 쓰는 이유는 확장성이 높기 때문입니다.
간혹 e.target으로 할 때, 만약 클릭하는 게 아닌 다른 걸 동작했을 때 바뀌도록 하는 경우도 존재합니다.

이럴 때에는, closest가 좀 더 편해서, 이러한 방식으로 개발하고 있습니다.
(노드의 깊이가 커질 수록 존재하는 리스크보다, forEach로 여러 개를 순회하는 비용이 더 클 것이라 판단하는 것도 있습니다. 이벤트 위임은 결국 여러 동일한 엘리먼트에 이벤트를 달기 싫어서 하는 것이니까요.)

커스텀 이벤트

이것도 의외로 알면 정말 좋습니다.
특히 SPA 하시는 분들은 꼭 알아야 하는 것 중 하나입니다.

우리, 전역 상태 관리. 들어본 적 있으신가요?
커스텀 이벤트를 통해, 이러한 전역 상태 관리도 구현할 수 있습니다.
만약 어떤 행동을 했다면, 이를 전역에서 이벤트를 호출시켜서, 특정 컴포넌트가 무언가를 하게 동작시킬 수 있기 때문입니다.

예전에 썼던 흑역사로 남은 글...을 참조해주세요.

🎉 마치며

이번엔 이벤트를 알아보았습니다.
간단할 줄 알았는데, 생각보다 꽤 많은 글이 나왔군요.
아무래도 중요한 개념이거니와, 저는 이벤트를 좋아하는 편이기 때문에 그렇습니다.

프론트엔드이기에, 화면 동작에서 이벤트는 반드시 수반되고, 이벤트에 따라 동적으로 무언가가 바뀌어 렌더링되는 그 과정이 재밌기 때문입니다.

이벤트가 어려우셨나요?
설명보다는, 직접 부딪히며 익히는 것이 좀 더 좋아요.
한 번 제가 했던 것처럼 직접 어떤 상황을 가정하고 구현하다 보면, 이미 체화된 자신을 발견할 수 있을 거에요. 그럼, 즐거운 코딩하시길 바라며. 이상. 🌈

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉
post-custom-banner

0개의 댓글