RxJS 반응형 프로그래밍을 읽으면서 느낀 것들을 제 입맛에 맞게 적은 글입니다!
따라서 글들이 똑같지 않고 다르다는 점, 유념하며 읽어주세요.
기본적으로 데이터를 주고 받는다는 것은 무엇일까요?
주고 받는다는 것은 통신을 한다는 것이고, 통신에는 항상 리소스가 존재합니다.
따라서 물리적으로나, 시간적으로 비용을 지불하고, 유저는 이에 대한 대가로 데이터를 시각적으로 볼 수 있는 거죠.
따라서, 데이터를 잘 처리하기 위해 현재 컴퓨터나 웹은 고도의 동시 처리를 지원해주려 노력하고 있어요.
그러나, 아직 이를 구현하는 프로그래밍은 여전히 콜백 패턴에 의존하고 있던 상황이었어요.
콜백 패턴은 비동기를 처리하기 위해 어쩔 수 없이 선택한 대안이었지만, 데이터를 여러 개를 조합해서 쓸 시 복잡도가 기하급수적으로 늘어났어요.
이러한 배경에서 RxJS는 탄생했군요.
포인트는, 데이터를 비동기적으로 다루는 데 있어 안정성과 확장성이 떨어졌고
이에 대한 대안으로 함수형, 반응형 프로그래밍인 RxJS의 필요성이 생긴 것입니다.
동기 연산과 비동기 연산의 핵심은, 로직을 블로킹하느냐, 논블로킹하느냐에 따라 달려있습니다.
즉, 순서를 보장하지 않는 대가로 시간을 단축시키는 트레이드 오프의 관계인 것인데요.
이러한 비동기 연산은 어떻게 가능한 것일까요?
callback
이라는 단어를 먼저 살펴볼 필요가 있어요.
호출을(call) 다시 뒤로 돌린다(back)라는 말에서 이 함수가 명명된 것입니다.
비동기 요청과 함께 사용하면, 이 로직을 둘러싼 콜백 함수는 데이터가 올 때까지 대기하고, 애플리케이션은 다음 줄의 코드를 실행하게 돼요.
한편, 이러한 지연된 코드가 데이터를 받게 되면 다시 제어권을 양도 받는데요.
보통 이러한 방식은 동기 연산에서는 말도 안되는 일이죠.
이처럼, 제어권이 엉뚱하게 이전의 코드로 넘어가는 현상을 제어의 역전이라고 합니다.
우리가 세상을 바라보는 시각은, 마치 동기적처럼 이루어집니다.
하지만, 때때로 생각지 못한 것들이 어디선가 시작되어, 결론적으로 우리가 원치 않는 일들이 일어나고는 하죠.
동기와 비동기 역시 마찬가지에요.
모든 일련의 일들은 얼핏 선형적으로 보이지만, 사실 동시에 일어나는 일들이 존재하고 있어요.
그리고 이러한 동시적인 일들이 잘못 만난다면, 결과적으로 그것을 위기(이슈)라고 하죠.
우리는 이러한 이슈들을 잘 관리하려고 노력해요. 이것을 전략이라고 합니다.
RxJS는 이러한 비동기에서 일어날 수 있는 사이드 이펙트를 순수함수를 통한 프로그래밍 전략으로 최소화하는 접근법을 택했다고 할 수 있겠네요.
콜백 중첩은 마치 비동기라는 병렬적 방식에, 동기의 직렬적 방식을 더합니다.
즉, 모든 것은 동시에 처리되는 것 같은 느낌을 자아내지만, 이후의 순서를 보장할 수 있도록 실행하는데요.
비동기를 통해 한창 미래를 고려하는데, 갑자기 비동기의 동기적 절차까지 생각해야 한다니! 꽤나 복잡해지죠.
그런데 이런 연산의 복잡도뿐만 아니라, 코드도 점차 중첩 횟수에 따라 스코프가 깊어지면서, 콜백 지옥을 만들어냅니다. 콜백 지옥이 무서운 이유는, 그 깊어지는 모양에서 비롯된 유지보수를 어렵게 하기 때문인데요.
특히 비동기 연산을 제대로 고려하지 못한 콜백은 그 안에서마저 순서를 보장하기 힘들기도 합니다.
대표적으로 for
문이 있죠. for
문은 콜백을 통한 반복이 아닙니다. 따라서 제어의 역전이 일어나지 않고 순차적으로 훑어버리기 때문에, 순서를 보장해버리지 않아버립니다.
이에 대한 해결방법은 요새 나온 for await
를 쓰거나, 혹은 reduce
, forEach
등 콜백을 인자로 받아 클로저를 생성하여 처리하는 메서드를 사용해야 하죠.
여기서 드러나는 특성이 있어요.
바로 함수형 프로그래밍은 더이상 루프를 사용하지 않는다는 것이죠.
콜백을 통해 좀 더 안정적으로 문제를 개선하는 방법을 제시합니다.
다시.
콜백의 방식은 분명 함수형 프로그래밍인 RxJS에도 영감을 주고 있다고 보여요.
다만 어떻게 더 좋게 문제를 개선하느냐에 초점을 맞췄는지를 살펴 보는 게 좋은 Focus겠군요.
우리가 DOM으로 EventListener을 달아, 특정 이벤트가 발생하면 어떤 동작을 하도록 콜백에 명령을 내립니다. 이러한 방식으로 비동기 이벤트들을 관리하는 방식이 바로 이벤트 이미터죠.
하지만 모든 이벤트들을 등록하고, 구독하는 방식 역시 결국에는 콜백 패턴을 벗어나지 않았기에 본질적 문제를 공유하고 있음은 동일합니다.
어떻게 하면 이 중첩을 해결할 수 있을까요?
이에 대한 대안으로, ES6에는 프로미스를 통한 개선된 패턴을 제시합니다.
흐름 파악과 유지보수를 위해서는 더이상 중첩하지 않아야 한다.
어떻게 하면 중첩된 비동기 흐름을 좀 더 잘 제어할 수 있을까.
Promise는 이러한 절망 속에서 태어난 대안이었습니다.
함수형 프로그래밍에서 착안된 프로미스 객체는 체이닝을 통한 연속된 동작을 규정할 수 있는 한편, 동기적 방식에서 비동기 연산을 위한 제어의 역전 역시 잘 반영할 수 있었어요.
심지어 체이닝을 통한 연속된 동작들을 then
으로 체이닝하는 방식은, 선언적으로 프로그래밍할 수 있도록 해주었지요.
저도 처음 알았는데, 프로미스 객체는 여러 값을 생성하는 데이터 소스는 처리할 수 없다고 합니다. 대표적으로, 파일 스트림의 바이트 시퀀스가 있다고 하네요.
또한 abort
가 불가합니다. 이는 불변성을 지키는 객체를 보장했기 때문인데요.
결론적으로 프로미스 객체 역시 안정성을 보장하기에는 다소 몇 가지 제약이 존재한다는 것을 알 수 있군요.
우리는 콜백 패턴이 대두되었던 상황 속에서 비동기를 처리하기 위한 방식으로 2가지를 살펴보았어요.
각자에는 분명 단점이 존재했으며, 그렇다고 이 2개를 합치는 것은 어떻게 해야할지 막막합니다. 안정성 있는 비동기를 위한 또다른 패러다임은 어떤 방향을 제시해줄까요?
RxJS는 이러한 막막한 함수형 프로그래밍과 반응형 프로그래밍의 중간점을 제시합니다.
특히 이러한 비동기를 연산하며 이러한 고민들이 있었다면 RxJS는 분명 좋은 대안이라고 합니다.
저는 이 내용들을 보며 갑자기 되게 설렜어요. 어떻게 도움을 줄까요? 🥰
try/catch
가 너무 많아지니... 어떻게 처리해야 하죠?와... 생각보다 많이 공감가는 내용들이었어요.
다만, 이제 말뿐인 아닌 방법을 제시해야 할 때라고 생각할 것 같아요.
RxJS는 어떤 대안을 우리에게 제시할까요?
결국 어떤 동작이 수행되기 위해서는 '지연시간'이 존재합니다.
이 지연시간이라는 것을 추상화하여, 그 존재를 반영할 수만 있다면?
더이상 지연시간으로 인해 비동기의 순서를 복잡하게 연산할 필요가 없어요. 그저 분명 지연은 어느정도 된다라고만 생각하면 되니까요. 그리고 이 역할을 이벤트가 해냅니다.
그렇기에 마치 선형적으로 이제는 문제에 접근할 수 있다!는 것이 바로 RxJS의 아이디어입니다.
RxJS
는 일관성 있는 처리를 위해 모든 데이터를 같은 방식으로 처리합니다.
이 책에서는 이를 데이터 스트림이라는 개념으로 정의하고 있어요.
스트림이란 대개 프로그래밍 언어에서 데이터를 처리하기 위한 추상 객체인데요.
반응형 프로그래밍에서는 소비할 수 있는 모든 데이터 소스를 의미합니다.
RxJS에서 이 데이터 스트림은 변화의 전파를 토대로 다음 값에 영향을 미치도록 합니다.
책에 있는 예제를 통해 살펴봅시다.
// 스트림은 RxJS에서 핀란드 표기법으로 보통 표현하는 것이 관례입니다.
A$ = [20];
B$ = [22];
C$ = A$.concat(B$).reduce(adder);
A$.push(100); // 만약
C$ = ?
결과가 어떤 것 같나요? 답은 122입니다. 어떤가요. 이상하지 않나요?
명령형 프로그래밍에 익숙했다면 아시겠지만, 원래는 push
메서드를 사용한 동작이, C$에는 영향을 미치지 않습니다.
하지만 스트림은 전파를 통해 데이터를 조작합니다.
A$
라는 데이터를 소비했다면, 그 친구들은 모두 A$
라는 데이터의 변화에 영향을 받습니다.
즉,
C$
는A$
라는 데이터의 변화에 반응했습니다.
그렇다면C$
는 현재로써A$
라는 상태를 반영한 시점에서, 가장 최신의 값이라는 것이죠. 이것이 반응형 프로그래밍입니다.
이를 확장해서 생각해볼까요?
만약 이 반응을 토대로, 변화에 대한 감지가 일어나면 액션을 발생시킨다면?
그렇다면, 전달된 이 값은 적어도, 그 감지한 상태를 기준으로 했을 때의 값을 보장할 수 있지 않을까요?
대표적으로 이벤트 리스너가 있죠. 인풋 이벤트를 받으면, 그 값을 핸들링해서, 결과를 반영하죠. 어때요, 이제 감이 잡혔나요?
💡 뭔가 굉장히 재밌는 포인트였어요.
저도 최근에 라우터를 직접 구현하면서 이벤트 기반으로 그 상태에서의 데이터 값을 페이로드로 전달하는 아키텍처 방식을 채택했는데, 이것이 반응형 프로그래밍이군요!
자, 우리는 이제
그렇다면, RxJS
의 역사도 한 번 살펴보죠.
처음에는 매튜 포드위소키가 MS에서 만든, Rx.Net에서 가져온 오픈 소스 프레임워크였습니다.
이후에는 넷플릭스에서 벤 레시를 필두로 한 커뮤니티에서 주도적으로 발전시켰어요.
그렇게 발전된 RxJS
5버전에서는 API 외형을 극도로 단순화시키며, 새로운 아키텍처를 기반으로 성능을 높일 수 있었어요.
RxJS는 스스로를 옵저버블 스트림을 사용하는 비동기 프로그래밍용 API라고 정의합니다.
옵저버블은 처음 들으니 그렇다치고, 우리가 현재 얼핏 들은 건 스트림이에요.
그렇다면, 데이터를 스트림에서 어떻게 다루는지를 살펴볼까요?
우리는 웹에서 일련의 동작을 합니다.
마우스를 움직이는가 한편, 입력을 하기도 하죠.
이렇게 상태의 변화는 전파가 되어 특정 값을 변화시킵니다. 웹은 이를 기반으로 다시 시각적으로 보여주죠.
한편, 이러한 이러한 값들이 필요할 수 있어요. 우리는 이를 위해 구독합니다.
구독을 함으로써, 기존의 컴포넌트가 다르게 변화하도록 제어를 하죠.
그리고 이렇게 구독한 소비자에게 어떤 결과가 오기까지, 우리는 내부 로직의 도움을 받아요.
즉, 값을 처리하는 일련의 로직들을 모아, 우리는 파이프라인이라고 합니다.
결국 말은 복잡했지만, 스트림이라는 건 별 거 없습니다.
소비할 수 있는 데이터는 결국 이벤트에 따라 상태를 전파할 수 있다는 개념이죠.
여태까지 잘 이해했다면, 스트림은 결국 반응형 프로그래밍에서 전파를 통해 상태가 변화하는 데이터이고, 어떤 객체라고 생각할 수 있겠어요.
RxJS는 옵저버 디자인 패턴을 기반으로 설계되어 있어요.
스트림 역시 이를 지켜 설계됐는데요.
실제로 구독하지 않는다면, 스트림은 아무 일도 일어나지 않아요.
굳이 전파를 하며, 메모리를 불필요하게 사용할 이유가 없기 때문이죠.
이러한 특성은 lazy하며, 바로 실행하는 Promise와 차별화된 패턴입니다.
나아가, 이러한 데이터 소스들은 동적이에요.
정확히 하나로 고정되어 있지 않고, 파이프라인을 따라 변화의 전파에 영향을 받죠.
데이터는 순수함수로 이루어진 파이프라인을 따라, 흐름대로 값을 반영해요.
결국 이러한 스트림 덕분에, 새로운 패러다임으로 옮길 수 있는 것입니다. 🎉
RxJS는 함수형 프로그래밍이며, 함수는 자바스크립트의 일급 객체이죠.
이를 기반으로, 시간에 따른 이벤트 시퀀스를 제시하는데요. 이는 자바스크립트를 위한 이벤트 하위 시스템으로 구성되어 있습니다.
이벤트 시퀀스는 지연 시간과 대기 시간을 고려해야 하는 비동기의 한계를 극복하여, 마치 선형적으로 데이터를 변환하는 것처럼 프로그래밍할 수 있도록 해줍니다.
서비스가 커지면 커질수록, 서비스의 유형에 따라서 불규칙한 데이터들은 실시간적으로 업데이트 됩니다.
이러한 복잡성이 높아질 수록 안정성 있는 데이터 처리는 무엇보다 중요한데요. RxJS는 스트림을 잘 처리하기 위한 컴포넌트를 통해 이를 극복해줍니다.
RxJS의 철학이 무엇이었죠?
어떻게 하면 비동기를 잘 처리할 수 있을지에 대한 대안을 만들자는 것이었죠!
따라서 항상 '스트림이란 데이터를 어떻게 잘 처리할 수 있을지'가 주된 관심사에요.
RxJS는 이를 관리하기 위한 컴포넌트를 다음과 같이 구성했습니다.
데이터의 원천을 제공합니다.
모든 로직의 시작점은 생산자로부터 발생하며, 생산자는 이벤트로부터 만들어집니다.
대개 옵저버 패턴에서는 이러한 생산자를 서브젝트라고 말하지만, RxJS에서는 관찰할 수 있는 것이라는 의미로 옵저버블이라 부릅니다.
생산자는 이때, 다시 또 이벤트를 통해 소비자들에게 전달해주고 이후의 처리에는 관심을 갖지 않아요. 이러한 생산자의 동작을 fire-and-forget
이라 부릅니다.
어떤 스트림을 받기로 구독한 친구입니다. 옵저버라고 부릅니다.
소비자가 구독하는 순간, 생산자는 스트림이라는 데이터를 생성합니다.
이후 생산자가 fire-and-forget
하면 이벤트는 파이프라인을 타고 푸시되기 시작하는데요.
여기서의 푸시는 역전 현상이 일어나지 않아요.
항상 워터폴하게, 위에서 아래로 흘러내려갑니다.
이러한 생산자와 소비자는 서로 느슨하게 결합된 형태를 유지하므로, 컴포넌트의 모듈성이 좋다는 장점이 존재합니다. (생산자가 누구던지간에, 새롭게 바꾸어도 똑같은 처리를 해줄 수 있죠)
업스트림에서 다운스트림으로 넘어가는 일련의 동작들이 모여 하나의 파이프라인을 이룹니다.
데이터 조작은 생산자와 소비자 사이에서 데이터만을 처리하도록 해줘요.
다르게 말하자면, 소비자가 원하는 처리 방법이 달라져도, 이벤트에 따른 결과 값을 쉽게 얻을 수 있도록 다른 파이프라인을 붙이면 그만이라는 이야기입니다.
즉, 파이프라인을 통한 모듈성의 향상은 생산자, 소비자가 각각의 역할에 맞는 결과를 달성할 수 있도록, 단일 책임의 원칙을 지키는 데 도움을 줍니다.
비동기에서 시간은 매우 유동적이에요. 가령 스케줄링에 따라 순서가 달라지기도 하고, 컨텍스트 스택이 포화상태라면 대기해야 하므로 지연 시간이 발생하기도 하죠.
그러나 확실한 것은, 스트림은 그 시점에서 상태가 변화됐다라는 사실입니다.
변화를 감지하고 값을 업데이트하며, 다시 다운스트림으로 전파를 하는 일련의 방식은 결과적으로 선형적으로 문제를 처리할 수 있음을 보장합니다.
생산자와 소비자, 파이프라인을 설계한다는 것은 이 시간을 항상 고려해야 해요.
특히 RxJS는 정형화된 패러다임을 고수하지 않습니다. 원하는 기대 결과 및 목적에 따라 다양한 패러다임을 조합하며, 비동기를 '잘' 처리하기 위해 노력할 뿐이죠.
프로그래밍 패러다임은 현실의 복잡한 문제를 해결하기 위해 그 대상을 추상화합니다.
그리고 그 차이는 관점에서 발생한다고 생각하는데요.
OOP는 객체는 어떤 상태를 갖고 있다고 생각하고, 이를 중심으로 배치합니다.
함수형 프로그래밍은 결국 그 문제 풀이의 관점을 동작으로 초점을 맞춥니다.
한편, 반응형 프로그래밍은 스트림의 변화에 집중하며 이러한 현실의 문제를 해결합니다.
차이를 느끼시나요?
중요한 건, 이러한 각 관점들이 완전히 다른 패러다임에 대치된다는 것이 아닙니다.
다만 어떤 것을 집중하며 문제를 효과적으로 해결할지에 대해 관심을 가질 뿐이죠.
RxJS는 결과적으로 이러한 패러다임을 모두 가능하게 합니다.
다만 이것은 기억해야 합니다.
결국 RxJS가 비동기를 안정적으로 연산하기 위해 사용한 것은 RP기반이며, 이는 스트림에서 생각해야 한다는 것이죠. 나아가, FP를 통해 선언적으로 관리한다는 것만 기억해도, RxJS의 첫 시작을 했다고 하기에는 충분한 것 같아요 🚀
RxJS
를 처음 살펴보았는데요. 이 리듬을 체화시키기까지 꽤나 고민했어요.
그렇지만, 확실히 재밌어요. 당장 머리 속으로만 구상을 해도, 어떻게 흐름이 이루어져 있을지가 짐작이 되기도 해요.
특히 옵저버 패턴에 대해 관심이 생겼습니다.
요새 OOP로 설계를 하다 보니 효과적인 컴포넌트 설계에 대해 고민이 들었는데요.
생산자와 옵저버로 분리하고, 이를 파이프라인을 통해 처리한다는 것이 꽤나 명료하고 유연하다는 생각이 들었습니다.
최근에 이런 다양한 디자인 패턴을 어떻게 실습할지 고민하며 프로젝트를 하나 구상했는데요. 조만간 한 번 옵저버 패턴도 구상해보아야겠어요.
RxJS
는 아마 2주간은 계속해서 공부할 것 같아요. 설레는데요! 😆
역시 배운다는 건 항상 제게 열정을 주는 것 같아요. 이상!