Rxjs 한번 배워보실래요?

teo.v·2022년 10월 9일
119

테오의 프론트엔드

목록 보기
34/48
post-thumbnail

나: "그래서 RxJs를 대체 할 만한게 있을까요?"
크루: "솔직히 비동기나 시간을 다루는 데에는 Rxjs를 대체 할 만한게 없긴 하죠. 진짜 좋다고 생각해요. ... 배우기 어려워서 그렇지. 웬만한 개발자들은 배워야 할 이유를 납득하는 것 부터 어려울걸요?"

- 함께 RxJs를 쓰고 있는 개발자 크루와의 대화 중에서...

프롤로그

오늘 써볼 이야기는 Rxjs 라이브러리입니다. 제가 블로그 글을 쓰면서 반응형 프로그래밍이라던가 함수형 프로그래밍을 설명하면서 한번씩 언급했던 그 Rxjs입니다.

자바스크립트에서 시간과 비동기를 다루는 방법은 어려운 고급스킬에 속해있습니다. 개인적으로 Rxjs는 이러한 비동기를 다루는 데 있어서 탁월하며 새로운 패러다임을 알려주는 좋은 라이브러리라고 생각을 합니다.

하지만 단순히 특정 기능을 쉽게 사용할 수 있게 하는 유틸성 라이브러리가 아니라 개발과 비동기를 바라보는 패러다임을 바꿔야 하는 만큼 선행해야할 학습으로 인해 진입 장벽이 굉장히 높은 편에 속합니다.

그래서 이번 글에서는 Rxjs를 배우고 있거나 Rxjs가 처음이신 분들을 위해서 어려운 개념들에 대해 조금 더 이해를 높일 수 있기 위해 중요하다고 생각했던 내용들을 한번 적어보았습니다.

Rxjs의 강의나 교과서적인 내용보다는 개인적으로 Rxjs를 이해하고 나서 알게 된 인사이트를 바탕으로 재해석해서 작성하는 글이라 지금 Rxjs를 공부하는 사람에게 조금 더 와닿을 것 같아요. Rxjs를 아예 모르는 분들은 그냥 흥미로 읽어주셔도 좋을 것 같습니다.

이번 글은 크게 2가지로 나눠서 적어보았습니다. 1부에서는 Rxjs가 뭔지 개론을 설명하며 왜 배우면 좋을지 희망을 얘기하고 2부에서는 실제 프로젝트에 적용을 하면서 어려웠던 개인적인 현실에 대해 이야기합니다.


1부. Rxjs 맛보기 - 희망편

RxJs가 뭔가요?

Rxjs의 공식 홈페이지로 들어가서 한번 RxJs의 정의를 한번 들여다봅시다.
https://rxjs.dev/guide/overview

언제나 공식문서에서 적혀있는 첫번째 정의는 실제 정의(?)가 아니라 배워야 할 목차와 키워드와 같은거라고 했습니다. 중요한 단어들만 그냥 체크하는 수준에서 넘어가고 언제나 새로운 것을 공부할때에는 일단 대충 이해하고 내가 알고 있는 내용을 바탕으로 대략적인 감을 잡아보도록 합시다.

keywords: 관찰가능한 시퀸스, 비동기, 이벤트, Observable, Observer, Subjects, 메소드, 연산자, Observer 패턴, Iterator 패턴, 함수형 프로그래밍, 컬렉션

일단 쉽게 생각해보자구요! - Think of RxJS as Lodash for events.

Think of RxJS as Lodash for events.
- RxJs를 이벤트용 Lodash 로 생각하십시오.

이 공식문서를 만드는 작성자도 정의를 쓰다가 이렇게 쓰면 이해하기 어려울거라고 생각했는지 중간에 Think of RxJS as Lodash for events. 라고 힌트를 주었습니다.

하지만 underscore.js혹은 lodash는 요새는 많이들 쓰는 라이브러리가 아니니 생소하신 분들도 있을 것 같습니다.

그래서 저는 RxJs를 다음과 같이 일단 정의하려고 합니다.

이벤트나 비동기, 시간을 마치 Array 처럼 다룰 수 있게 만들어 주는 라이브러리

이 말도 당장은 이해가 되지 않을 것이기에 지금까지의 키워드와 내용들을 잠시 머리속에 넣어두고 예시를 통해 살펴보도록 하겠습니다.

예시를 통해 Rx를 이해해봅시다.

다음과 같은 두수로 이루어진 배열이 있습니다.
이 중 1사분면에 있는 점을 5개만 골라 각 점과 원점과의 거리의 합을 구하는 코드를 작성해 보시오.

const points = [ [1,-1], [5,10], [10,-2], [-3,-5], [-10,9], ... ]

코딩테스트의 초급 수준의 문제를 하나 만들어보았습니다.
해당 문제를 해결하기 위해서 다음과 같은 코드를 어렵지 않게 작성할 수 있을 것 같습니다.

let sum = 0
let count = 0

for (let i = 0; i < points.length; i++) {
  const pos = points[i];
  const [x, y] = pos;

  // 1사분면에 있는 값만,
  if (x > 0 && y > 0) {
    // 원점과의 거리의 합을 더해서,
    sum += Math.sqrt(x*x + y*y)
    count++
  }

  // 5개면 그만,
  if (count === 5) {
    break;
  }
}

console.log(sum)

우리가 조금 더 자바스크립트 프로그래밍을 배우다보면 같은 문제를 조금 더 고급스럽게(?) 풀 때, for문을 이용해서 프로그램을 작성하기 보다는 ArrayMethod를 이용하라고 권장합니다.

