프로그래밍 패러다임과 반응형 프로그래밍 그리고 Rx

teo.v·2022년 2월 4일
72

테오의 프론트엔드

목록 보기
15/48
post-thumbnail

테오님을 비롯한 선배님들은 spa 프로젝트 설계를 어떻게 하시나요?

언제나 좋은 소재를 제공해주시는 분들께 감사를 드립니다.

설계에 관한 이야기를 먼저 쓰려고 했는데 먼저 설계의 원칙이라고 할 수 있는 프로그래밍 패러다임에 대한 설명이 선행이 되어야 할 것 같아, 현재 제가 쓰고 있는 개발 패러다임인 "반응형 프로그래밍(Reactive Programming)" 에 대한 이야기를 해보고자 합니다.

객체지향 프로그래밍이나 함수형 프로그래밍과 같은 개념의 내용이라 깊게 파고 들면 방대한 내용이니 최대한 단계별로 이해할 수 있게 풀어서 적어보았으나 이해가 안되더라도 그냥 재미있게 개발이야기 정도로만 소비해주시면 좋을 것 같습니다.

프롤로그

패러다임의 전환

“관점을 바꾼다는 것은 매우 어려운 일이다.
... 새로운 패러다임은 기존과는 다르게 생각하는 방법을 알려줄것이다.
그리고 아마도 예전 방식으로 절대 돌아 가지 않을 것이다.”

오늘 해볼 주제는 "반응형 프로그래밍(Reactive Programming)" 에 대한 이야기입니다. 우선 위키피디아로 가서 한번 "Reactive Programming"이 무엇인지 검색을 해보겠습니다.

What is Reactive Programming?

reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change. -wikipidea

... 반응형 프로그래밍이란, 데이터의 흐름변경사항의 전파에 중점을 둔 선언적 프로그래밍 패러다임이다. - 위키피디아

그렇다는군요. 전에도 언급했지만 컴퓨터 사이언스에서 정의는 이해가 아니라 배워야 할 목차와 같은 거라고 했습니다.

따라서 이번 글에서는 다음과 같은 핵심키워드인 패러다임, 변경 사항의 전파, 데이터의 흐름 그리고 선언적 프로그래밍을 머리속에 넣어두시고 읽어주시기 바랍니다.


패러다임이 뭔가요?

패러다임(영어: paradigm)은 어떤 한 시대 사람들의 견해나 사고를 근본적으로 규정하고 있는 테두리로서의 인식의 체계, 또는 사물에 대한 이론적인 틀이나 체계를 의미하는 개념이다. - 위키피디아

역시 예시를 드는 게 제일 좋겠죠. 모두가 이해할만한 좋은 패러다임과 관련한 예시로는 지동설과 천동설이 있습니다.

지구가 중심이냐? 태양이 중심이냐? - 천동설과 지동설

(이미지 출처: 간단하고 단순한 것이 답이다!)

지구가 중심이라는 관점을 가지고 우주의 움직임을 설명하는 천동설과 태양이 중심이라는 관점으로 우주의 움직임을 설명하는 지동설이 존재했습니다. 이렇듯 어떠한 관점을 가지느냐에 따라 같은 우주 움직임을 전혀 다른 2가지의 방식으로 설명을 할 수 있던 시절이 있었습니다.

흔히 지동설이 옳고 천동설이 틀린거라는 생각하는 사람들이 있지만 실제로 그러한 것은 아니라고 합니다. 정말로 관점의 차이만 다를 뿐 둘다 충분히 우주의 움직임을 설명할 수 있었다고 합니다.

[참고] 천동설은 지동설보다 열등한가?
https://brunch.co.kr/@freewriter21/143
https://inmun360.culture.go.kr/content/357.do?mode=view&page=3&cid=103083&sf_cat1=CWS1304

다만 이후 지동설이 주류가 된것은 천동설보다 훨씬 더 간결하고 단순하게 설명을 할 수 있는 관점이기 때문이라고 합니다. 덕분에 우리는 조금 더 간결한 방식으로 우주를 바라보고 설명 할 수 있게 되었습니다.

프로그래밍에서 패러다임이란?

프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다. 예를 들어 객체지향 프로그래밍은 프로그래머들이 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하는 반면에, 함수형 프로그래밍상태값을 지니지 않는 함수값들의 연속으로 생각할 수 있게 해준다. - 위키피디아: 프로그래밍 패러다임

천동설과 지동설의 예시와 같이 내가 프로그램이라고 하는 것을 어떠한 관점으로 바라보고 설계를 하느냐에 따라서 같은 목적을 이루고자 하더라도 전혀 다른 형태로 프로그래밍을 할 수 있도록 해줍니다.

또한 잘 만들어진 프로그래밍 패러다임은 보다 좋은 프로그램을 만들 수 있는 방법과 시각을 제공해줍니다. 그래서 우리가 다양한 프로그래밍 패러다임을 알고 있다면 프로그램을 이해하는 데 훨씬 더 도움을 받을 수 있게 됩니다.

