Javascript

removeEventListener, this 동작 :: 마이구미

mygumi 2018. 9. 20. 14:19
반응형

이 글은 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() 와 같은 메소드를 이벤트 리스너를 통해 하나로 묶어 이해에 도움을 주기 위해 다루었습니다.

혹시나 잘못된 부분이 있다면 알려주면 감사합니다.

반응형