그래서 다음과 같이 ArrayMethod와 함수를 활용해서 작성을 하면 훨씬 더 간결하고 라인별 의미가 분명해지고 재사용하기 용이한 코드가 만들어지게 됩니다.

const sum = points // 점들 중에서
  .filter(([x, y]) => x > 0 && y > 0) // 이중 1사분면에 있는 값을 추려내
  .slice(0, 5) // 5개만 골라서
  .map(([x,y] => Math.sqrt(x*x + y*y)) // 원점과의 거리들의
  .reduce((a, b) => a + b) // 총합

이러한 방식을 우리는 선언적 프로그래밍이라고 부릅니다.

이런 식으로 데이터 객체의 메소드 함수를 조립해서 이렇게 데이터가 파이프라인으로 연결되는 마치 함수형 프로그래밍과 같은 방식으로 작성할 수 있습니다. 위 코드는 객체 지향과 함수형 프로그래밍이 적절히 잘 섞여 있는 매우 좋은 자바스크립트스러운 방식입니다.

우리가 이런 코드를 보통 좋은 코드라고 하죠.

Level up!

레벨업을 해봅시다 다 똑같은 문제입니다. 조금 더 차원을 높여봅시다.

위와 로직은 동일합니다. 1사분면에 있는 점을 5개만 골라 각 점과 원점과의 거리의 합을 구하는 코드를 작성해 보시오. 단 입력이 배열이 아니라 마우스를 통한 입력으로 받아보겠습니다.

points = [... click, ... click, ...click, ...click, ...]

어렵지 않죠? 아까 만들어두었던 코드에 적당히 이벤트 처리를 통해서 간단히 다음과 같이 코드를 작성해 볼 수 있겠습니다.

let sum = 0
let count = 0

window.onclick = function(event) {
  const [x, y] = [event.pageX - screen.width/2, event.pageY - screen.height/2]

  // 1사분면에 있는 값만,
  if (x > 0 && y > 0) {
    // 원점과의 거리의 합을 더해서,
    sum += Math.sqrt(x*x + y*y)
    count++
  }

  // 5개면,
  if (count === 5) {
    window.onclick = null // 이벤트를 더 이상 입력받지 않도록 한다.
    console.log(sum)
  }
}

그런데...

우리는 앞서 배열에서와 같이 선언적으로 함수형 프로그래밍처럼 작성을 하는게 더 나은 방법이라고 배웠습니다. 그렇다면 지금과 같이 이벤트를 다루는 경우에도 선언적으로 작성할 수는 없을까요?

// 선언적으로 코드를 짜면 훨씬 더 간결해질텐데 배열이 아니라 이벤트라서... 🤔
// clicks:??? = [... click, ... click, ...click, ...click, ...]

const sum = clicks
  .map(event => [event.pageX - screen.width/2, event.pageY-screen.height/2])
  .filter(([x, y]) => x > 0 && y > 0) // 1사분면
  .slice(0, 5) // 5개만
  .map(([x,y] => Math.sqrt(x*x + y*y)) // 원점과의 거리
  .reduce((a, b) => a + b) // 총합

아쉽게도 우리가 알고 있는 자바스크립트에서는 이러한 객체와 메소드는 존재하지 않습니다.

우리가 짜는 비동기 코드가 어려워지는 것은 비동기 코드는 선언적으로 작성하지 못하고 결국 이벤트와 시간을 다루기 위해서는 callbacksetTimeout등의 코드들로 절차식으로 작성을 해야만 하고 이는 곧 복잡한 코드를 만들어내기 때문입니다.

이벤트를 Array처럼 다룰 수는 없을까?

자바스크립트의 기본 객체에는 이러한 API가 없지만 뭔가 코드를 작성해보니 위와 같이 이벤트도 선언적으로 작성 못할 이유가 없어 보입니다.

points = [ [1,-1], [5,10], [10,-2], [-3,-5], [-10,9], ... ]

clicks = [... click, ... click, ...click, ...click, ...]

Event 역시 Array와 같이 같은 타입의 데이터를 여러개를 가지고 있습니다. 다만 미리 존재하고 있는 것이 아니라 비동기이며 아직은 존재하지 않을 뿐이죠.

그러나 우리에게는 callback이 있고 선언적 프로그래밍에서도 callback을 사용하기에 사실 같은 형식으로 작성을 할 수 있을 것 같습니다.

const points = clicks.map(event => [event.pageX - screen.width/2, event.pageY - screen.height/2])
                  
// 같은 코드네??
const sum = points // 점들 중에서
  .filter(([x, y]) => x > 0 && y > 0) // 이중 1사분면에 있는 값을 추려내
  .slice(0, 5) // 5개만 골라서
  .map(([x,y] => Math.sqrt(x*x + y*y)) // 원점과의 거리들의
  .reduce((a, b) => a + b) // 총합

그래서 이벤트를 다루는 새로운 객체인 Observable을 만들고 Array와 같은 메소드들을 추가하여 이벤트를 선언적으로 Array를 다루듯이 만들 수 있는 라이브러리인 Rx가 탄생하게 됩니다.

// click을 통해 Observable
const clicks:Observable<MouseEvent> = fromEvent(window, "click")

// fromEvent 코드는 어떻게 생겼을까?
const fromEvent = (target, type) => new Observable(observer => {
  target.addEventListener(type, (event) => observer.next(event))
  return () => target.removeEventListener(type)
})

Observable = Array와 Promise의 상위호환!

Observable를 통해서 우리는 여러가지의 Event들을 마치 Array처럼 다룰 수 있는 새로운 객체타입을 하나 얻게 되었습니다.

그러고보니 Observable을 생성하는 패턴은 Promise를 만들어내는 방식과 매우 흡사해보입니다. 사실 Observable은 Promise의 상위호환입니다. Promise로 만들 수 있는 값을 Observable 방식으로도 생성이 가능합니다.


// 통신이나 시간과 같은 비동기로직을 다루기 쉽게 만들어주는 Promise
// Promise는 비동기 로직을 값으로 만들 수 있다.
const fetchXXX = (props) => new Promise((resolve, reject) => {
  fetch(url, props).then(res => resolve(res), err => reject(err))
})

// Observable은 Promise 대신 쓸 수 있다.
const fetchXXX = (props) => new Observable(observer => {
  fetch(url, props).then(res => observer.next(res), err => observer.error(err))
})


// 반대로 Promise는 Observable가 될 수 없다.
const fromEvent = (target, type) => new Promise(resolve => {
  // Promise는 여러개의 값을 받을 수 없다.
  target.addEventListener(type, (event) => resolve(event))

  // Promise는 종료 시 cleanup을 할 수가 없다.
  return () => target.removeEventListener(type)
})

자바스크립트 세상에서는 Array는 비동기를 다룰 수는 없지만 여러개의 값을 다룰 수 있고, Promise는 비동기를 다룰 수 있지만 하나의 값만 다룰 수 있습니다. Observable은 이 2가지의 역할을 동시게 수행할 수 있습니다. 여러가지의 값을 다루면서 비동기를 다룰 수 있고 Array와 Promise와 같이 메소드를 가지고 있습니다.

이러한 성질을 가진 Observable객체를 통해서 우리는 비동기를 선언적인 방식으로 개발을 할 수 있게 됩니다. 더 자세한 내용은 다음 챕터에서 이어서 설명하겠습니다.

지금까지의 내용을 바탕으로 공식문서의 Rxjs의 정의를 제 방식대로 한번 정리해보았습니다. Rxjs가 어떤 라이브러리인지 감이 오셨기를 바랍니다.

RxJsArrayPromise의 성질을 모두 가진 이벤트를 다룰 수 있는 Observable이라고 하는 새로운 객체타입을 제공하는 라이브러리입니다.

이 객체는 Array의 메소드(map, filter)와 같은 연산자를 제공하며 이를 통해 비동기 이벤트를 컬렉션을 다루듯이 처리할 수 있게 만들어줍니다.

= 비동기 이벤트와 시간을 Array처럼 다룰 수 있게 만들어주는 라이브러리

Rxjs를 하면 뭘 할 수 있나요?

비동기 이벤트를 컬렉션 다루듯이 즉 Array를 다루듯이 선언적으로 개발을 할 수 있습니다.

트리플 클릭을 구현해야 한다고 한번 생각해봅니다. 0.25초 내에 클릭한 개수가 3개라면 트리플 클릭이라고 가정하고 한번 코드를 작성해봅시다.

굉장히 단순할 것 같은 이 코드를 막상 eventListener와 setTimeout등을 이용하여 작성을 하려고 하면 순간 막막해지기 마련입니다. 하지만 Rxjs를 이용한다면 굉장히 직관적으로 작성을 할 수가 있습니다.

const tripleClicks$ = fromEvent(window, "click")
  .bufferTime(250) // 0.25초간 
  .filter(clicks => clicks.length === 3) // 클릭이 3개면
  .subscribe(...)

뿐만 아니라 우리가 흔히 작성을 하는 서버와의 통신에 대한 예외처리들, 가령 서버와 통신을 시도했을때 5초내에 응답이 없으면 실패로 간주하고 다시 시도하고 실패시 1초뒤에 재시도하나 3번 연속으로 실패시에는 별도 처리를 하고 서버 응답을 연속으로 요청할 경우 이미 처리되고 있는 통신이 있다면 무시하도록 한다라는 등의 요구사항을 그냥 작성하려고 막막하지만 Rxjs에서도 꽤 직관적으로 작성을 할 수 있게 됩니다.

request$
  // 아직 진행중이면 skip
  .exghustMap(params => post_some_request(params) // 서버와의 통신
    .timeout(5000) // 5초간 응답이 없으면 에러로 취급
    .retryWhen(error => error.delay(1000).take(3)) // 에러가 발생시 1초 지연, 3번까지
  )
  .subscribe(...)

구현하기 까다로운 이벤트와 비동기 그리고 시간을 복잡한 코드가 아니라 값으로 다루게 되는 시각을 가지게 되면 훨씬 더 프로그램을 단순한 시각에서 개발을 할 수 있게 해줍니다.

뿐만 아니라 Value와 Array와 Promise, 그리고 Observable 모두 (비)동기 컬렉션, 즉 스트림으로 추상화하여 생각할 수 있습니다. Value는 그냥 값이고, Array는 동기 컬렉션, Promise은 비동기 값, Observable은 비동기 컬렉션으로 모든 것은 스트림으로 되어 있다고 생각을 하면 프로그램은 결국 스트림을 다루는 것으로 귀결되는 아주 심플한 사고방식인 반응형 프로그래밍 패러다임으로 개발을 할 수 있게 됩니다.

Rxjs는 왜 써야 하나요?

웹에서 화상회의 서비스를 개발한다고 생각을 해봅시다. 화상회의를 만들기 위해서는 다소 어려운 API들을 다뤄야 합니다. WebRTC라는 것도 해야하고 카메라나 마이크 데이터를 가져오기 위해서는 로컬 미디어 스트림 API도 알아야하고 미디어 장비등의 조회나 권한등의 API를 알아야합니다. 보통 개발 초기에는 이러한 API들을 배우고 학습하는 것이 오래걸리는 작업이 됩니다.

그런데 실제 프로그램을 개발하면서 복잡해지고 어려워지는 이유는 API가 어려워서가 아닙니다. 전체 코드에서 이러한 핵심 API가 차지하는 비중은 사실 얼마되지 않습니다.

실제로 우리가 구현하고 기획서에서 요구하는 내용은 이러한 동작들이 언제, 어떤 조건에 발동을 할지를 정하는 시나리오를 구현하는 비중이 훨씬 큽니다.

개발자가 늘 흔히 받는 기획서의 내용

참여자가 10명 이상이 들어오면 마이크를 꺼주세요.
참여자가 입퇴장시에 토스트 팝업을 띄워 주세요.
장비가 바뀌면 토스트 팝업을 띄우고 장비를 알아서 변경해주세요
... 하면 ... 이렇게
... 하면 ... 저렇게

그리고 이러한 동작은 대부분이 비동기 형태로 발생합니다.

우리가 개발하는 앱이 복잡해지는 이유는 이러한 비동기의 ... 하면 ... 이라는 구현이 많아서 입니다. Rxjs는 이러한 ... 하면 ... 의 구현을 로직의 조합이 아니라 값으로 처리할 수 있도록 해줍니다.

뿐만 아니라 여러가지 Operator들을 제공하면서 적절한 조합을 통해서 우리가 원하는 형태의 코드를 직관적이며 단순하고 선언적으로 작성할 수 있도록 만들어 줍니다.

Array를 절차형이 아니라 함수형으로 다루면 코드가 간결해지듯이,
비동기 로직도 데이터로 취급해 함수형으로 다루면 코드가 간결해집니다.

RxJs는 어떻게 쓰는 걸까?

Array를 다루기 위해서는 map, filter, reduce 뿐만 아니라 every, concat, fill, join 등 다양한 Array를 다루는 Method들이 존재합니다.

마찬가지의 개념으로 Observable은 비동기 Array의 형태이며 이러한 데이터 형태를 다루는 여러가지 Operator들이 존재합니다.

각각의 주요한 메소드들을 이해한 뒤에 원본 Observable을 적절히 Operator를 조합하여 원하는 형태의 결과물을 만들 수 있기 위한 코드를 작성하면 됩니다.

실전 예시

참여자 입장시 'OO님이 참여하였습니다.' 토스트 팝업을 띄워주세요.
단, 여러명이 동시에 입장하면 2초동안 모았다가 'OO 외 2명이 참여했습니다.'
이런식으로 표기해주세요.

const participants$:Observable(Array<Participants) = ...

participants$
  .distintUntilChanged((a, b) => a.length === b.length) // 참여자 명수가 변할때만,
  .bufferCount(2, 1) // 2개씩 짝지어 전후를 비교하여
  .filter(([prev, curr]) => curr.length > prev.length) // 
  // => 새로운 참여자가 입장했는가?

  // 전후 데이터를 비교하여 새롭게 참가한 사람만 추려내어
  .map(([prev, curr]) => array_diff(prev, curr, p => p.uid))

  // 2초간 모아보고 새로운 참가자가 있으면
  .bufferTime(2000)
  .filter(x => x.length)

  // 동시참가자수에 따라 토스트 팝업 출력 
  .tap(participants => {
    if (participants.length === 1) show_toast("OO 님이 입장")
    else show_toast("OO 님외 N명 입장")
  })

어떠신가요? 한번 Rxjs를 해보고 싶다는 생각이 드시나요? Rxjs를 내 기술스택에 탑재를 한다면 다음과 같은 것들을 할 수 있게 됩니다.

비동기 로직를 Data로 다룰 수 있게 됩니다.
모든 코딩을 스트림과 함수형으로 쉽게 작성할 수 있게 됩니다.
Pull 패러다임에서 Push 패러다임으로 코딩할 수 있게 됩니다.


2부. Rxjs를 배우며 어려웠던 것들 - 현실편

Rxjs의 활용이나 장점이나 개념들을 공부하다보면 정말 새로운 세상을 만난 것 같고 내가 작성하던 어려운 코드들이 정말로 쉽게 작성이 될 것 같은 희망을 품게 됩니다. 그렇지만 정작 실제 코드에 도입을 하기에 많은 장벽들이 존재합니다. 2부에서는 그러한 이야기들을 다루고자 합니다.

pipe?

Rxjs를 지금에 와서 배우려고 하면 Rx를 참 낯설게 만드는 것이 Rxjs 5.5부터 도입이 된 pipe라는 개념입니다. Rx의 초창기는 Event를 Array처럼 다루는 개념으로 시작한 것이기에 Array의 Method 문법과 많이 닮아 있었습니다. 그러나 이후 module과 번들의 개념이 도입이 되고 Rxjs의 기본 덩치가 커짐에 따라서 Method 방식은 Tree-Shaking에 불리하다는 문제가 있었습니다.

그래서 각 Method를 함수로 만들어서 함수형 프로그래밍의 파이프방식을 차용하여 다음과 같은 방식으로 코드를 작성할 수 있도록 만들었습니다.

// 기존 Method 방식 (dot-chain 방식)
import {Observable} from "rxjs"

const tripleClicks$ = Observable.fromEvent(window, "click")
  .bufferTime(250)
  .filter(clicks => clicks.length === 3)
  .subscribe(...)
// Pipe Method 방식
import {fromEvent, bufferTime, filter} from "rxjs"

const tripleClicks$ = fromEvent(window, "click").pipe(
  bufferTime(250),
  filter(clicks => clicks.length === 3),
).subscribe(...)

이렇게 Method가 아니라 Operator 함수로 분리를 하면 import를 한 만큼만 번들에 포함시킬수가 있게 되기 때문에 조금 번거롭고 코드의 가독성을 희생이 되어도 사용하지 않은 Operator를 코드에 포함시키지 않도록 하여 번들의 크기를 줄일 수 있게 되었습니다. 이렇게 만들면 Custom한 Operator를 Method에 포함시키지 않고 얼마든지 만들 수 있다는 장점도 있습니다.

그렇지만 그 조금 번거롭고 가독성이 희생된다는 단점과 타입스크립트와 호환도 좋지 않다는 점이 쓰던 사람이 넘어가기에는 조그만한 허들이지만 처음 배우는 사람에게는 굉장히 낯선 방식이기에 상당히 큰 허들로 다가오게 됩니다.

그렇기에 처음 Rxjs를 접하시는 분들이라면 혹은 아직 낯선분들이라면 pipe의 개념을 Array의 Method와 비슷한 형태로 간주하고 바라본다면 조금 더 친숙하게 사용할 수 있습니다.

Rxjs측도 이러한 점을 충분히 인지하고 있어도 새로운 대안을 제시하였습니다. 그것은 바로 Pipeline Operator |> 가독성의 문제는 결국 문법적인 문제이므로 JS에 새로운 문법을 도입하여 더 간결한 형태로 함수형 프로그래밍을 잘 할 수 있는 Operator를 제안합니다.

// Pipe Operator란?
const lowercase = (str) => str.toLowerCase()
const capitalize = (str) => str.slice(0, 1).toUppercase() + str.slice(1)
const wow = (str) => str + "!"

wow(capitalize(lowercase("hEllo wORlD"))) // Hello world!
// => 함수를 연속해서 호출을 하려니 적용되는 순서가 반대가 되어 헷갈린다. 🤔

"hEllo wORlD" |> lowercase |> capitalize |> wow // Hello world!
// => 값이 함수를 통해 전달되는 코드를 작성할 수 있게 되어 함수 조립을 훨씬 더 직관적인 코드형태로 작성할 수 있다.

이와 같이 Pipeline Operator를 쓸 수 있게 되면 Rxjs는 Method가 아닌 함수로 분리한 장점을 다 가져가면서도 훨씬 더 함수형스러운 방식으로 코딩을 할 수 있게 됩니다.

// Pipeline Operator 방식
import {fromEvent, bufferTime, filter} from "rxjs"

const tripleClicks$ = fromEvent(window, "click")
  |> bufferTime(250),
  |> filter(clicks => clicks.length === 3),
  |> subscribe(...)

그렇지만 이 방식은 현재 ES2022가 된 지금까지도 아직 표준 제안에만 계류중이며 아직 정식 표준이 되지 못하고 있는 중입니다.

이렇게 오래 걸릴 줄 몰랐지 ㅠㅠ Observable과 Pipeline은 아직도 논쟁중...

현재 자바스크립트의 표준 API인 Promise도 처음부터 자바스크립트에 포함이 되어있지는 않았습니다. 자바스크립트가 싱글쓰레드의 이벤트루프 방식을 택하면서 필연적으로 비동기처를 콜백으로 해야만 했기에 코드가 callback 지옥이 되었고 여기에 예외처리까지 더해지면 복잡한 코드가 만들어지기 십상이었죠.

그때나온 Promise A+ 라는 라이브러리가 만들어졌고 (https://promisesaplus.com/) 비동기를 다루는 방식이 획기적으로 쉬워지면서 Promise는 자바스크립트의 표준 API가 되었습니다. 이후 async await이라는 새로운 문법까지 생겨났습니다.

제가 이 모든 역사를 실제로 겪으면서 또 Rxjs를 접하게 되면서 Observablepipeline operatorTC39 표준 라이브러리 제안에 올라가있는 상태였기에 이 둘도 Promise처럼 자바스크립트 표준에 포함될 거라고 생각해서 예습차원에서 열심히 공부하고 익혔습니다.

Observable 표준제안 https://github.com/tc39/proposal-observable
Pipeline 문법 표준제안 https://github.com/tc39/proposal-pipeline-operator

하지만 7년이 지난 지금 아직도 두 개의 방식은 현재 표준이 되지는 못하고 아직까지 논의중에 있습니다. 아직도 자바스크립트 표준에 추가되어야 설문항목에 항상 포함이 되어 있지만 모든 사람이 공감하는 주제는 아직 아닌 듯 하네요. 반응형 프로그래밍함수형 프로그래밍 패러다임을 너무 사랑하는 저로써는 이러한 주류의 시각이 아쉬울 따름입니다.

(state of 2021 자바스크립트에 추가되었으면 하는 기능 설문 결과)

그렇기에 현재 자바스크립트 문법체계에서는 낯설고 다소 번거로운 방식으로 작성을 해야하는 문제가 있기에 Array Method를 사용하는 관점이라는 것을 알면 조금 더 쉽게 적응을 할 수 있습니다. 추후 Pipeline Operator가 표준이 되는 날이 와서 간결한 문법으로 사용할 수 있게 되기를 희망합니다.

사실 Operator가 중요한게 아니다.

Rxjs를 공부하를 하고 나서 실전 적용시 제일 막막한 부분은 많은 것을 배웠음에도 그래서 실전에서 어떻게 써야 할지가 어렵다는 점입니다. 그리고 그 이유는 Rx학습의 대부분이 map(), filter(), tap(), take() 등의 Operator 에 있기 때문입니다.

이러한 Operator들을 배우면서 이런 경우에는 이렇게 쓰면 되겠구나를 상상하면서 머리속에 분류체계들을 만들곤 하지만 실전에 오게 되면 Operator를 몰라서가 아니라 Operator를 적용할 Source인 Observable이 없다는 점입니다.

기껐해야 사용해보는 것들이 fromEvent()를 통한 이벤트처리나 timer()등을 이용한 시간처리 ajax()를 이용한 서버응답처리가 존재하는데 이벤트 처리는 이미 프레임워크에서 이벤트 핸들러로 처리하고 있고 시간을 다루는 경우는 사실 많지 않으며 Ajax의 경우에는 어차피 데이터를 1번만 전달받다 보니 Promise로도 충분했기에 Rxjs를 현재 웹 프레임워크에서 어디서부터 적용을 해야할지가 점접을 찾는 것이 참 어렵습니다.

일단 Source로 만들 수 있는 것 부터 찾아보자.

제가 처음으로 Rxjs를 실무에 적용을 했던 것은 Realtime DatabaseFirebase를 연동하면서 였습니다. Ajax와는 다르게 1번의 값이 아니라 변경되는 값이 연속적으로 전달받는 방식이기에 훨씬 Stream 한 방식과 잘 어울린다고 생각을 했습니다.

// Firebase는 이런식으로 Callback과 API를 조합해서 데이터를 사용해야해서 복잡한 코드를 만들어야 했다.
const ref = firebase.database().ref(path)

ref.on("value", snapshot => {
  const value = snapshot.toJSON()

  // doSomething here
})

ref.off()

위와 같이 콜백을 통해서 로직의 형태로 개발을 해야하는 방식에서 Rxjs를 도입하여 Firebase API를 Observable의 문법으로 변경을 해보았습니다.

const fromFirebase = (path:string) => new Observable(observer => {

  const ref = firebase.database().ref(path)

  ref.on("value", snapshot => {
    const value = snapshot.toJSON()
    observer.next(value)
  })

  return () => ref.off()
})

위와 같이 Rxjs로 Firebase API를 캡슐화하고서 더이상 로직이 아니라 값으로 다루기 시작하니 신세계가 열렸습니다. 연관된 computed Value를 만들거나 참가자가 새롭게 들어왔다는 것을 알아내는 이벤트도 데이터를 기준으로 아주 쉽게 작성을 할 수 있었습니다.

const users_in_chat$ = fromFirebase("/chat/users").pipe(
  map(users => Object.values(users))
)

const num_users_in_chat$ = users_in_chat$.pipe(
  map(users => users.length),
  distinctUntilChanged() // 같은 값이면 중복 전달 방지
)

num_users_in_chat$.pipe(
  bufferCount(2, 1), // 예전 값을 가져와서
  filter(([prev, curr]) => curr > prev), // 참가자 수가 커진 상황에서만
  tap(() => { // 새로운 참가자가 들어왔다!! })
).subscribe()

상태관리도 Rxjs로 다 할 수 있을 것 같은데?

Firebase의 데이터를 Rx로 다루는 방식을 경험하고나니 너무 편하다는 생각이 들면서 이러한 방식을 토대로 프론트엔드의 전체로직인 상태관리마저 Stream이라고 생각을 하고 Rxjs로 만들 수 있지 않을까 생각을 하고 Rxjs를 기반으로 하는 상태관리 라이브러리를 만들어 사용하고 있습니다.

https://github.com/developer-1px/adorable

(사실 Rxjs를 이용한 프론트엔드 상태관리와 패러다임에 관한 글을 먼저 작성하려고 했지만 Rx에 대한 배경지식이 없으면 설명하기가 힘들다보니 프리퀄 겪으로 지금 Rx에 대한 글을 쓰고 있는 중이랍니다.)

그 밖에 API 라이브러리, Aniamtion 라이브러리, Drag 라이브러리 도 Rxjs로 만들게 되면 훨씬 더 단순화된 관점으로 풍성하게 다룰 수 있습니다. Rxjs를 이용한 다른 응용에 대해서는 한데 모아서 따로 글을 작성해 볼 계획입니다.

Hot? Cold?

Rxjs로 Source를 만들 수 있게 되면 기존의 복잡한 콜백로직등을 모두 Observable의 세계로 전환하여 선언적으로 코드를 다룰 수 있게 되면서 우리는 훨씬 더 고급스러운(?) 코드를 작성하게 될 수 있게 되었습니다.

그리고 그 다음은 마주치는 허들이 바로 HotCold의 개념입니다.

하나하나씩 다 파고들면 내용이 상당히 복잡하기 때문에 우리가 잘 알고 있는 Promise와 비교를 통해서 핵심개념만 공유하고자 합니다.

Promise의 경우는 함수를 사용하는 순간 이미 실행이 되며 데이터가 공유되는 Hot 방식입니다. 예제코드를 통해 Promise의 동작방식을 상상해봅시다.

// 생성 시점에 이미 실행이 되어 1초 뒤 Promise에 값이 보관된다.
const p = new Promise(resolve => {
  setTimeout(() => resolve("promise!"), 1000)
})

// 아직 값이 없으니 기다렸다가 1초 뒤 값이 출력이 된다.
p.then(res => console.log(res))


// 2초 후에 요청을 한다면
setTimeout(() => {
  // 이미 Promise는 이미 1초대에 값이 보관이 되어 있으므로 호출 즉시 출력이 된다. 
  p.then(res => console.log(res))  
}, 2000)

하지만 Rxjs는 함수형 프로그래밍 패러다임에 근간을 두고 있기 때문에 지연평가(Lazy evaluation) 방식을 사용합니다. 그렇기에 Observable의 세상에서는 생성시에는 선언만 해두고 구독이 요청한 시점에 해당 코드를 실행하는 Cold 방식을 가지고 있습니다.

// 생성을 했지만 실행은 시키지 않고 선언만 해둔 상태이다.
const p$ = new Observable(observer => {
  setTimeout(() => observer.next("observable!"), 1000)
})

// 구독 요청을 했으니 이 함수가 실행된 시점부터 1초 뒤 값이 출력이 된다.
p$.subscribe(res => console.log(res))


// 2초 후에 요청을 한다면
setTimeout(() => {
  // 지금 요청을 했기 때문에 별도의 observer가 생성이 되기 때문에 바로 출력이 되지 않고 다시 1초를 더 기다린 뒤에 출력이 된다.
  p$.subscribe(res => console.log(res))  
}, 2000)

즉 Observable은 데이터를 요청할때마다 그때 함수가 실행되어 결과를 전달해주는 방식이라서 여러군데서의 같은 값을 사용하게 되면 데이터를 공유하지 않고 그때마다 선언된 함수를 호출하는 방식이 됩니다.

🔥 이걸 모르고 있다면 API를 Observable로 만들고 난뒤 여러군데에서 해당 데이터를 사용하게 되면 API콜을 중복으로 계속 호출되는 문제가 발생하게 됩니다. 에니메이션과 같은 경우에는 Cold 한 방식이 어울리나 서버 데이터나 상태관리와 같이 API를 다루는 데이터는 Hot 방식이 더 잘어울립니다.

그렇지만 Rxjs에서 Cold한 Observable을 Hot하게 만들기 위한 과정(Multicasting)이 상당히 복잡합니다. Rxjs에서도 이걸 인지했는지 Rx7에서 여러가지 개편을 시도했고 앞으로 나오게될 Rxjs8에서도 개념이 있을 거라고 하네요. 그래서 복잡하게 파고들기보다는 이러한 Hot, Cold에 대한 개념만 이해를 해두고 개선된 버전을 기다려보는 것도 좋을 것 같습니다.

Rxjs Observable의 기본이 Cold인데 그렇다면 Multicasting을 쓰지고 않고 Hot Observable을 만드려면 어떻게 해야 할까요?

Subject, BehaviorSubject

그래서 Hot Observable 전용 객체인 SubjectBehaviorSubject가 존재합니다. Hot이 필요하다면 복잡한 Multicasting보다는 Subject를 사용해보시면 좋을 것 같아요.

프로그램에서 값을 계속 공유해서 사용을 해야하는 경우에는,

1) 이벤트나 동작과 같이 시점이 중요한 경우 -> Subject
2) API등의 동작의 결과값을 계속 공유해야하는 경우 -> BehaviorSubject

와 같이 사용을 할 수 있습니다. 그래서 Firebase의 API는 BehaviorSubject를 이용하여 다음과 같이 변경을 해야겠네요.

const memo = Object.create(null)

export const fromFirebase = (path:string, initValue:T) => {
  const r$ = memo[path] = memo[path] || new BehaviorSubject(initValue)
  const ref = firebase.database().ref(path)

  ref.on("value", snapshot => {
    const value = snapshot.toJSON()
    r$.next(value)
  })
  
  return r$.asObservable()
})

SubjectBehaviorSubject는 특히 프론트엔드 상태관리를 만들기 위해서 정말 중요한 개념이므로 Rxjs와 프론트엔드 상태관리와 함께 설명할 글에 또 자세하게 설명을 드려보도록 하겠습니다.


끝으로...

프론트엔드 프로그래밍을 하다보면 특정 기능을 구현하는 것보다 사용자들의 동작과 조건에 맞춰 적절히 연결을 하는 것이 더 복잡하는 것을 알게 됩니다. 이러한 연결을 하는 과정이 복잡해지는 것에는 비동기라는 문제가 있고 이 비동기를 잘 다루는 것이 참 어렵고 고급 개발자로 가기 위해서 정복해야할 분야라는 것을 알게 됩니다.

Rxjs는 이러한 비동기를 잘 다루기 위해 만들어졌으며 특히 함수형 프로그래밍, 반응형 프로그래밍, 선언적 프로그래밍이라는 새로운 패러다임을 기반으로 하는 멋진 라이브러리입니다. 물론 그 새로운 패러다임이 훌륭하지만 배워야 할 것도 많고 낯설기도 한것도 사실입니다.

그러다 보니 만들어진지 꽤 오래 된 라이브러리임에도 사용하는 유저층이 많지 않지만, 또 한번 다루게 되면 패러다임이 바뀌게 되어 기존방식으로는 이제 개발하기 힘든 몸(?)이 되는 저같은 골수 팬들도 많이 보유하고 있습니다.

또한 Google의 웹 프레임워크인 Angular에서 사용하는 공식 상태관리용 라이브러리이기도 합니다. Promise에 이어 라이브러리에서 웹 표준 API로 등록되기 위해 TC39에 계류되어 있는 라이브러리이기도 합니다. (물론 벌써 7년째 계류중이지만요... ㅠㅠ)

제 예측과는 달리 Rxjs는 아직 주류가 되지 않았고, 웹 표준 API가 되지 못했고, pipeline operator |>로 개발하는 함수형 프로그래밍의 세상은 오지 않았지만 정말로 이 한번의 패러다임을 바꾸기만 한다면 기존과는 달리 정말로 편하게 비동기 프로그래밍을 할 수 있는 세상이 펼쳐집니다.

Rxjs의 학습허들이 높지만 사실 이 모든 것이 필요하다고 생각하지는 않기에 그 중에서 꼭 알아야할 핵심만 골라서 알려주면 얼마 없겠지 하면서 쉽게 시작한 글인데 글을 줄이고 줄였음에도 정작 알려주고 싶은 프론트엔드에서 Rxjs로 상태관리 하는 법을 위한 배경지식조차 다 설명하지를 못했네요.

(지금 설명하고 있는게 이런 기분이긴 합니다만...ㅋ)

아직 프론트엔드에서는 이러한 허들로 인해서 Rxjs가 주류가 아니지만 충분히 배워볼만한 가치가 있다고 생각을 합니다. Everything is Stream이라는 새로운 패러다임은 프로그래밍을 정말로 단순하게 만들어주니까요. 그리고 정말로 Observable이 표준이 되거나 pipeline operator가 표준이 될 수도 있어요.

무엇보다 Rx는 javascript에만 국한되어 있지 않습니다. 수많은 언어와 플랫폼에서 동작을 하기 때문에 하나의 패러다임과 방식으로 다양한 언어와 프레임워크에서도 동일한 개념과 방식으로 코딩을 할 수 있게 됩니다.

뿐만 아니라 많은 회사들이 쓰고 있진않지만 쟁쟁한 회사들이 사용하고 있습니다. Rxjs에 많은 관심 부탁드리며 궁금한 내용이 있거나 어려운 개념 설명이 필요한 내용이 있다면 편하게 댓글로 질문 남겨주세요.

이 글이 Rxjs를 지금 배우고 있는 데 어려움을 겪고 있거나 Rxjs를 새롭게 익혀보려고 하는 분들에게 Rxjs가 너무 어렵지 않게 바라볼 수 있게 도와줄 수 있는 글이 되기를 바랍니다.

감사합니다. 😙

profile
AdorableCSS를 개발하고 있는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

28개의 댓글

comment-user-thumbnail
2022년 10월 9일

요 몇 주간 현업에서 스크롤, 터치 이벤트를 제어하기 위해 rxjs를 도입해 사용하고 있습니다. 어렵더군요.. ㅎㅎ
익숙해지면 여러 상황에서 유용하게 쓰일 것 같아 계속 사용해보려고요.

1개의 답글
comment-user-thumbnail
2022년 10월 11일

rxjs 의 한줄 요약 정도만 알고있었는데 글을 읽어보니 유용할 것 같은 상황들이 연상되기도 하네요..!
글을 너무 잘 쓰시는 것 같습니다..! 항상 배우고 있습니다 감사합니다ㅎㅎㅎ

1개의 답글
comment-user-thumbnail
2022년 10월 12일

react-query 같은 것도 처음에 컨셉을 받아들이는게 어려워서 그랬지 점점 이거 없을 때로 못돌아가겠다 생각이 들더라고요 ㅋㅋㅋ rxjs도 그렇게 느껴졌으면 좋겠네요 잘 읽었습니다!

1개의 답글
comment-user-thumbnail
2022년 10월 13일

최근에 함수형 프로그래밍 스터디를 한 적 있는데 맥락이 비슷하네요 ㅎㅎ 비동기 처리와 동기 처리를 자연스럽게 엮어서 복잡한 연속적인 처리도 깔끔하게 표현할 수 있지만...! 정작 이 높은 러닝 커브를 극복해서 팀원 전체가 익숙해지기에는 그정도로 쓸 일이 많이 있나 싶은 ㅎㅎ

1개의 답글
comment-user-thumbnail
2022년 10월 15일

ㅇ ㅓ...c++도 어려운데 함수형 코딩이라닛...
담에 도전 해보겟습니다 감삼다

1개의 답글
comment-user-thumbnail
2022년 10월 15일

최근에 함수형 프로그래밍과 함께 rxjs에 대한 도입을 논의중인데 내용을 정리할 좋은 자료였습니다.
저도 언능 도입해보고 후기를 작성해보고 싶네요!!

3개의 답글
comment-user-thumbnail
2022년 10월 25일

푸쉬 기능 개발에 RxJS와 파이어베이스 예시를 적용해보고 싶네요 :-) 잘 읽었습니다!

답글 달기
comment-user-thumbnail
2022년 11월 2일

좋은 내용 너무 감사합니다! Rxjs 들어만 봤지 제대로 찾아보진 않았었는데,
테오님 덕분에 Rxjs 매력에 빠져버렸습니다 ㅋㅋㅋㅋ 좀 더 공부해서 한번 써보아야겠어요 +_+ 다시 한번 좋은 내용 감사드립니다 (__)

1개의 답글
comment-user-thumbnail
2022년 11월 3일

리액트 + 리액트 쿼리 + 서스펜스 조합을 주로 사용하다보니... 테오님이 실무에서 어떤 케이스에 어떻게 사용하고 있는지 궁금해지네요. Vue3 컴포저블 이전의 이벤트 버스 컴포넌트 역할이 아닌가 생각도 들고요...ㅋㅋ; RxJS 실전 활용기로 후속 아티클 기대해도 될까요? 좋은 글 감사합니다

1개의 답글
comment-user-thumbnail
2022년 11월 10일

졸업후 신입으로 취업한 첫 직장에서 Angular 와 함께 Rxjs 를 사용하는 Obserbable 패턴을 사용하고 있습니다.
처음 접하는 개념이라 너무 어려웠는데, 테오님 글을 우연히 발견하여 읽고나니 한결 이해가 된것 같네요.
좋은 글 정말 감사드립니다. 여러번 정독 하겠습니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2022년 11월 28일

오 완전 잘 읽었습니다

1개의 답글