[우아한테크코스 #4] 이벤트를 관리해보자 (당신이라면 어떻게 하시겠습니까)

rat8397·2022년 3월 6일
2

우아한테크코스

목록 보기
6/15
post-thumbnail

만약 당신이라면 다음과 같은 상황에서 어떻게 이벤트를 바인딩 할 것인가..!

문제 상황!

A class 에서 비즈니스 로직을 담은 핸들러를 멤버로 갖고 있고, B Class 에서 A의 핸들러를 DOM 에 바인딩 하려고 한다면 ?

class A {
  onSubmitHandler(){
	// 비즈니스 
    // 로직 군..
  }
}
class B {
	bindEventHandler(){	
      // 요기서
      // 바인딩
	}
}

저는 크게 세 가지 방법을 떠올릴 것 같아요.

1. 핸들러를 B에게 인자로서 넘긴다.

A와 B는 포함관계이다라는 설정을 추가한다면 다음과 같이 인자로 넘길 수 있겠죠 ?

class A {
  #b = null;
  constructor() {
    this.#b = new B({ onSubmitHandler: this.#onSubmitHandler });
  }
  #onSubmitHandler = () => {
    // 비즈니스
    // 로직 군..
  };
}
class B {
  constructor({ onSubmitHandler }) {
    this.#bindEventHandler(onSubmitHandler);
  }
  #bindEventHandler(onSubmitHandler) {
    document.querySelector("tag").addEventListener("submit", onSubmitHandler);
  }
}

A와 B가 포함관계가 아니라면? A의 핸들러를 꺼내와서 B 클래스에 주입해주는 수 밖에 없습니다..

class B {
  constructor({ onSubmitHandler }) {
    this.#bindEventHandler(onSubmitHandler);
  }
  #bindEventHandler(onSubmitHandler) {
    document.querySelector("tag").addEventListener("submit", onSubmitHandler);
    // 요기서
    // 바인딩
  }
}
class A {
  constructor() {}
  onSubmitHandler = () => {
    // 비즈니스
    // 로직 군..
  };
}

const a = new A();

const b = new B({ onSubmitHandler: a.onSubmitHandler });
  1. 두 클래스가 서로 연관을 갖는다면, 직접 인자를 넘기는 것을 통해 구현할 수 있다.

  2. 두 클래스에게 연관이 없다면, 둘을 사용할 수 있는 다른 스코프에서 작업을 해주면 된다.

위와 같은 방식으로 구현할 수 있지만, 기존의 설계를 어딘가 무너뜨리는 것 같지 않나요 ?!

조금 확장시켜서 인자로 넘기는 경우 다음을 대처하기가 힘들 것 같아요.

👊 핸들러의 이름이 변경되면, 전달하고 있는 쪽, 받고 있는 쪽 모두 수정해야하는 번거로움이 존재합니다. 이 구조가 깊으면 깊을수록, 비즈니스 로직이 다양하면 다양할수록 대처하기 힘들 것 같아요.

👊 위에서 말했다 싶이 기존의 구조에서 변경이 발생할 것 같아요.

👊 비즈니스 로직은 외부(연관관계가 없는) 에서 접근하게 하고 싶지 않아요. 그런데 인자로 넘겨야된다면 이 규칙을 깨버릴 것 같아요.

인자로 넘길때는 이런 문제가 있을 수 있어요. 그렇다면 커스텀 이벤트로 바인딩 해보는 건 어떨까요 ?

2. 커스텀 이벤트로 바인딩하기

기본지식에 대해서 설명하진 않을게요. 대신 !! 이뻐진 MDN 문서를 참고해주세요 !

우선 비즈니스 로직을 담은 A의 핸들러가 바인딩되는 이벤트는 가짜, B의 이벤트는 진짜라고 불러볼게요. 지금부턴 커스텀 이벤트를 통해 BA의 핸들러의 내용을 모르지만 실제로는 바인딩 되어있어보이는 코드를 만들어볼게요!

// B에는 진짜 이벤트에 핸들러가 바인딩 되지만, 이 핸들러는 가짜 클릭 이벤트를 발생시키기만 해요
class B {
  constructor(fakeClickEvent) {
    this.$app = document.querySelector("#app1");
    this.fakeClickEvent = fakeClickEvent;
    this.#bindEventHandler();
  }
  #bindEventHandler() {
    this.$app.addEventListener("click", () => {
      
      // trigger A's fakeClickEvent - realClickHandler
      dispatchEvent(this.fakeClickEvent);
    });
  }
}

// A에는 가짜 이벤트에 핸들러가 바인딩 되지만, 이 때 바인딩되는 핸들러는 비즈니스 로직이 담긴 진짜 핸들러에요
class A {
  customEvent = null;
  constructor() {
    this.#bindEventHandler();
  }
  #onRealClickHandler = () => {
    // 비즈니스
    // 로직 군..
    console.log("hi");
  };

  #bindEventHandler() {
    this.fakeClickEvent = new CustomEvent("fakeClick");

    addEventListener("fakeClick", this.#onRealClickHandler);
  }
}

const a = new A();

const b = new B(a.fakeClickEvent);

하지만, 이 또한 custom event를 B에게 주입해주어야 해요. 이렇게 주입하게 되면 인자로 넘길때와 다른 점이 있는 건가? 싶으실 거에요. 하지만 장점이 있답니다.

