본문 바로가기

Prog. Langs & Tools/RxJS

[RxJS] Ch04. RxJS에서의 시간 - 상

이번 포스팅에서는 RxJS에서 시간에 대한 개념을 정리해 보고자 한다. 왜 우리가 시간을 신경써야 하는지, RxJS에서는 시간을 어떻게 처리하는지 등에 대해서 알아보고자 한다.

 

앞서 우리가 살펴본 바에 따르면 옵저버블은 시간 경과에 따른 이벤트의 무한 시퀀스이다. 동기적인 코드라면 실행 시간을 정확하게 측정하여 예측할 수가 있지만 비동기는 명령들이 선형적으로 실행이 되지 않기 때문에 실행 시간을 정확하게 알 수 없고 예측은 더더욱 할 수가 없다. 따라서 특정 작업이 언제 완료될지를 추측하는 것이 아닌, 작업에 반응한다는 방식으로 접근해야 한다.

 

왜 시간을 신경써야 할까

시간을 신경써야 하는 이유는 단순하다. 시간이 오래 걸리면 사용자는 초조함을 느끼고 결국에는 떠난다. 따라서 우리는 시간을 최대한 단축할 수 있는 데까지 단축해야 한다. 어플리케이션 시간에 영향을 미치는 요인에는 네트워크 속도, 브라우저 환경, 애니메이션, 이벤트 등등 여러가지가 있다.

이러한 이유로 순수함수의 관점에서 본다면 시간 기반 함수는 외부 상태에 직간접적으로 의존하기 때문에 순수함수로 볼 수가 없다. 그럼에도 불구하고 RxJS는 이러한 문제들을 연산자 체인을 통해 순차적인 동기 함수의 형태로 실행시켜서 시간에 따른 부가작용을 최소화한다. 이번 포스팅에서는 RxJS에서 시간을 검사하고 조작할 수 있게 제공하는 여러가지 도구들에 대해서 살펴보고자 한다.

 

자바스크립트의 비동기 타이밍 이해하기

앞서 언급한 것처럼 비동기 어플리케이션의 실행 시간은 우리의 통제 영역을 벗어난다. 주요한 문제들은 다음과 같다.

  • 모호하다. 언제든지 일어날 수도 일어나지 않을 수도 있다.
  • 조건부이다. 이전 작업이 올바르게 실행되었는지 여부에 따라 이후 실행 결과가 좌우된다.

RxJS는 이러한 비동기 작업을 마치 동기적인 것처럼 처리한다. 한 코드는 다른 코드가 완료된 후에만 실행되게 작업을 직렬화하도록 설계했다. 자바스크립트에서 이러한 비동기 처리는 크게 두 가지 방식으로 나뉘는데 첫 번째는 암시적 타이밍이고, 두 번째는 명시적 타이밍이다. 첫 번째의 경우 우리가 익숙한 Node.js의 I/O API나 클라이언트 AJAX API가 콜백 함수 형태의 암시적 타이밍으로 처리된다. 콜백 함수가 마치면 그를 이어서 바로 다음 함수가 실행이 되고 이는 마치 달리기 계주에서 바톤을 주고받는 것과 비슷하다.

두 번째의 경우 다음과 같은 특성을 가지고 있다.

  • 구체적: 정해진 시간에 발생
  • 명시적: 사용자가 명확하게 정의하고 제어할 때 발생
  • 무조건적: 에러가 발생하지 않거나 스트림이 취소되지 않는 한 항상 발생

명시적으로 시간이 지정된 작업은 보통 사용자 중심의 경우와 리소스 중심의 경우가 있다. 전자의 경우는 애니메이션, 대화 상자, 유효성 검사 등이 예시가 될 수 있다. 후자의 경우는 네트워크 I/O 작업, 빠른 사용자 입력, CPU 집약적 계산등이 포함이 될 수 있다. 일반적으로 명시적 타이밍은 컴퓨팅에서 유리하다. 작업 순서에 영향받지 않고 코드를 원할 때 실행하고 제어하는 것이 가능하기 때문이다.

자바스크립트에서는 대표적으로 setTimeout(), setInterval() 두 개의 타이밍 인터페이스를 제공한다.

 

setTimeout

