Skip to content

도서 리뷰 시리즈 - RxJs 퀵스타트

출처

『RxJs 퀵스타트』 | 손찬욱 저 | 루비페이퍼 | 2018년 07월 24일

한 줄 리뷰

RxJs를 시작할 때 꼭 봐야 하는 책

요약

서론

책을 쓰면서 RxJS는 왜 만들어졌고, 또 어떤 철학 배경으로 RxJS가 탄생했는지를 찾아가다 보니 결국 RxJS를 어떻게 써야 하는 지 자세히 알게 되었다. 또한 RxJS를 왜 써야 하는 지도 더욱 분명해졌다.

개발을 처음 시작할 때는 요구사항을 어떻게 구현할지 구현 자체에만 초점을 맞추게 된다. 하지만 경험이 쌓이기 시작하면 구현보다는 얼마나 효과적으로 유지보수할 수 있을 지 또는 얼마나 많은 문제점을 설계나 테스트 코드를 통해 사전에 해결할 수 있을 지 고민하게 된다. 그래서 우리는 자연스레 기존 문제들을 해결하기 위해 고심했던 라이브러리, 디자인 패턴들을 하나둘씩 적용하게 되고, 결국에는 이런 고민들이 녹아들어 간 프레임워크에 관심을 갖게 된다.

프레임워크를 잘 사용하는 것도 중요하지만 그 기술의 결과물이 어떤 고민의 산물인지 아는 것이 더 중요하다. 고민의 원인을 알게 되면 그 기술을 보다 깊게 이해할 수 있고 폭넓게 활용할 수 있다.

웹 애플리케이션은 상태 머신이다.

상태 머신이란 주어진 시간의 상태가 존재하고, 어떤 한 사건에 의해 다른 상태로 변할 수 있는 수학적 모델을 의미한다.

웹 애플리케이션 오류가 발생하는 경우

사용자의 입력에 따라 프로그램이 예상하는 결과를 얻지 못하는 것을 우리는 프로그램 오류라고 이야기한다. 웹 애플리케이션을 하나의 상태 머신으로 본다면 정확한 입력과 로직으로 상태를 관리하는 방법이 중요하다.

입력 오류

각 구성요소에게 정확한 입력값을 전달하는 문제는 단순히 값을 전달하는 것이기 때문에 쉽게 생각할 수 있지만 쉬운 문제만은 아니다. 서버에서 전달받은 응답 결과값이 정상적으로 왔을 경우에만 입력값을 전달해야 한다. 비정상적인 응답 데이터를 입력값으로 전달하면 오류가 발생할 수 있다.

상태 오류

상태 오류가 발생하는 이유는 상태 변화를 정확하게 전달하지 못하는 경우이다. 구성요소 간 의존도가 있는 경우, 구성요소 간 호출 순서에 의존도가 있는 경우 프로그램은 오류에 직면할 수 있다.

로직 오류

가장 간단하면서도 빈번하게 발생하는 오류는 분기문, 변수에 따른 오류이다.

분기문이 많으면 많을수록 프로그램 흐름이 복잡해진다. 더군다나 중첩 분기문이 오류에 직면할 확률을 높인다.

또한 변수를 많이 사용하면 할수록 로직 오류는 빈번히 발생할 수 있다. 변수는 우리가 의도치 않게 변경할 수 있으며 또한 누군가에 의해 변경될 수 있기 때문에 많은 변수의 사용은 프로그램의 복잡도와 오류 발생률을 높인다.

RxJS 가 해결하려고 했던 문제

입력 데이터의 오류

입력 데이터의 전달 시점은 다양하다. 동기비동기 로직을 함께 사용하여 동작 순서를 보장하기 위해 우리는 많은 작업을 하고 있다.

RxJS는 이런 구조적인 문제를 개선하기 위해 단 하나의 방식을 사용할 수 있는 구조를 제공한다. 이런 구조의 일원화는 개발을 단순화시킨다. 이런 단순화는 결국에는 오류 발생 빈도를 낮추고, 생산성 향상에 도움을 준다.

동기와 비동기의 차이점을 시간이라는 개념을 도입함으로서 해결하려고 한다. 동기와 비동기는 시간의 축으로 봤을 때는 같은 형태이다. 또한 이런 형태는 시간을 인텍스로 둔 컬렉션으로 생각할 수도 있다. RxJS에서는 이를 스트림(Stream)이라 표현한다. 이런 스트림을 표현하는 Observable클래스를 제공한다.