커스텀 이벤트 만의 장점

  1. custom eventdispatch 하는 시점이 자유롭다.

  2. 비즈니스 로직을 외부에 들키지 않아도된다.

그럼에도 다음 문제점은 해결하지 못했어요. (인자로 넘길때와 공유되는 문제점)

  1. 구조의 확장 혹은 변경이 생길 수 있다.
  2. 인자의 이름 때문에 생기는 번거로움이 있다.

아직도 해결되지 못한 문제점은 클로저를 활용하여 해결해볼 수 있을 것 같아요

3. 클로저로 해결해보기

우선 클로저랑 커스텀 이벤트를 같이 사용하여 문제점들을 해결해볼게요.

// index.js
import { bind, dispatch } from "./eventFactory.js";


class B {
  constructor() {
    this.$app = document.querySelector("#app1");
    this.#bindEventHandler();
  }
  #bindEventHandler() {
    this.$app.addEventListener("click", () => {
      dispatch("fakeClick");
    });
  }
}


class A {
  customEvent = null;
  constructor() {
    this.#bindEventHandler();
  }
  #onRealClickHandler = () => {
    // 비즈니스
    // 로직 군..
    console.log("hi");
  };

  #bindFakeClickHandler() {
    const eventType = "fakeClick";

    bind(eventType);

    addEventListener(eventType, this.#onRealClickHandler);
  }
}
// 사용처에서 B에게 a의 이벤트를 전달해줄 필요가 없다.
const a = new A();
const b = new B();
// eventFactory.js

export const { bind, dispatch } = (function () {
  const customEvents = {};
  return {
    bind: (type) => {
      customEvents[type] = new CustomEvent(type);
    },
    dispatch: (type) => {
      dispatchEvent(customEvents[type]);
    },
  };
})();

위 방식으로 커스텀 이벤트의 장점을 유지하면서, 구조가 변하지 않아도 되고 인자로 넘기지 않으니 불편한 점도 개선되었어요. 근데 이런 고민을 하실 수 도있을 것 같아요. 그러면 그냥 커스텀 이벤트 안구현해도되는거아냐? 핸들러들을 클로저에 두고 쓰면되잖아! 핸들러 두 개씩 구현해야하는 커스텀 이벤트 방식을 왜 사용해야 하지?

이렇게 생각하신다면 다음 코드를 봐주세요. 다음은 custom event를 사용하지 않고 클로저만 활용하여 짜본 코드입니다.

import { bind, dispatch } from "./eventFactory.js";

class B {
  constructor() {
    this.$app = document.querySelector("#app1");
    this.#bindEventHandler();
  }
  #bindEventHandler() {
    this.$app.addEventListener("click", (e) => {
      // 선 작업 해도됨. 추가적인 인자를 사용할 수 있도록 구현해도되고.. 
      dispatch("clickHandler"));
      
    }
  }
}
class A {
  customEvent = null;
  constructor() {
    this.#bindEventHandler();
  }
  #onClickHandler = () => {
    // 비즈니스
    // 로직 군..
    console.log("hi");
  };

  #bindEventHandler() {
    bind("clickHandler", this.#onClickHandler);
  }
}

const a = new A();
const b = new B();
// eventFactory.js
export const { bind, dispatch } = (function () {
  const eventHandlers = {};
  return {
    bind: (key, handler) => {
      //   customEvents[type] = new CustomEvent(type);
      eventHandlers[key] = handler;
    },
    dispatch: (key, e) => {
      //   dispatchEvent(customEvents[type]);
      const handler = eventHandlers[key];
      handler(e);
    },
  };
})();

요렇게도 커스텀이벤트 (+클로저)를 활용해서 짜볼 수 있어요. 원하는 시점에 event handler의 로직을 실행 시킬 수도 있고, 인자로 넘길때의 문제점도 해결되었어요. 하지만 큰 문제점이 존재한답니다.

1. 비즈니스 로직이 담긴 메소드를 외부로 보내주어야 한다(은닉화 실패)

2. 자유롭게 이벤트를 발생시킬 수 없다.

어떤 동작을 하는 커스텀 이벤트를 정의해두는 방식이 아니고, 핸들러를 외부 객체에 넣어두고 다른 클래스에서 바인딩 하는 방식이기 때문에 custom event 방식에서 처럼 유연하게 이벤트를 발생시키는 것이 불가능해요.

또 사실 크게 와닿지는 않지만, 비즈니스 로직을 꽁꽁 숨기고 싶어한다면 이렇게 핸들러를 다른 객체에서 관리하게끔 하는 방식은 좋지 않을 것이라고 생각해요 !!

정리하자면

이벤트 핸들러를 관리하는 클래스가 아닌 다른 클래스에서 바인딩하고 싶다면, 지금으로서 가장 좋은 방식은 커스텀 이벤트 - 클로저를 활용하는 방식인 것 같아요.

인자로 핸들러를 넘겨서 생길 수 있는 문제를 해결할 수 도있고, 정의한 커스텀 이벤트를 원하는 시점에 수행하게 하는것도 가능해요 (custom event를 발생시키는 것으로) 또, 비즈니스 로직이 담긴 메소드를 꽁꽁 숨길 수도 있으니 여러 방면에서 좋다고 할 수 있겠죠?!?!

논리적인 오류가 있다면 댓글로 지적해주세요 !

Reference

profile
Frontend Ninja

0개의 댓글