setTimeout() 함수(전역 컨텍스트 객체의 메서드)는 명시적으로 일회성 작업을 now로부터 상대적인 미래의 특정 시점에 실행되게 설정한다. setTimeout() 함수를 호출하면 몇 밀리초 후에 코드를 실행해야 함을 자바스크립트 런타임에 알린다. setTimeout() 함수는 콜백이 미래에 정확히 한 번 호출된다는 점에서 프로미스와 유사하다.

그리고 setTimeout()은 각 구독자에게 일회성으로 방출되는 간단한 옵저버블이다. 즉, setTimeout()으로 래핑된 옵저버블을 생성할 수 있다. 아래 예제를 한 번 살펴보자.

const source$ = Rx.Observable.create(observer => { // 옵저버블 팩토리 메서드 안에 모든 것을 래핑
  const timeoutId = setTimeout(() => {
    observer.next();
    observer.complete(); // 구독이 발생하고 1초 후 단일 next 함수와 완료 플래그 보냄
  , 1000);
  return () => clearTimeout(timeoutId); // 구독 취소 동작을 정의
});

source$.subscribe(() =>  // 타이머를 시작하기 위해 옵저버블을 구독
  document.querySelector('#panel').style.backgroundColor = 'red'); // CSS 연산자 실행
  
  
// RxJS의 timer 연산자를 사용하면 같은 기능 구현을 더 간결하게 아래와 같이 할 수 있다.
Rx.Observable.timer(1000)
  .subscribe(()=>
    document.querySelector('#panel').style.backgroundColor = 'red');

예제를 보면 비즈니스 로직에 얽매이지 않고 1초 후에 한 번 실행되고 나서 완료되는 옵저버블을 생성한다. 그리고 이 옵저버블은 clearTimeout(timeoutId)을 취소 로직으로 다시 전달하여 타이머의 구독 취소 메커니즘을 제공한다. 그리고 RxJS에서 이는 timer() 연산자를 통해 지정된 시간이 지나면 단일 이벤트를 방출하는 옵저버블 객체를 만들 수가 있다.

 

setInterval

앞에서 살펴본 setTimeout()은 Promise와 유사하지만, setInterval()은 밀리초 단위로 지정된 시간에 여러 이벤트를 만들 수 있다는 점에서 이벤트 이미터와 유사하다. 이 함수는 하나의 연산을 수행하지 않고 호출 간격을 지정하여 연산을 반복적으로 호출한다.

아래 예제는 2초 간격으로 결과가 나오는 간단한 옵저버블을 생성한다. 이 코드는 2초 후에 숫자를 세는 자바스크립트의 명시적 setInterval() 함수로 옵저버블 객체를 래핑한다. 8초가 지나면 구독이 취소되고 4개의 이벤트가 생성된다. 구독이 취소되었기 때문에 옵저버의 완료 함수도 생략된다.

const source$ = Rx.Observable.create(observer => {
  let num = 0;
  const id = setInterval(() => {
    observer.next(`Next ${num++}`);
  }, 2000); // 2초마다 다음 숫자를 출력한다.
  return () => { // 반환된 객체는 이 옵저버블 객체에 대한 구독 취소 로직이 포함된다.
    clearInterval(id);
  }
});

const subscription = source$.subscribe(
  next => console.log(next), // next 처리
  error => console.log(error.message),
);

setTimeout(function () { // 8초 후에 이벤트를 취소한다.
  subscription.unsubscribe();
  console.log('Done!') // 스트림이 완료되면 플래그를 출력한다.
}, 8000);

// 실행 결과
// Next 0
// Next 1
// Next 2
// Next 3
// Done!

 

RxJS로 시간 다루기

RxJS에서 시간은 함수들은 여러가지 방식이 있다. 먼저 정적 메서드인 interval()과 timer()는 앞서 살펴보았던 setInterval()과 setTimeout() 메서드와 유사한 기능을 한다. 이러한 메서드들은 다른 팩토리 메서드와 동일한 방식으로 작동하지만, 대부분 제공된 시간이 경과한 후 이벤트를 방출한다.

이러한 타이밍 연산자는 옵저버블 스트림과 결합하면 옵저버블에서 데이터를 소비하는 빈도를 타이머로 동기화 할 수 있어서 매우 강력하다. 아래의 예제는 2초 간격으로 작동하는 가상의 주식 시세를 표시하는 시뮬레이션이다. 2초 간격은 옵저버블 스트림으로 전달되는 알림의 빈도이며, 구독자는 각 값을 받고 DOM을 업데이트 한다. 이처럼 RxJS의 시간 연산자로 연산자가 속해있는 스트림의 진행을 제어할 수 있다.

const newRandomNumber = () => Math.floor(Math.random() * 100);
const USDMoney = Money.bind(null, 'USD');

Rx.Observable.interval(2000)
  .skip(1) // 첫 번째 방출 숫자에서 0 생략
  .take(5) // 무한 연산자이므로 5개만 시뮬레이션하게 설정
  .do(int =>
    console.log(int.interval)) // interval 속성은 한 간격과 다음 간격 사이의 경과 시간(밀리초)을 나타낸다.
  .map(int => new USDMoney(newRandomNumber())) // USDMoney 값 객체의 값 속성은 옵저버블에 의해 방출된 간격의 수를 반환한다.
  .map(usd => usd.toString())
  .forEach(console.log);

명시적 타이밍과 암시적 타이밍은 상호 배타적이지는 않다. 하지만 이 둘을 함께 사용하려면 시간이 다운스트림 연산자에 전파되는 방식을 이해해야 한다. 명시적 타이밍의 경우 순서와 관련된 문제가 생길 수 있다. 예를 들면 delay() 연산자를 도입할 때를 생각해 보자. 이벤트가 delay() 1초 후에 도착하고 지연이 2초라면 이벤트는 3초 후 구독 메서드로 방출된다. 이를 다이어그램으로 나타내면 다음과 같다.

<RxJS 반응형 프로그래밍> p132

여기서 알 수 있는 점은 첫 번째로 각 연산자는 이벤트 생성이 아닌 이벤트 전파에 영향을 미친다는 것이고, 두 번째로 시간 연산자는 순차적으로 동작한다는 점이다. 지금부터는 이 두가지의 관점에서 이야기를 해보려고 한다.

 

전파

연산자들은 자신이 속해있는 옵저버블을 알지 못하므로 이벤트 생성에 영향을 미치지 않는다. 각 연산자들이 역할을 나누어서 서로서로 독립적으로 진행하기 때문이다. 이렇게 분리가 되었을 때 문제가 생기는 경우는 하나의 작업 시간이 다른 작업 시간과 크게 차이가 나는 경우이다. 예를 들어 어떤 배열에 대해 한꺼번에 지연이 발생하는 옵저버블을 살펴보자.

Rx.Observable.of([1, 2, 3, 4, 5])
  .do(x => console.log(`Emitted: ${x}`)) // 효과적인 계산을 위해 .do() 연산자를 사용하는데, 이 경우 방출된 데이터를 콘솔에 출력
  .delay(200) // 200밀리초만큼 이벤트 전파를 지연
  .subscribe(x => console.log(`Received: ${x}`));

어떤 결과가 예상이 되는가? 실제 결과는 아래와 같다.

// "Emitted: 1,2,3,4,5"
// 200 ms 후
// "Received: 1,2,3,4,5"

이벤트 생성은 delay() 연산과 별개이다. 따라서 이러한 예시는 생산과 전파가 일치하지 않는 경우이다. 결론적으로 지연이 작동하려면 받은 이벤트를 적절한 시간에 방출하기 전에 버퍼링 해야 한다. 대부분의 경우는 연산자가 전파하는 한 버퍼는 상한을 항상 유지하며 일정 크기 이상으로 커지지가 않는다.

 

순차적인 시간

연산자를 체인화하면 항상 순차적으로 작동하여 체인 앞부분의 연산자들이 체인 뒷부분의 연산자들보다 먼저 실행된다. 시간 기반 연산자도 이렇게 될 것이라 예상할 수 있다. 물론 delay()가 순차적이기는 하지만, 다른 비시간 연산자와 관련해서 스트림 선언에 나타나는 대로 실행되지 않으므로 혼동이 될 때가 있다. 아래의 예제에서 우리는 각각의 요소 쌍 [1,2]. [3,4], [5,6]이 2초 간격으로 방출된다고 예상할 수도 있다.

Rx.Observable.from([1, 2])
  .delay(2000)
  .concat(Rx.Observable.from([3, 4]))
  .delay(2000)
  .concat(Rx.Observable.from([3, 4]))
  .delay(2000)
  .subscribe(console.log);

하지만 실제로 결과를 보면 6초 경과 후 [1, 2, 3, 4, 5, 6]을 출력한다.

<RxJS 반응형 프로그래밍> p135

 

참고자료

  • <RxJS 반응형 프로그래밍> 폴 대니얼스, 루이스 아텐시오 저