removeEventListener, this 동작 :: 마이구미
이 글은 removeEventListener 관련된 주제로 글을 다룬다.
이벤트 리스너를 관련해서 원하는 결과를 얻지 못한 경험이 있다면, 읽어보길 바란다.
그리고 자연스럽게 this 와 call, bind, apply 와 같은 메소드를 익힐 수 있을 것이다.
참고한 링크 - https://kostasbariotis.com/removeeventlistener-and-this/
글을 진행하기 전에, 사전에 필요한 것들을 많은 용도가 있지만 여기서는 간략히 다뤄본다.
- addEventListener
- removeEventListener
- bind
- call, apply
이벤트 리스너 등록을 위해 addEventListener 메소드를 사용한다.
등록된 이벤트 리스너를 제거하기 위해 removeEventListener 메소드를 사용한다.
target.addEventListener(type, listener, useCapture);
target.removeEventListener(type, listener, useCapture);
호출되는 listener 함수의 this 는 target 이 되는 엘리먼트를 가르킨다는 것을 인지하자.
bind, call, apply 메소드의 경우에는 근본적으로는 동일한 목적을 가진다.
this 에 대한 참조를 제어할 때 사용한다.
bind() 는 this 값을 설정하고 새로운 함수를 생성한다.
call(), apply() 는 bind() 와 달리 함수를 생성하는 것이 아닌 호출한다.
function Person() { this.name = this.name || "default"; this.authority = "guest"; return this.name + " is " + this.authority; } const User = { name: "bill" } Person(); Person.call(User); const userInfo = Person.bind(User); userInfo();
진짜 주제로 넘어온다.
사실 이벤트 리스너 제거를 위해서는 등록한 리스너 함수를 그대로 removeEventListener 를 통해 호출하면 된다.
function clickHandler() {} target.addEventListener("click", clickHandler); target.removeEventListener("click", clickHandler);
하지만 이벤트 리스너 제거가 동작하지 않는 경험 한번쯤은 해봤을 것이다.
대부분 서로 다른 context 에서 작업할 때 일어난다.
이를 알아보기 위해 원하는 요구사항을 맞춰가면서 예제를 통해 진행한다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler); } return class Button { constructor() { this.el = document.createElement("button"); } clickHandler() { console.log("Click!!"); } }; })();
const b = new btn(); b.el.dispatchEvent(new Event("click"));
위와 같은 코드가 존재한다.
핵심은 클릭 이벤트에 대한 핸들러 함수를 등록하기 위한 addEvents() 함수가 존재한다.
하지만 Button 클래스가 아닌 다른 context 에 존재하는 것을 볼 수 있다.
여기서 먼저 우리가 해야할 일은 addEvents() 함수를 호출하는 것이다.
Button 클래스의 생성자에서 호출한다고 가정한다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler); } return class Button { constructor() { this.el = document.createElement("button"); addEvents(); } clickHandler() { console.log("Click!!"); } }; })();
위 코드를 실행하면 당연히 동작하지 않는다.
addEvents() 함수에서의 this 는 Button 을 참조하고 있지 않고, 현재 window 을 참조하고 있다.
그로 인해, 다음과 같은 오류가 발생한다.
const b = new btn(); b.el.dispatchEvent(new Event("click"));
=> Uncaught TypeError: Cannot read property 'addEventListener' of undefined
addEvents() 함수에서 this.el 는 버튼 엘리먼트가 되어야한다.
처음에 언급했던 this 를 제어하기 위한 메소드인 call() 메소드를 사용해서 this 값을 전달한다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler); } return class Button { constructor() { this.el = document.createElement("button"); addEvents.call(this); } clickHandler() { console.log("Click!!"); } }; })(); const b = new btn(); b.el.dispatchEvent(new Event("click")); => Click!!
그 결과, 올바르게 동작하는 것을 볼 수 있다.
클릭 이벤트가 발생할 때, 다음과 같은 경우를 추가해보자.
clickHandler() { console.log("Click!!"); this.obj["a"] += 1; }
추가된 코드를 실행했을 때, 원하는 동작을 하지 않는다.
그 이유는 clickHandler 함수의 this 의 값은 addEventListener 의 타겟인 엘리먼트가 되기 때문이다.
clickHandler() 의 this 값 => <button></button>
=> Uncaught TypeError: Cannot read property 'a' of undefined
addEvents() 함수에 call() 을 통해 원하는 this 를 전달했듯이, clickHandler() 함수 또한 this 를 전달해주면된다.
이 경우네느 bind() 를 통해 this 를 전달 및 새로운 함수를 생성함으로써, 콜백 형식을 유지할 수 있다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler.bind(this)); } return class Button { constructor() { this.el = document.createElement("button"); this.obj = { a: 1 }; addEvents.call(this, this.clickHandler); } clickHandler() { console.log("Click!!"); this.obj["a"] += 1; } }; })(); const b = new btn(); b.el.dispatchEvent(new Event("click")); // obj.a = 2 b.el.dispatchEvent(new Event("click")); // obj.a = 3
위처럼 원하는 결과를 얻을 수 있다.
이번에는 본격적으로 removeEventListener 를 사용해 등록된 이벤트 리스너를 제거하기 위한 작업을 추가한다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler.bind(this)); } function removeEvents() { this.el.removeEventListener("click", this.clickHandler.bind(this)); } return class Button { constructor() { this.el = document.createElement("button"); this.obj = { a: 1 }; addEvents.call(this, this.clickHandler); } clickHandler() { console.log("Click!!"); this.obj["a"] += 1; } destroyed() { removeEvents.call(this); } }; })();
위 코드를 통해 등록된 이벤트 리스너를 제거할 수 있는가?
문제를 정확히 파악하고 있다면, 동작하지 않는다는 것을 알 수 있다.
const b = new btn(); b.el.dispatchEvent(new Event("click")); // obj.a = 2 b.destroyed(); b.el.dispatchEvent(new Event("click")); // obj.a = 3
이유는 앞서 과정에서 bind() 를 통해 리스너 함수를 지정했다.
bind() 는 새로운 함수를 생성하기 때문에, addEventListener 와 removeEventListener 의 리스너 함수는 같지 않다.
결과적으로 다음과 같은 코드를 통해 해결할 수 있다.
const btn = (() => { function addEvents() { this.el.addEventListener("click", this.clickHandler); } function removeEvents() { this.el.removeEventListener("click", this.clickHandler); } return class Button { constructor() { this.el = document.createElement("button"); this.obj = { a: 1 }; this.clickHandler = this.clickHandler.bind(this); addEvents.call(this, this.clickHandler); } clickHandler() { console.log("Click!!"); this.obj["a"] += 1; } destroyed() { removeEvents.call(this); } }; })();
bind 한 함수를 리스너에서 할당하는 것이 아닌, 미리 할당해놓으면 된다.
그 결과 클릭을 해도 clickHander() 함수는 호출되지 않는다.
즉, a 의 값은 변화하지 않는다.
const b = new btn(); b.el.dispatchEvent(new Event("click")); // obj.a = 2 b.destroyed(); b.el.dispatchEvent(new Event("click")); // obj.a = 2
결론적으로 이 글은 this 와 call(), apply(), bind() 와 같은 메소드를 이벤트 리스너를 통해 하나로 묶어 이해에 도움을 주기 위해 다루었습니다.
혹시나 잘못된 부분이 있다면 알려주면 감사합니다.