우리가 객체지향 프로그래밍이라던가 함수형 프로그래밍에 대해서 배우는 것들도 그러한 맥락입니다.

나아가 여러가지 패러다임을 알고 있다면 그 중에서 더 간결하고 단순하게 설명하고 있는 관점을 선택적으로 취할 수 있게 됩니다.

현대에 와서는 하나의 프로그램을 하나의 패러다임만이 아니라 여러 패러다임을 함께 섞어서 사용하려는 시도가 늘고 있으며 특히 웹 프론트의 자바스크립트야말로 여러가지 패러다임의 공존의 장이기에 다양한 프로그래밍 패러다임을 익히는 것은 더 좋은 프로그램의 설계를 할 수 있는 토대가 되어줍니다.

... 순수한 관점에서 이질적으로 보여졌던 패러다임간의 공존이 갈수록 많이 등장하며, 상황과 맥락에 따라 패러다임간 장점만을 취하려는 시도는 계속되고 있다. - 위키피디아: 프로그래밍 패러다임

반응형 프로그래밍도 객체지향 프로그래밍과 함수형 프로그래밍과 같이 프로그래밍을 바라보는 관점을 갖게하고 결정을 하는 역할을 하는 체계라고 이해하시면 좋을 것 같습니다.


1. 반응형 프로그래밍 패러다임이란?

반응형 프로그래밍이란, 데이터의 흐름변경사항의 전파에 중점을 둔 선언적 프로그래밍 패러다임이다.

정의가 언뜻 듣기에는 어려워보이나 사실 반응형 프로그래밍은 아주 오래되고 친숙한 개념입니다. 가장 쉽게 반응형 프로그래밍의 패러다임을 이해할 수 있는 예제는 바로 스프레드 시트입니다.

예시에 있는 C1의 셀에는 =A1+B1 이라는 수식을 선언적 으로 작성을 해두고 A1 B1의 값을 변경을 하면 즉시 변경사항이 전파 되어 C1의 값이 자동으로 변경이 됩니다.

D1에는 =A1+B1+C1 이라는 수식을 선언적 으로 작성을 해두었고 A1 B1의 값을 변경하자 변경사항이 전파 되어 C1의 값이 변경되었고 다시 그 값이 D1의 수식에 반영이 되어 D1의 값이 변경이 되는 데이터의 흐름 이 만들어졌습니다. (저는 이러한 순서를 정의한 적이 없습니다.)

이러한 개념을 조금 더 확장을 해서 이러한 관점으로 프로그래밍을 할 수 있다면 좋지 않을까 하는 패러다임이 바로 반응형 프로그래밍 입니다.

언제부터 웹에서 반응형 프로그래밍이 중요해졌을까?

