들어가며
안녕하세요, CGEX Frontend Engineer 고승훈입니다. CGEX와 Coinone은 Frontend framework로 Angular를 사용하고 있습니다. Angular를 사용하는 이유는 크게 두 가지로 생각합니다.
하나는 다른 Frontend framework와 다르게 Dependency Injection이 가능하기 때문입니다. Dependency Injection을 사용할 경우 Component의 Dependency를 줄일 수 있는 것은 물론 더 Testable하고, Reusable하며, Readable한 코드를 쓸 수 있기 때문입니다.
다른 이유로는 RxJS 사용을 “Recommend”하고 RxJS가 잘 녹아들 수 있도록 Angular는 많은 노력을 하고 있습니다. RxJS가 별로 유명하지도 않을 때 RxJS를 first class citizen으로 받아들인 Angular는 참 용감했습니다. (많은 분들이 어렵다며 떠나는 것을 무시하며…)
이번 블로그에서는 RxJS가 무엇이며, Angular 환경에서 RxJS를 최대한 활용할 수 있는 pattern을 공유하고자 합니다.
Functional Reactive Paradigm(FRP)
RxJS가 무엇인지 알기 전에 큰 그림 안에서 FRP가 추구하는 방식을 이해하는 것이 우선이라고 생각합니다.
FRP가 추구하는 방식은 간단한 레시피를 이용하여 다음과 같이 설명할 수 있습니다. (Functional Reactive Programming이라는 책에서 사용하는 예제입니다.);
레시피의 순서는 다음과 같습니다.
프라이팬에 오일을 50ml 넣고 달궈줍니다.
양파가 살짝 노랗게 될 때까지 구워줍니다.
고기와 토마토를 넣고 한 번 더 구워줍니다.
……
이렇게 순서대로 또는 sequence대로 요리하는 방법이 programming에서는 procedural 하다고 합니다.
그러나 문제는 라자냐가 어떤 음식인지 모르는 분도 있을 것이며, 어떤 요리로 완성되는지 순서대로 만든 후에 알게 됩니다.
저를 포함한 보통 개발자분들은 Procedural 환경에서 State를 관리하며 개발을 하고 있습니다. 이렇게 procedure와 state 관리하는 방식을 FRP에서는 “machine space”라고 합니다.
machine space에서는 ‘프로그램이 무엇이냐’ 가 아닌 ‘이 프로그램이 무엇을 하느냐’를 표현하는 것이죠. 예를 들어;
if 조건이 붙을 때 결과값은 어떨 것이고, else일 때의 결과값은 이러하며 array를 for loop로 돌아 그 값들을 가지고 오는 코드들을 잘 생각해보면 프로그램이 무슨 프로그램인지를 알기보다 이 프로그램이 무엇을 하는지를 알게 되는 겁니다. array를 왜 for loop로 돌며 값을 가지고 오는지, ‘ if 문’과 ‘else if 문’은 무엇을 이루기 위한 체크인지 등, 결국 “무엇”을 알기 위해 많은 procedural 코드들을 읽고 나서야 “이 프로그램은 무엇이다!”라고 결론낼 수 있는 것이 procedural하고 state관리하는 object oriented programming의 현실이 아닌가 싶습니다.
반면 Functional Reactive Paradigm을 적용하여 레시피를 쓴다면 다음과 같이 쓸 수 있습니다.
라자냐는 평평한 파스타 위에 볼로냐를 놓고 그 위에 치즈 소스를 넣으며, 치즈 소스 위에 파스타를 다시 올리고 볼로냐를 놓고 그 위에 치즈 소스를 넣고 그 위에 파스타를 놓고 치즈가루를 뿌리고 45분간 오븐에 베이킹합니다.
볼로냐는 양파와 오일을 살짝 노랗게 될 때까지 구운 뒤에 고기와 토마토를 넣어 20분간 끓입니다.
치즈 소스는 우유와 치즈 그리고 룩스(roux)를 천천히 볶아 소스로 만든 것입니다. (참고로, 룩스(roux)는 밀가루와 버터를 볶은 것입니다.)
베이크는 뜨거운 오븐에 넣어 굽는 것을 의미합니다.
볶는 것이란 뜨거운 후라이팬에 올려 섞는 것을 말합니다.
위처럼 ‘무엇을 하느냐’가 아니고 ‘무엇이다’라고 표현하는 것이 FRP의 가장 큰 장점입니다. 그리고 로직보다 무엇이 문제인지를 살펴봅시다. 이렇게 문제를 살펴보는 것을 problem space라 하며 FRP는 problem space에 집중합니다. 또한 이렇게 problem space에서 집중하는 경우, 코드들을 Reactive하게 작성하기가 쉬워집니다. 예를 들어 볼로냐를 만들 때, 고기가 아닌 고기 질감이 나는 버섯으로 대체한다면 볼로냐를 사용하는 라자냐의 정의가 즉시(Reactive) 변하게 되는 것입니다.
RxJS란 무엇인가?
RxJS는 JavaScript 환경(NodeJS와 Browser)에서 Functional Reactive Programming을 가능하게 하는 JavaScript Library입니다. 즉, Angular에서만 사용할 수 있는 것이 아니라 다른 Framework에서도 사용 가능합니다. Netflix 같은 경우 React와 RxJS를 사용하고 있습니다.(https://medium.com/netflix-techblog/performance-without-compromise-40d6003c6037)
Functional Reactive Programming을 가능하게 하는 Library는 RxJS 이외에도 많습니다. RxJS가 다른 FRP Library와 다른 점은 Object Oriented Programming과 조화를 이루며 쓸 수 있다는 것입니다. 그렇기에 정말 완벽한 Functional Reactive Programming이 아니라고 생각되는 그룹이 있다면, 이 그룹은 RxJS가 Functional Reactive Programming을 대표하는 것에 의문이 드는 그룹이기도 합니다. 그럼에도 불구하고 중요한 것은 Pure한 FRP가 아니더라도 최대한 Pure한 FRP에 맞게 사용이 가능하다는 것이며, 몇십 년이 넘게 유지해온 OOP를 한꺼번에 바꿀 수 없다는 것을 고려한다면 RxJS는 다른 FRP library에 비하여 정말 실용적인 Library라고 개인적으로 생각합니다.
RxJS를 한 줄로 설명하자면 “시간이라는 개념을 Abstract Away하여 Control하는 아주 깔끔한 녀석”이라 표현할 수 있습니다. 시간이라는 개념을 없애기에 비동기 코드이든 동기 코드이든 상관없이 똑같이 다룰 수 있게 해줍니다.
RxJS 문서를 보고 어떻게 사용하는지 보면 RxJS의 기능(operator, obervable)의 사용 방법은 의외로 간단합니다. 하지만 RxJS를 Functional Reactive Paradigm을 이해하지 않고 사용하면 이해하기 힘든 코드가 생기기에 Object Oriented Programming에 익숙한 많은 개발자분들이 처음 사용할 때 RxJS의 장점을 이해하지 못하며 일부분의 기능만을 사용하여 해결하는 경우가 많은 것 같습니다.
RxJS를 RxJS 문서만 보며 사용하기 전에 Functional Reactive Paradigm(FRP)의 이해가 우선시되어야 합니다. FRP 또한 정말 간단한 Paradigm입니다. 아래 한 장의 그림으로 설명될 정도로 간단합니다;
Image Source: Angular- Introduction to Reactive Extensions (RxJS)
FRP는 정말 모든 것을 스트림으로 생각합니다. 물론 이곳에서 구체적으로 설명하겠지만 RxJS의 근본은 모든 것을 스트림(Stream)으로 생각하여 나온 개발 방법론입니다. 여기서 스트림이란 강이 흐르는 것을 비유해서 나온 단어입니다. 강을 타고 종이배가 내려가는 것처럼, Stream을 타고 Data가 이동하는 것을 의미하고 이렇게 Stream을 타고 흘러가는 Data를 가지고 로직을 구현합니다.
간단하지만 이미 머릿속에 깊숙이 박혀있는 Object Oriented Paradigm과 충돌이 나는 것 또한 중요한 포인트입니다. 그래서 정말 실력 있는 개발자가 이해를 해도 막상 사용할 때가 다가오면, “이건 좀 아닌 거 같은데”라는 생각이 들며 결국 사용하기가 꺼려지는 것입니다. 개인적인 의견으로는 OOP를 더 잘하고 더 익숙한 개발자일수록 이런 생각의 충돌이 있지 않을까 싶습니다.
이 블로그에서는 Functional Reactive Paradigm의 설명보다는 Angular 환경에서 기본적으로 알아야 할 FRP Pattern을 RxJS를 사용하여 표현하고 Object Oriented Paradigm과 최대한 충돌이 나지 않는 방법으로 시도해보도록 하겠습니다.
RxJS를 사용 하기위해 필수적으로 알아야할 Functional Reactive Paradigm 3가지 요소
RxJS를 100% 활용한다는 것은 모든 것들을 Stream으로 바꾼다는 뜻이 됩니다. 모든 것들이 Stream 이라는 것은 말 그대로 모든 것들을 의미합니다. 예를 들어 Mouse Click 도 stream, HTTP request도 Steam, Keyboard input도 Stream, LocalStorage에서 값을 가지고 오는 것도 Stream… 여러분이 개발할 때 썼던 모든 코드들은 이제 전부다 Stream으로 보내는 것입니다. 그리고 Stream이 아닌 요소들을 Stream에 올리는 행위를 FRP에서는 Lifting이라고도 합니다. 이 Stream들을 어떻게, 어디로 흘려보내며 조합(Compose)을 하느냐가 개발의 중점이 되는 것입니다. 이렇게 모든 것들이 Stream으로 변하면 하나의 Stream이 바뀌었을시 즉각적으로 (Reactive하게) 다른 Stream들도 바뀌게 할 수 있기에 State 관리가 필요 없거나 또는 최소화할 수 있게 됩니다.
실용적으로 생각한다면 Functional Reactive Paradigm은 아래와 같이 크게 3가지 요소로 나눌 수 있습니다.
Stream
Cell
Operator (Compose를 가능하게 하는 것들)
위 3가지 요소를 간단한 예제 그림으로 표현하여 설명한다면 다음과 같습니다.
Image source : Functional Reactive Pogramming (Manning)
Stream은 위 그림에서 sClicked, sRed, sGreen, sColor 부분으로 Sbutton을 클릭 시 나오는 이벤트(엄밀히 따지면 Stream이지요)를 input으로 시작하여 sColor라는 stream output으로 구성되어 있습니다.
Operator의 종류로는 Stream을 타고 들어오는 Data를 변경하는 operator와 Stream 자체를 변경하는 operator가 있습니다.
위 그림을 예제로 설명하자면 다음과 같이 설명할 수 있습니다.
위 그림의 map은 sClicked라는 스트림에 담긴 데이터를 ‘red’나 ‘green’으로 변경하여 다시 stream을 태워 내보내는 역할을 하는 Data를 변경하는 operator입니다.
merge라는 operator는 sRed와 sGreen과 같은 Stream 자체를 합쳐서 sColor라는 새로운 Stream을 만드는 operator입니다.
또한 hold라고 쓰여있는 부분을 Cell이라고 합니다. Cell은 데이터를 저장하는 공간이라고 볼 수 있습니다. Stream을 강의 흐름이라고 표현한다면 Cell은 강에서 물이 소용돌이처럼 고여있는 곳이라고 볼수 있습니다. 물이 고여있기에 이곳에서 데이터를 `hold` 즉, 저장해 놓을 수 있는것으로 Object Oriented Programming에서 State를 저장하는 역할을 한다고 볼 수 있습니다.
이렇게 모든 것들을 Stream과 Cell로 표현하고 Operator를 사용하여 Compose(조합과 로직 구현)를 함으로서 우리는 비동기와 동기 코드를 같은 Stream과 Cell을 다루는 똑같은 방식으로 개발을 할 수 있습니다.
Angular에서 위의 Stream, Cell, Operator의 조합으로 모든 로직을 구현 가능하기에 Angular 환경에서 모든 로직을 100% RxJS로 구현이 가능한 것이지요.
이제 위의 그림을 시작으로 Angular 환경에서 RxJS 100% 활용 pattern을 코드를 쓰며 설명하도록 하겠습니다.
Angular환경에서 RxJS로 Stream, Cell, Operator 구현 방법
우선 Angular 환경에서 위 그림을 RxJS로 코딩한다면 다음과 같이 쓸 수 있습니다.
위 코드는 아래 링크에서 작동 가능합니다;
angular-byblbd - StackBlitz Starter project for Angular apps that exports to the Angular CLIstackblitz.com
위 코드를 이해하기 위해서는 기본적인 RxJS 지식이 필요하기에 RxJS를 처음 접하시는 분은 아래 비디오를 먼저 보시면 위 예제를 더 쉽게 이해할 수 있습니다.
SButtonRed와 SButtonGreen은 template의 click event에 바인딩 하여 클릭 시 해당 Subject의 next method를 불러 데이터를 Stream에 보내고 있습니다. 지금과 같은 경우, Subject이 next method로 Lifting 하는 값은 void입니다. 이 Button Subject에서 나오는 Stream이 바로 sClicked Stream이며 sClicked Stream 에서 나오는 Data(void)를 `map` operator를 사용하여 ‘red’나 ‘green’ String 값으로 변환 후 sRed와 sGreen의 Stream으로 보냅니다. 이때 생성되는 Stream은 새로운 Stream이고 이것은 Observable로 변환된 Stream이지요.
이렇게 새로 생성된 sRed Stream과 sGreen Stream을 merge하여 또 다른 새로운 sColor Stream이 만들어지고 이 sColor에서 Stream을 따라 흐르는 Data는 바로 SLabel Cell에 저장이 됩니다.
RxJS에서는 Cell을 BehaviorSubject이라는 class로 표현을 합니다. BehaviorSubject은 생성할 시 기본값을 항상 주어야 하며 위 예제에서는 기본값으로 empty string (‘’)을 주었습니다.
Cell (BehaviorSubject)에서 저장(‘hold’) 하고 있는 값을 template에 연결해주는 역할을 하는 것은 Angular에서 제공하는 `async`라는 파이프로 위 template부분을 보시면 {{ SLabel | async }} 라고 쓰여 있는 것을 볼 수 있습니다. 이것은 Angular가 SLabel의 값을 가지고 오기위해 SLabel에 subscribe를 하는 것입니다. subscribe은 Stream을 흐르게 하기 위해 하는 행위라고 보면 됩니다.
즉, subscribe이라는 method가 불리기 전에는 기존에 생성된 Stream을 흐르게 하지 않습니다. Stream을 강물이 흐르는 것에 비유했다면, subscribe은 강에 설치되어있는 댐을 개방하는 역할입니다. 댐을 열어야 강의 물이 흐르듯이 subscribe을 해야 Stream이 흐르기 시작합니다.
위 코드 예제처럼 모든 로직을 Stream 으로 구현할 수 있습니다. 이것을 Generalize하게 되면 다음과 같은 pattern이 나옵니다.
Generalized Pattern은 다음과 같은 규칙을 갖고 있습니다;
모든 Stream과 Cell은 이름에 Stream이라는 표현으로 $를 post-fix 합니다. 예외로는 Stream을 멈추게 하기 위한 _unsubscribe이라는 이름의 Subject이 있습니다.
User Action (click, input, 등등)은 Subject로 구현하며 Action Stream을 표현하는 A$를 post-fix 합니다.
Template에 바인딩 되는 값들은 Cell (BehaviorSubject)으로 구현하며 Template View로 가는 Stream으로 표현하는 V$로 post-fix합니다.
Stream을 input으로 받아 새로운 Stream이 만들어 지는 행위는 constructor 안에서 합니다. pipe로 “건설(construct)”하는 행위들이기에 constructor에서 하는 것이 적합하기 때문입니다.
Subscribe하여 Stream을 흐르게 하는 행위는 ngOnInit에서 합니다. ngOnInit은 component가 생성됐을 때와 ngOnDestroy는 component가 없어질때 부르는 LifeCycle hook으로 Angular의 component caching 방식에 따라 constructor는 한번 실행되지만, ngOnInit과 ngOnDestroy는 여러 번 불릴 수 있습니다.
Component가 없어지면 기존에 Subscribe되어 Stream이 흐르고 있는 것들을 닫기 위해 Stream을 막아주는 로직이 필요합니다. 모든 Subscription 전에 takeUntil(unsub$)로직을 써주고 Component가 destroy 될 때 unsub$으로 값을 쏴 stream을 닫아주는 역할을 합니다.
위 Generalized Pattern을 사용하면 Angular 환경에서 100% RxJS를 사용하여 로직 구현이 가능해집니다.
이제 위 Generalized Pattern을 사용하여 거래소에서 사용할만한 비교적 복잡한 로직을 RxJS로 구현하는 LiveCoding Video 를 마지막으로 이 섹션을 마무리 하겠습니다.
마무리하며
다음 편에서는 Opinionated Angular Project Structure를 다룰 예정입니다. 위 비디오에서 보이던 Service, Repository pattern에 대한 자세한 설명을 다루며 실제 CGEX에서 사용하고 있는 Project Structure이기도 합니다.
고승훈, Frontend Engineer, CGEX