Observable은 시간을 인덱스로 둔 컬렉션을 추상화한 클래스이다. 이 클래스는 동기비동기의 동작 방식으로 전달된 데이터를 하나의 컬렉션으로 바라볼 수 있게 해준다. 이렇게 함으로써 개발자는 데이터가 어떤 형태로 전달되는지에 대해 더이상 고민할 필요가 없어진다.

fromEvent, from, of 함수는 Observable 생성자를 이용하여 만든 팩토리 메소드이다.

js
const {fromEvent, from, of} = rxjs
const key$ = fromEvent(document, 'keydown')
const arrayFrom$ = from([10, 20, 30])
const numberOf$ = of(10, 20, 30)

상태 전파 문제

웹 애플리케이션의 상태 변화로 인한 문제점은 크게 세가지가 있다.

  1. 인터페이스 변경되면 함께 변경해야 한다.
  2. 상태를 확인하기 우해 인터페이스에 대한 의사소통 비용이 발생한다.
  3. 다수가 A라는 한 클래스에 의존 관계가 있는 경우 A의 변경 여부를 반영하기 위해 다수에 A의 상태를 모두 반영해야 한다.

이러한 문제를 해결하기 위해 우리가 이미 알고 있는 솔루션 옵서버 패턴을 사용한다. 느슨한 결함, 자동 상태 전파, 인터페이시스의 단일화 할 수 있다.

js
class Subject {
  add(observer) {}
  remove(observer) {}
  nofity(observer) {}
}

class Observer {
  update(status) {}
}

하지만 RxJS는 옵서버 패턴의 아쉬웠던 몇 가지를 개선하였다.

  1. 상태 변화는 언제 종료되는 지
  2. 상태 변화에서 에러가 발생할 경우 => 인터페이스의 확장을 통해 종료시점과 에러발생을 해결했다.
  3. Observer에 의해 Subject 상태가 변경되는 경우 => Read-Only와 단방향 데이터 흐름으로 해결
js
class Observable {
  subscribe(observer) {}
}

class Observer {
  next(status) {}
  error(error) {}
  complete() {}
}

리액티브 프로그래밍은 데이터 흐름과 상태 변화 전파에 중점을 둔 프로그램 패러다임이다. 사용되는 프로그래밍 언어에서 데이터 흐름을 쉽게 표현할 수 있어야 하며 기본 실행 모델이 변경 사항을 데이터 흐름을 통해 자동으로 전파한다는 것을 의미한다.

로직 오류

웹 애플리케이션은 전달받은 입력값을 로직을 통해 새로운 결과를 반환하거나 표현한다. 로직은 산술적인 로직이나 비즈니스적인 로직이 될 수 있다. 또는 if문과 같이 간단한 프로그램의 흐름을 담당하는 부분일 수도 있다.

RxJS은 오퍼레이터는 항상 새로운 Observable을 반환함으로써 불변 객체를 반환한다. 불변 객체는 생성 후 그 상태를 바꿀 수 없는 객체이다. 불변 객체는 외부에서 값을 변경할 수 없기 때문에 불변 객체를 사용하는 것만으로도 프로그램의 복잡도가 줄어들 수 있다.

로직상에 존재하는 반복문, 분기문, 변수를 제거하기 위해서 함수형 프로그래밍 개념을 근간으로 하는 오퍼레이터를 제공한다. Observable이 제공하는 오퍼레이터를 통해 생성, 변환, 병합, 분리와 같은 다양한 연산을 적용할 수 있으며 항상 Immutable Object를 반환한다.

불변 객체 Observable

불변 객체(Immutable Object)은 생성 후 그 상태를 바꿀 수 없는 객체이다. 불변 객체는 외부에서 값을 변경할 수 없기 때문에 불변 객체를 사용하는 것으로도 프로그램의 복잡도가 줄어들 수 있다.

Observable은 새로운 Observable을 만들고 그 Observable이 오퍼레이터를 호출한 원래의 Observable을 내부적으로 구독한다. 즉, 링크드 리스트 형태로 기존 Observable 객체와 새롭게 만든 Observable 객체를 오퍼레이터로 연결한다.

map 오퍼레이터는 다음과 같은 원리로 구현되었다. 실제 구현은 lift 함수를 이용하여 이전 Observable과 연결하는 방식을 사용한다.

js
const map = function (transformationFn) {
  const source = this
  const result = Observable(observer => {
    source.subscribe(
      (x) => { observer.next(transformationFn(x)) },
      (err) => { observer.error(err) },
      () => { observer.complete() }
    )
  })
  return result
}

RxJS란?

RxJS의 공식 사이트에서는 다음과 같이 정의하고 있다.

RxJS를 Observable를 사용하여 비동기 및 이벤트 기반 프로그램을 작성하기 위한 라이브러리이다.

