RxJS 의 쓰임새 - DOM 이벤트편

seop·2022년 4월 10일
0

RxJS 쓰임새 고민

목록 보기
1/1
post-thumbnail

RxJS 쓰임새 고민 1탄으로, 이벤트를 사용함에 있어 어느 이점이 있는지 고민하는 시간을 갖도록 하겠습니다.

RxJS 의 공식문서 Overview 중, Observables 챕터를 보면, 다음과 같은 설명이 있습니다.

As opposed to EventEmitters which share the side effects and have eager execution regardless of the existence of subscribers, Observables have no shared execution and are lazy.

해석해보자면, EventEmitter 는 Subscriber 의 존재와 상관 없이 eager execution 을 하면서 사이드이펙트가 공유 되고, Observable은 lazy excution 을 하면서 사이드이펙트가 공유되지 않는다고 말할 수 있습니다.

저는 저 문구에서, Observable 이 사이드이펙트를 공유하지 않음으로써 유지보수가 용이한 이벤트 핸들링이 가능할 것이라 판단했습니다. 이를 증명하기 위해, DOM Event 환경에서 EventEmitter를 쓰는 방법, RxJS의 Observable 을 쓰는 방법 총 2가지의 예시를 들어 설명드려 보겠습니다.

결론먼저 말씀드리자면, 역시 코드 유지보수에 탁월하다는 것입니다.

EventEmitter 예시

let count = 0;
let rate = 1000;
let lastEvent = Date.now() - rate;
document.addEventListener('click', () => {
  if (Date.now() - lastEvent >= rate) {
    console.log(`Clicked ${++count} times`);
    lastEvent = Date.now();
  }
});

EventEmitter는 아니지만, DOM API의 addEventListener도 이와 비슷한 역할을 하므로 DOM API 의 예시를 들겠습니다.
해당 코드는, 1초 단위로 throttle 하여, 최소 1초 간격으로 이벤트 실행을 보장시키는 코드입니다.

이벤트 Subscriber가 여러 모듈에 공유되는 전역변수를 수정하고 있습니다. 이렇게 할 수밖에 없는 것이, 관련 변수를 Subscriber 내부에 포함시킨다면, 매 Subscriber 실행시 마다 변수가 초기화되므로 Throttle 의 목적에 위배되는 결과를 낳을 것입니다.

  • eager execution 이라는 것은 emit 메서드가, 해당 이벤트에 등록된 Subscriber들을 순회하는 그 자체의 연산을 의미합니다.
  • eager execution 을 한다는 말 자체에는 그다지 깊은 뜻은 가지고 있지 않습니다. Observable 과 비교하기 위한 말 입니다만, 간단히 말씀 드리자면 "나는 Subscriber가 뭐가 있든간에 관심없고, 이벤트만 emit 할 것이다. 등록된 이벤트가 있었다면 땡큐? 아님말고." 라는 단지 관심사적인 뉘앙스의 차이일 뿐입니다. 그에관한 코드 예시가 필요하다면 node EventEmitter 설명 의 맨 하단을 참조해주세요. (이 포스팅의 후반부를 다 읽는다면 이해가 되실겁니다.)

이러한 로직을 다른 이벤트에 아래와 같이 재사용 하고자 하는 경우, 문제가 발생합니다.

let count = 0;
let rate = 1000;
let lastEvent = Date.now() - rate;

function countWithTrottle() {
  if (Date.now() - lastEvent >= rate) {
    console.log(`Clicked ${++count} times`);
    lastEvent = Date.now();
  }
}

document.addEventListener('click', countWithTrottle);
document.addEventListener('keypress', countWithThrottle);

결과는 뻔하지요. lastEvent 변수, count 변수가 공유되어 말도 안되는 결과가 발생할 것입니다.

이를 해결하기 위해서 EventEmitter 사용시에는 클로저를 사용하여 사이드이펙트를 분리시켜야 할 것입니다.

function getThrottleCounter(rate) {
  let count = 0;
  let lastEvent = Date.now() - rate;
  
  return function () {
    if (Date.now() - lastEvent >= rate) {
      console.log(`Clicked ${++count} times`);
      lastEvent = Date.now();
    }
  }
}

document.addEventListener('click', getThrottleCounter(1000));
document.addEventListener('keypress', getThrottleCounter(1000));

실행컨텍스트의 분리로 인해 사이드이펙트가 공유되지 않지만, 함수 호출시 매번 똑같은 함수가 메모리에 적재된 것도 비효율 적일 뿐더러 코드가 직관적이지 못합니다.

이제 이러한 불편함을 RxJS 에서는 어떻게 해결하였는지 살펴보도록 하겠습니다.

RxJS Observable 예시

fromEvent(document, 'click')
  .pipe(
    throttleTime(1000),
    scan(count => count + 1, 0)
  )
  .subscribe(count => console.log(`Clicked ${count} times`));

fromEvent(document, 'keypress')
  .pipe(
    throttleTime(1000),
    scan(count => count + 1, 0)
  )
  .subscribe(count => console.log(`Clicked ${count} times`));

throttleTime 연산자로 인해 throttle 의 구현도 편해졌을 뿐더러, scan 연산자로 인해 자신만의 상태를 encapsulation 하여 사이드이팩트 분리도 매우 간단해졌습니다.

또한 공통된 모듈을 Observable 로, Observable이 emit 한 값을 사용하는 부분을 Observer(Subscriber) 로 분리함으로써 Subscriber가 Consumer로서의 역할만 수행하는데 집중할 수 있게 되었습니다.

여기서 유지보수 하는 방법은 pipe 메서드 내부에서 scan 위의 throttle 함수 부분부터 Custom Operator로 정의하는 방법이 있겠습니다. 지금은 소스의 양이 많지 않으므로 그대로 두도록 합니다.

Observable 은 lazy execution 을 수행합니다. 그 이유는, subscribe() 를 하고 나서야 비로소 Observable 이 연산을 수행하고 값을 Subscriber 에 전달하기 때문입니다. EventEmitter 가 자신이 직접 먼저 emit 을 하는 것과는 엄연히 다른 동작을 수행합니다.

RxJS 를 통해 개발자의 숙원사업인 관심사의 분리, 최적화에 좀 더 다가갈 수 있게 되어 매우 뜻깊은 하루였네요. 읽어주셔서 감사합니다!

profile
지식을 주도하는 법을 터득하는중..

0개의 댓글