(출처: https://trends.google.com/trends/explore?date=all&q=%2Fm%2F02vz4nz)

웹 개발에서 반응형 프로그래밍이 중요해진 계기는 다들 알고있는 웹 프레임워크로의 전환입니다.

웹 개발에서 가장 큰 패러다임의 전환, jQuery웹 프레임워크
= DOM을 내가 쉽게 조작하게 도와준다. → DOM을 알아서 렌더링 해준다.

웹 프레임워크의 본질을 반응형 프로그래밍 패러다임 의 관점에서 다시 한번 적어봅시다.

Web Framework의 본질 (MVVM 패턴)
Change Detection + Binding + Template + Auto Render = Reactive
즉, 값이 변경되었을때 템플릿에 선언 해둔대로 알아서 렌더링을 해준다 = 반응형

어떤가요? 우리가 본 반응형 프로그래밍의 정의와 유사하지 않은가요?

웹 프레임워크의 등장은 기존의 프로그래밍 방식과는 혁신적인 패러다임이었기에 웹 프론트에서 아주 중요한 개념이 되었고 이를 정의하는 Reactive Programming이라는 용어의 관심사가 올라가기 시작했습니다.

초기 Reactive Programming의 패러다임은 이렇게 데이터의 변경을 감지하고 선언적으로 프로그래밍을 하는 방법을 통해 View를 업데이트를 하는 방식으로 발전을 하기 시작합니다.

패러다임의 전환 #1 - 선언적 프로그래밍

선언적 프로그래밍이란? - 무엇을 해야할지 따로 약속(표현)을 만들어 기술하게 하고, 언제 어떻게 동작하는지는 내부에 처리하는 방식의 프로그래밍 기법

웹 프레임워크의 가장 큰 핵심은 js를 통해 어떻게 DOM 조작을 할지가 아니라 jsx 혹은 template binding을 통해서 무엇을 해야할지 선언을 해두면 내부적으로 알아서 적절하게 렌더링을 하는 방식입니다.

언제 어떻게 해야할지 내부 매커니즘은 숨기면서 필요한 로직만 외부에서 선언을 할 수 있게 함으로써 알고리즘을 구현을 하는 데 발생하는 에러를 최소화 하고 시간과 변화에 구애받지 않고 개발을 할 수 있도록 해줍니다.

<!-- svelte로 엑셀 시트 예제 만들어 보기 -->
<!-- 아무런 로직도 코딩하지 않았지만 동작한다. -->
<script>
  let a = 0, b = 0;
</script>

<input type="number" bind:value={a}/>
<input type="number" bind:value={b}/>
<div>{a + b}</div>

한번 https://svelte.dev/repl/hello-world 에서 위 코드를 복사-붙여넣기 해보세요 :)

패러다임의 전환 #2 - 변경사항의 전파(Pull → Push)

어떻게 화면에 그릴지를 중심으로 개발을 하다가 이렇게 무엇을 할지만 선언을 하는 방식으로 변경이 되면서 데이터를 대하는 패러다임도 변경이 됩니다.

기존에는 DOM 조작을 통해 화면을 구현을 하는 것이 중심이니 render를 해야 하는 시점에 필요한 데이터를 모두 불러와서(Pull) 화면을 출력을 하는 Pull의 관점에서 UI 개발을 하였습니다.

하지만 새로운 반응형 프로그래밍의 패러다임에서는 미리 선언이 되어 있는 구조에서 값이 변화할때 마다 템플릿으로 데이터를 전달(Push) 를 하는 Push의 관점으로 설계가 되도록 변화가 되었습니다. (React를 하는 분이라면 setState를 떠올려 주세요.)

반응형 프로그래밍 패러다임으로의 전환

이렇듯 웹 프로그래밍은 데이터를 가져와서 화면을 만드는 방향에서 무엇을 할지 선언을 하고 변경된 데이터를 감지하고 전파하는 방향으로 패러다임이 변해왔습니다.

어떤가요? 스프레드 시트웹 프레임워크를 통해서 반응형 프로그래밍이 어떠한 개념인지 감이 좀 잡히시나요? 사실 패러다임이 먼저 존재하지는 않습니다. 이러한 현상이 먼저 있고 그것들을 후에 하나의 관점으로 정리를 한 것이 패러다임이죠. 이러한 라이브러리와 방법들이 먼저 존재를 했었고 이후 Reactive Programming이라는 것이 정립이 되는 것입니다.

그렇다면 우리는 이제 프레임워크를 당연히 쓰는 시대이니 반응형 프로그래밍을 하고 있는 걸까요? 틀린 말은 아니지만 다시 한번 구글 트렌드의 그래프를 확인해 볼까요? 반응형 프로그래밍은 그 이후에 개념이 확장되며 더 두각을 나타나게 됩니다.

초기에는 이 반응형 프로그래밍 패러다임을 뷰에 집중이 되었다면 이후에는 뷰를 넘어 비지니스 로직을 포함한 모든 스크립트에서 사용할 수 있도록 개념이 확장이 됩니다.

이후 Redux를 필두로한 상태관리라고 하는 것들 역시 데이터의 변경을 감지 하고 변경을 전파 하고 선언적으로 값을 만들어낸다는 반응형 프로그래밍 패러다임에 속하게 됩니다.

이번 글에서는 반응형 프로그래밍 패러다임의 끝판왕 겪인 Rx를 통해서 조금 더 이야기를 해보려고 합니다.


2. 비동기 프로그래밍과 반응형 프로그래밍

왜 프론트엔드 프로그래밍에서 반응형 프로그래밍이 중요할까요? 그것은 비동기 프로그래밍 을 잘하기 위해서입니다.

비동기 프로그래밍은 상당히 어렵습니다. 하지만 프론트엔드의 대부분의 로직이 비동기로 되어 있습니다. 프론트엔드는 사람을 상대하는 UI를 만드는 개발자이며 사람의 동작은 어떻게 동작할 지 예측할 수 없기 때문입니다. 또한 우리는 서버를 상대하는 사람이며 서버의 응답이 언제 어떻게 올지는 예측할 수 없기 때문입니다.

그렇기에 고급 프론트엔드 개발자가 되기 위해서는 이 비동기를 아주 잘 다룰수 있어야만 합니다.

왜 비동기 프로그래밍이 어려울까요?

비동기 프로그래밍은 다음과 같은 이유로 어렵습니다.

  1. 비동기 프로그래밍은 작성한 코드 순서대로 동작하지 않는다.
  2. 언제 실행이 될지 예측할 수 없다.
  3. 호출한 순서대로 동작한다는 보장도 없다.
  4. 그래서 호출 당시의 값과 실제 실행 되었을 때의 값이 그대로일 거라는 보장이 없다.

javascript의 비동기 프로그래밍 대응의 역사

적어도 비동기를 작성한 순서대로 동작할 수 있게 하자!

  • ES5 이전 시설 - 그저 Callback

    (Callback Hell에서 우리를 구원하소서!)

  • ES6 Promise

  • ES7 async / await

javascript는 최대한 비동기 로직을 동기 프로그래밍처럼 동작할 수 있는 방향으로 진화를 했습니다. 하지만 이것으로 충분할까요?

Real world Problem!

당신은 아래와 같은 자동완성을 구현해 달라는 요청을 받았습니다. 어떻게 코딩 할 지 상상해 봅시다.

  • 타이핑 할때 마다 서버에서 데이터를 받아서 보여주세요.
  • 너무 잦은 요청은 부하가 심하니 타이핑 간격이 좁으면 대기하다가 입력이 늦어지면 그때 서버에 요청하세요.
  • 같은 내용일때는 요청하지 마세요.
  • 일정 시간 동안 응답이 없으면 3회 재시도 하고 그래도 응답이 없으면 에러 메시지를 출력해 주세요
  • 데이터는 캐시로 보관을 해서 먼저 보여주고 요청이 완료되면 새로 갱신된 데이터를 보여주세요.
  • 엔터를 누르면 서버로 요청한 건 취소하고 검색 결과를 보여주세요.

Promise, async/await 이 이 문제를 해결 할 수 있을까요? 기존 pull 기반의 비동기 순서를 맞추는 방식의 패러다임에서는 비동기가 복합적으로 존재하면 개발 난이도가 비약적으로 어려워지게 됩니다.

전통적인 백엔드에서는 하나의 request에서 비동기가 여러개가 있더라도 순차적으로 처리해 하나의 response를 만들어주면 되지만 프론트엔드은 사용자의 동작과 서버의 동작의 비동기 동작이 혼재되어 있는 세상입니다.

어떻게 하면 이러한 복합적인 비동기 세상에서 조금 더 프로그래밍을 잘 할 수 있을까요? Rx와 반응형 프로그래밍은 어떠한 방식으로 이 비동기 문제를 접근하는지 한번 살펴보도록 하겠습니다.


3. 반응형 프로그래밍 Deep Dive!

반응형 프로그래밍은 변경을 감지하고 전파 하고 선언적으로 프로그래밍을 작성한다는 패러다임을 구현하기 위해서 아래와 같은 구조를 가지고 있습니다.

응? 어디서 많이 본 구조인데... 아 뭐더라... 아! addEventlistener?

그렇습니다. DOM에서 쓰는 이벤트 리스너를 등록하고 전달하는 방식 역시 반응형 프로그래밍입니다. 이러한 방식으로 인해 웹 프레임워크는 자연스레 반응형 프로그래밍의 구조를 따르게 되었죠.

🔥 우리가 가고자 하는 방향은 모든 스크립트에서 이러한 Event Listener의 관점으로 프로그래밍을 하는 것입니다!

제가 이해를 돕기 위해서 계속 반응형 프로그래밍 패러다임 위에서 이러한 것들이 만들어졌다고 설명은 하고 있지만 사실은 반대입니다. 패러다임은 이미 만들어진 현상을 해석해서 하나로 정리한 관점이자 체계라는 점 다시 언급하고 넘어갑니다.

왜 Event 방식으로 개발을 하나요? - Reactive First!

출처: cycle.js
반응형 프로그래밍에 대한 철학을 통해 만들어진 라이브러리 입니다. 라이브러리가 흥하지는 않았지만 반응형 프로그래밍에 대한 철학을 잘 설명하고 있기에 한번 원문을 읽어보시면 좋을 것 같아요.
https://cycle.js.org/streams.html#streams-reactive-programming

주의: 여기에서는 프로그래밍 패러다임과 관련된 용어들이 많이 등장하다보니 다소 어렵게 느껴질수도 있습니다. 잘 따라와주시길 바랍니다.

제어의 역전!

2개의 모듈이 있습니다. Foo모듈에서는 네트워크 요청을 받으면 Bar 모듈의 값을 증가하는 로직이 있다고 상상해보세요. Foo 모듈에서는 Bar 모듈에게 영향을 끼치므로 위와 같이 그림을 그려보았습니다. 코드로 표현하면 다음과 같습니다.

// Foo.js

import Bar

function onNetworkRequest() {
  // ...
  Bar.incrementCounter(value);
  // ...
}

이 경우 상태를 변화하는 로직은 Bar에 존재하는데 동작은 Foo가 하도록 하고 있습니다. 더군다나 Bar에서는 Foo의 존재를 알 수가 없습니다. 이렇게하면 Bar가 수동적인 모듈이 되고 Foo 모듈과는 강결합되어 Bar에서는 자체 상태관리를 하지 못할 뿐더러 수정이 필요한 모든 로직을 Foo에 공개하는 형식으로 만들어야 합니다.

어떻게 하면 이러한 문제를 수정할 수 있을까요? 이 접근 방식의 대안은 화살표의 방향을 반전시키지 않고 화살표의 소유권을 반전시키는 것입니다.

// Bar.js

import Foo

const incrementCounter = () => { ... }

Foo.onNetworkRequest((event) => {
  // ...
  incrementCounter(event.value);
  // ...
}

이렇듯 모듈과 모듈간의 결합 시 참조의 주체를 바꾸어 사용하는 방법을 제어의 역전(Inversion of Control) 이라고 부릅니다.

이 접근 방식을 통해서 모듈의 상태를 변화하는 로직을 외부 모듈에 의존하지 않고 모듈 내부에서 처리함으로써 보다 캡슐화느슨한 결합을 하기 용이하도록 할 수 있습니다.

책임의 분리와 비동기 처리

반대로 Foo의 입장에서는 어떨까요? Foo 모듈에서도 로직을 수행하는 부분에 있어서 자유로워졌습니다. 이제 누구에게 어떻게 데이터를 전달해야할 지 몰라도 됩니다. 그저 데이터가 만들어지는 시점에 전달만 하면 됩니다. 클릭 이벤트라던가 Promise와 같은 경우가 그렇습니다.

const p = new Promise(resolve => {
//...
  
// 필요한 값이 만들어진 시점에 그저 던지면 된다. 누가 받을지 몰라도 된다.
resolve(value)
  
//...
})


// 전달 받는 쪽에서는 실행 시점이나 순서를 몰라도 된다.
p.then(() => {
// ...
})

처음에 배웠던 데이터의 흐름, 변경사항의 전파, 선언적의 반응형 프로그래밍 패러다임이 기억이 나시나요? 복잡한 비동기 프로그래밍도 반응형 프로그래밍을 이용한다면 선언적으로 데이터의 흐름을 통해 간단하게 처리를 할 수 있게 됩니다.

Reactive First!

이러한 제어의 역전 방식은 이미 DOM의 EventObserver 패턴, Pub/Sub 패턴과 같이 오래전부터 사용해오던 방식이었습니다.

책임의 분리는 미들웨어플러그인과 같은 방식에서도 이미 자주 쓰이던 방식이었습니다.

반응형 프로그래밍은 이러한 관점을 일부가 아닌 모든 스크립트에 적용을 해서 항상 Reactive를 먼저 고려하자는 패러다임입니다.

이러한 방식은 여러개의 모듈간의 결합이 많아질수록 더 유효합니다.

기존의 패러다임에서는 왼쪽의 그림처럼 하나의 모듈에서 여러가지 비동기 코드를 호출하는 pull 방식의 개념을 통해 호출 순서와 실행 순서와 데이터 처리를 한번에 하는 것이 관리를 어렵게 만들었다면, 반응형 프로그래밍에서는 오른쪽 그림과 같이 push를 통해서 하나의 모듈(함수)에서 하나의 화살표만 처리하면 되는 단순한 형태로 생각을 할 수 있게 됩니다.

이 내용이 없어도 Rx로 빌드업을 하는데는 문제가 없을 것 같아 넣을까 말까 고민을 많이 했지만 이 글이 Rx가 아니라 반응형 프로그래밍에 관련된 글이기에 알고 있으면 좋을 것 같았습니다. 당장은 이해가 안되더라도 너무 깊게 고민하지는 않기를 바랍니다.


4. ReactiveX - 스트림을 이용한 개발!

그래서 우리는 Rx와 같은 반응형 프로그래밍 라이브러리를 이용해서 이러한 구조를 만들게 되면 보다 프로그래밍을 단순한 관점으로 개발을 할 수 있게 됩니다.

반응형 프로그래밍은 어떠한 라이브러리나 구현체가 아니라 하나의 관점일 뿐입니다. 하지만 구체적인 예시가 없는 설명은 이해를 어렵게 만들 뿐입니다. 이 글에서는 Rx를 통해서 반응형 프로그래밍을 설명할것이지만 정작 Rx를 어떻게 쓰느지에 대해서는 설명하지 않습니다. (마치 객체지향을 Java를 통해 설명하듯이 말입니다.)

Rx 학습을 추천드리는 이유!
Promise와 async await는 현재 자바스크립트를 배우는 사람에게는 비동기를 다루는 너무나 당연한 개념입니다만 초창기 자바스크립트에는 이러한 개념이 존재하지 않았습니다. Ajax가 보편화되고 Node가 생기면서 callback만 가지고서는 비동기 처리가 너무 복잡해짐에 따라 Promise 라는 Library가 생겨났고 이것이 Javascript의 표준제안에 등록이 되면서 결국 Javascript의 Native한 기능이 되었습니다.

더 복잡한 비동기 프로그래밍을 해결하기 위한 Observable 이라는 API 역시 현재 ECMA의 기본 기능에 탑재 요청이 올라가 있는 상태입니다. 어떻게 될지는 모르겠지만 만약에 Native한 기능이 된다면 Javascript 비동기의 개발의 판도가 바뀔지도 모릅니다. 그 전신이 될 수 있는 Rx를 실무에 쓰지는 않더라도 개념은 한번 쯤 배워보는 것은 어떨까요?

https://www.google.com/search?q=tc39+observable&oq=tc39+%E3%85%92&aqs=chrome.1.69i57j0i512l2.3064j0j7&sourceid=chrome&ie=UTF-8

Rx가 무엇일까?

ReactiveX
An API for asynchronous programming with observable streams
- 관측가능한 스트림을 통해 비동기 프로그래밍을 하기 위한 API

Rx를 알고나면 이 정의가 너무나 당연한 말인데 이해하는 과정이 좀 길고 복잡합니다. 비동기(asynchronous), 옵저버블(observable), 스트림(streams) 이라는 키워드만 일단 기억을 해둡시다.

Reactive Revolution
ReactiveX is more than an API, it's an idea and a breakthrough in programming. It has inspired several other APIs, frameworks, and even programming languages.

왜냐하면 Rx는 단순한 API라기 보다는 새로운 패러다임에 가깝기 때문입니다. 다시 한번 말하지만 이 글은 Rx가 아니라 반응형 프로그래밍 패러다임에 대해서 설명하는 글입니다.

앞선 내용을 차근차근 복습을 한번 해봅시다.

비동기 프로그래밍은 실행 순서와 데이터를 제어하기가 어렵습니다. 하지만 프론트엔드는 비동기 프로그래밍 덩어리입니다. 그렇기에 조금 더 간결하고 쉬운 개발을 하기 위한 관점과 체계가 필요했고 그것이 바로 반응형 프로그래밍입니다.

반응형 프로그래밍은 변경사항의 전파데이터 흐름을 중심으로 선언적 으로 작성하여 어려움을 해결합니다. 그러기 위해서는 PullPush 의 패러다임 전환이 필요하고 이를 구현하기 위해서는 Event 와 같은 방식을 통해 제어의 역전 을 만들어야 한다고 하였습니다.

여기서 우리는 다음과 같은 깨달음을 하나 얻을 수 있습니다.

아하! 변경사항의 전파 + 데이터 흐름 = Event 였구나!

그래서 우리는 반응형 프로그래밍을 다음과 같이 확장을 할 수 있게 되니다.

변경사항의 전파 + 데이터 흐름 Event + 선언적 = 반응형 프로그래밍
반응형 프로그래밍이란 이벤트를 선언적으로 작성하는 프로그래밍 패러다임

Event를 넘어 Stream으로...!

Rx에서는 이러한 이벤트의 종류를 Next, Error, Complete의 3가지 type을 정의해서 조금 더 보편적인 상황을 처리할 수 있도록 정의하였습니다. try catch finally와 같은 맥락으로 일단은 생각해두시면 좋을 것 같아요.

변경사항의 전파 + 데이터 흐름 Event 스트림(Stream) + 선언적 = 반응형 프로그래밍
반응형 프로그래밍이란 스트림을 선언적으로 작성하는 프로그래밍 패러다임

그렇습니다. 반응형 프로그래밍은 스트림을 선언적으로 작성하는 프로그래밍 방식이었습니다! ... 지금은 이해가 안가는게 당연합니다. 조금 더 빌드업과 예시를 통해 이해해봅시다.

선언적으로 작성한다는 것!

조금 더 돌아가겠지만 선언적 프로그래밍에 대해서 한번 생각해봅시다. 여러분들이 제일 많이 알고 있는 것은 바로 forArray Method 의 차이입니다. 일단 이 차이는 알고 있다고 하고 진행을 할게요.

우리는 for가 아니라 .map, .reduce, .filter 를 사용하게 되면 Array를 선언적으로 다룰 수 있다는 사실을 알고 있습니다. 그러면 이러한 방식처럼 Stream(or Event)선언적으로 다룰 수 있지 않을까요?

스트림을 선언적으로 작성한다는 것!

= Strem(or Event)를 Array Method처럼 만들어서 사용해보자!

조~금 억지스로운 예시를 하나 가져왔습니다.

다음 코드를 살펴봅시다. 랜덤한 숫자 5개에서 짝수만 2개를 골라 출력하는 프로그램 입니다.

// 랜덤한 숫자 5개를 받아서 짝수를 2개 출력하기
Array.of(1,2,3,4,5) // = [1,2,3,4,5]
  .map(x => parseInt(Math.random() * 10))
  .filter(x => x % 2)
  .slice(0, 2)
  .forEach(x => console.log(x))

일단 잘은 모르곘지만 Array의 값 대신 Stream을 다루는 객체의 이름을 Observable이라고 지어봅시다. 그렇다면 다음과 같은 코드의 형태로 작성을 할 수 있을 것 같습니다.

// 랜덤한 숫자 5개를 받아서 짝수를 2개 출력하기
Observable.of(1,2,3,4,5)
  .map(x => parseInt(Math.random() * 10))
  .filter(x => x % 2)
  .slice(0, 2)
  .forEach(x => console.log(x))

Observable는 Array가 아니라 Event라고 했으니 사용자가 클릭을 할때마다 랜덤한 숫자를 받아서 짝수를 2개 출력하기는 어떨까요?

// [클릭]할때 마다 숫자를 받아서 짝수를 2개 출력하기
Observable.fromEvent(window, "click") // [... click, .... click, ... click]
  .map(x => parseInt(Math.random() * 10))
  .filter(x => x % 2)
  .take(2)
  .forEach(x => console.log(x))

또는 1초마다 랜덤한 숫자를 받아서 짝수를 2개 출력하기는 어떨까요?

// 1초마다 숫자를 받아서 짝수를 2개 출력하기
Observable.timer(1000) // [1, ... , 2 ... , 3, ... 4, ... 
  .map(x => parseInt(Math.random() * 10))
  .filter(x => x % 2)
  .take(2)
  .forEach(x => console.log(x))

🎉 축하합니다! 이제 우리는 비동기 프로그래밍스트림(Stream)을 통해 선언적 으로 프로그래밍을 할 수 있게 되었습니다!

이렇듯 이벤트를 하나의 값으로 생각하고 이를 Array의 Method를 다루듯이 작성을 한다면 훨씬 더 직관적이고 간결하게 프로그래밍을 할 수 있을 것 같습니다. (실제로 1초마다 숫자를 받아서 짝수를 2개 출력하기를 그냥 구현을 하면 어떻게 코딩을 할지 상상해 보세요.)

스트림을 여러개 연결을 해보면 어떨까?

우리가 알고 있는 일반적인 설계는 저렇게 화살표를 통해서 데이터가 전달되는 노드의 형태로 표현할 수 있습니다.

반응형 프로그래밍에서는 전체 프로그램의 복잡도를 고려하지 않고 각 노드를 하나의 스트림으로 정의하고 선언적으로 개발을 할 수 있게 됩니다.

Data-flow 프로그래밍

반응형 프로그래밍의 두번째 키워드인 데이터의 흐름(data-flow) 은 바로 이러한 구조를 의미합니다.

각자의 변경사항을 전파를 하도록 설계를 하면 알아서 복잡한 데이터의 흐름이 만들어지지만 내부를 들여다보면 단순한 형태의 프로그래밍을 할 수 있게 합니다. 특히 이벤트를 으로 다룬다는 관점이 매우 중요합니다.

함수형 프로그래밍

반응형 프로그래밍에서는 이러한 data-flow를 함수형 프로그래밍을 통해서 구현을 하고 있습니다.

함수형 프로그래밍이란 원본값을 함수를 통해서 전달을 하고 새로운 값을 만들어내는 조합이 다시 하나의 함수가 되는 형태의 구조를 의미합니다.

그래서 아래와 같은 형태로 이벤트를 통해 데이터가 전달되는 흐름을 중첩된 Pipline을 통해서 전달을 할 수 있도록 도와줍니다.

Rx는 무엇인가요?

다시보는 ReactiveX의 정의
An API for asynchronous programming with observable streams
- 관측가능한 스트림을 통해 비동기 프로그래밍을 하기 위한 API

변경사항의 전파 + 데이터 흐름 Event 스트림(Stream) + 선언적 = 반응형 프로그래밍
반응형 프로그래밍이란 스트림을 선언적으로 작성하는 프로그래밍 패러다임

Everything is stream!

결국 반응형 프로그래밍의 최종적인 정리는 결국 이것입니다.

반응형 프로그래밍이란, 데이터의 흐름변경사항의 전파에 중점을 둔 선언적 프로그래밍 패러다임이다.
= 모든 것을 스트림으로 간주하고 선언적으로 개발하는 것

(... 결국 결론은 맨 처음 정의랑 똑같죠? ㅋㅋ 이번에는 이해가 된 채로 정의가 다시 읽혔으면 좋겠습니다.)

Array도, Iterator도, Promise도, Callback, Event를 스트림이라고 하는 하나의 관점으로 간주하고 개발을 하는 패러다임이 바로 반응형 프로그래밍입니다.

Rx 맛보기

당신은 아래와 같은 자동완성을 구현해 달라는 요청을 받았습니다. 어떻게 코딩 할 지 상상해 봅시다.

  • 타이핑 할때 마다 서버에서 데이터를 받아서 보여주세요.
  • 너무 잦은 요청은 부하가 심하니 타이핑 간격이 좁으면 대기하다가 입력이 늦어지면 그때 서버에 요청하세요.
  • 같은 내용일때는 요청하지 마세요.
  • 일정 시간 동안 응답이 없으면 3회 재시도 하고 그래도 응답이 없으면 에러 메시지를 출력해 주세요
  • 데이터는 캐시로 보관을 해서 먼저 보여주고 요청이 완료되면 새로 갱신된 데이터를 보여주세요.
  • 엔터를 누르면 서버로 요청한 건 취소하고 검색 결과를 보여주세요.

let autoCompleteRef

const input$ = fromEvent(inputElement, "input")
const enter$ = fromEvent(inputElement, "keydown").filter(event => event.key === "Enter")

const request = (data) => fromPromise(fetch("URL", ...))

// 1) 타이핑을 입력할때 마다 타이핑된 내용을 서버로 요청한다.
input$.pipe(
  map(event => event.value),
  distinctUntilChanged(), // 2) 내용이 같을 경우에는 서버로 요청하지 않는다. 
  debounce(500)  // 3), 서버 부하를 줄이기 위해 타이핑이 .5초내로 이루어 지면 기다렸다가 입력이 없는 경우 서버로 요청한다.
  mergeMap(text => request(text).pipe(
    map(res => res.result)
    tap(value => autoCompleteRef = res.value)
    timeout(7000), // 4) 서버에서 7초간 응답이 없을 경우 타임아웃 에러로 취급
    retry(3), // 4) 에러시 3회 재시도 그래도 문제라면 에러메시지 노출
    catchError(e => console.error(e)),
  ),
  takeUntil(enter$) // 5) 엔터를 눌러 검색을 하게 되면 이미 요청된 서버 내용은 무시한다.
)
.subscribe()

끝으로...

객체지향 프로그래밍, 함수형 프로그래밍과 같이 프로그램 씬에는 여러가지 패러다임이 존재합니다. 이러한 패러다임을 이해하면 프로그램을 작성할 때 일관성있고 좋은 관점을 가질 수 있고 설계와 결정에 도움을 줍니다.

반응형 프로그래밍은 스프레드 시트와 같이 미리 선언된 방식에 데이터의 변경을 전파하여 프로그램이 동작하게 만드는 방식의 패러다임을 말합니다.

이러한 반응형 프로그래밍은 비동기 프로그래밍을 조금 더 간결하게 바라볼 수 있는 관점을 제공해줍니다.

프론트엔드는 특히 비동기 프로그래밍 덩어리이므로 반응형 프로그래밍 패러다임은 프론트엔드에서 중요한 패러다임이 되었습니다. 웹 프레임워크가 그러했고 이 후 만들어진 상태관리 역시 그러합니다.

패러다임이 먼저 생겨났고 그 패러다임에 맞춰 라이브러리나 프레임워크가 나오는 것은 아닙니다. 오히려 반대죠. 특별한 문제를 해결하기 위해서 만들어진 것들의 공통점들을 모아 하나의 관점으로 정리해서 바라보는 일종의 체계와 같은 것입니다.

그래서 반응형 프로그래밍의 관점에서 바라본다면 React도 React-Query도 Redux도 Recoil도 Svelte도 얼마든지 데이터의 흐름 변경의 전파 선언적 프로그래밍 이라는 관점에서 바라볼 수 있습니다.

또한 제어의 역전 책임의 분리 느슨한 결합 data-flow programming 함수형 프로그래밍 Event를 값으로 생각하기 Everythis is stream 과 같은 개념들은 Rx뿐만 아니라 다른 프론트엔드 라이브러리를 이해하고 프로그램을 이해하고 본인의 프로젝트의 설계 방향을 정하는데에도 큰 도움이 됩니다.

객체지향을 하나의 포스트로 끝낼수가 없듯이 반응형 프로그래밍에 대해서도 깊게 들어 갈 수 있는 부분들이 있겠지만 이 글이 반응형 프로그래밍의 소개가 아니라 프론트엔드 전반의 프로그램과 라이브러리의 방향성과 관점을 약간이나마 알 수 있게 되는 계기가 되었으면 좋겠습니다.

긴 글 읽어 주셔서 감사합니다. ❤️

못다한 이야기

인터넷에서 반응형 프로그래밍을 검색을 하면 대부분 Rx를 설명하는 글이 대부분입니다. Rx는 좋은 라이브러리기는 하나 필수는 아니기에 자칫 반응형 프로그래밍 = Rx와 같은 생각으로 이어질 것 같아 Rx의 소개는 정말 최소한으로 하였습니다.

반응형 프로그래밍은 구현체나 라이브러리가 아니라 비동기를 잘 다루기 위한 프로그래밍 관점 및 체계라는 점을 다시 한번 알려드립니다.

그러면 반응형 프로그래밍과 Rx로는 어떻게 SPA를 설계하나요? 에 대한 질문에 대한 글은 따로 기회가 되면 작성을 해보도록 하겠습니다.

같이 읽어보면 좋은 글

반응형 프로그래밍(Reactive Programming)과 Rx
https://subeen.io/blog/devs/2021-09-02-Reactive-Programming/

[10분 테코톡] 🍎 그루밍의 상태관리와 반응형 프로그래밍
https://www.youtube.com/watch?v=alsCMx6vpG4

streams-reactive-programming
https://cycle.js.org/streams.html#streams-reactive-programming

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

15개의 댓글

comment-user-thumbnail
2022년 2월 4일

이런글 너무 좋습니다 잘봤습니다!!!

1개의 답글
comment-user-thumbnail
2022년 2월 4일

reactivex 에 대한 개념과 라이브러리를 접하고 패러타임이 마음에 들어 살짝 찍어먹어보고 매워서 던져버렸었습니다.
하지만 테오님 글을 읽고 다시 찍먹할 용기가 생겼습니다.

1개의 답글
comment-user-thumbnail
2022년 2월 5일

이벤트를 값으로 다룬다 = 스트림
이 부분에서 이해가 확 오는 느낌이네요 감사합니다 :)

1개의 답글
comment-user-thumbnail
2022년 2월 7일

좋은 글 감사합니다. 조금씩, 천천히 그리고 꾸준히 발전 할 수 있는 계기와 정보를 주셔서 감사합니다.

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

fxJs나 fxTs 같은 라이브러리는 어떻게 생각하시나요?

4개의 답글