만약에 RxJS가 어렵다면 비동기 컬렉션 데이터를 다루는 라이브러리 정도로 생각하고 접근해보자.

RxJs시작하기

이벤트 핸들러를 만들고 그 핸들러를 addEventListener를 통해 등록하기만 하면 우리가 원하는 코드를 작성할 수 있다.

js
const eventHandler = event => console.log(event.currentTarget);
document.addEventListener('click', eventHandler);

이 코드와 동일한 기능을 RxJS로 작성해 보자.

js
const { fromEvent } = rxjs;
const click$ = fromEvent(document, 'click');
const observer = event => console.log(event.currentTarget);
click$.subscribe(observer);

이벤트 핸들러를 등록하는 것은 동일하지만 다른 점이 있다면 브라우저를 통해 전달되는 이벤트 정보를 Observable로 변환하는 작업을 추가로 한다는 점이다.

RxJS 스케줄러

RxJS에서 자바스크립트의 비동기 작업을 효과적으로 처리할 수 있도록 도와주는 역할을 한다.

자바스크립트 엔진

자바스크립트 엔진은 기본적으로 하나의 스레드에서 동작한다. 하나의 스레드는 하나의 스택을 가지고 있다는 의미하고, 동시에 단 하나의 작업만을 할 수 있다는 의미이다. 그 비밀은 이벤트 루프와 큐에 있다.

이벤트 루프와 큐

이벤트 루프는 계속 반복해서 콜 스택과 큐 사이의 작업을 확인한다. 콜 스택이 비워 있는 경우 큐에서 작업을 꺼내어 콜 스택에 넣는 다.

콜 스택에 작업이 없을 경우 우선적으로 마이크로태스크 큐를 확인한다. 마이크로테스크에 작업이 있다면 작업을 꺼내서 콜 스택에 넣는 다. 만약 마이크로테스크 큐가 비어서 더 이상 처리할 작업이 없으면 태스크 큐를 확인한다. 태스크 큐에 작업이 있다면 작업을 꺼내서 콜 스택에 넣는 다.

자바스크립트 처리 과정

  1. 비동기 작업으로 등록되는 작업은 Task와 Microtask 그리고 AnimationFrame 작업으로 구분된다.
  2. Microtask는 Task보다 먼저 작업이 처리된다.
  3. Microtask가 처리된 이후 requestAnimationFrame이 호출되고 이후 브라우저 렌더링이 발생한다.
js
console.log('script start')

setTimeout(() => console.log('setTimeout'), 0)

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'))

requestAnimationFrame(() => console.log('requestAnimationFrame'))

console.log('script end')
$ script start
$ script end
$ promise1
$ promise2
$ requestAnimationFrame
$ setTimeout

RxJS 스케줄러와 자바스크립트 비동기 작업의 종류

  1. 태스크
    • 비동기 작업을 순차적으로 수행될 수 있도록 보장하는 형태의 작업 유형
    • RxJS에서 asyncScheduler 스케줄러를 이용하여 구현
    • asyncSchedulersetInterval로 구현됨
  2. 마이크로태스크
    • 비동기 작업이 현재 실행되는 자바스크립트 바로 다음에 일어나는 작업
    • 태스크보다 항상 먼저 실행
    • MutationObserverPromise가 해당
    • RxJS에서 asapScheduler 스케줄러는 이용하여 구현
    • asapSchedulerPromise로 구현됨

Pull과 Push가 가지는 의미

Pull 방식은 개발자가 주도적으로 데이터의 상태를 확인하고 관리할 수 있다. 이 방식은 개발자가 데이터의 변경 여부를 주기적으로 확인해야 하며 개발자가 많은 것을 고려해서 처리해야 한다.

반면, Push 방식은 데이터를 전달하는 주체의 상태에 관심을 둘 필요가 없으며, 내가 관심있는 데이터에 한정하여 관리할 수 있다. 이 방식의 가장 큰 장점은 데이터의 거부권을 가지고 있다는 점이다.

Push 방식은 데이터 처리에 따른 오류 처리가 필요 없다

Push 방식은 데이터가 전달되었을 때 처리되면 되기 때문에 대전체는 데이터가 존재한다 이다. 따라서 데이터가 없을 경우에 데이터를 다시 조회한다는지 또는 다른 데이터를 제공하는 등에 대한 별도의 오류 처리를 할 필요가 없다.

Push 방식은 데이터가 전달되었을 때 바로 처리하기 때문에 Pull 방식보다 빠르게 데이터의 변경에 반응할 수 있다.

또한 데이터의 변경에 따라 자동으로 다른 객체에 데이터를 전달할 수도 있다.