프로그래밍 패러다임 관점에서나 탄생 이유의 관점에서나 리액티브(Reactive)를 이해하기 위해서는 현재 개발자와 회사가 직면한 문제들과 10년전의 문제를 비교하여 생각해보아야한다.

개발자와 회사에 바뀐 두가지는 아래와 같다.
  • 하드웨어의 발전
  • 인터넷

"모닥불 주위에 모여 옛날일을 이야기하는 것"은 일부에의해 대화의 수준이 낮아질 수 있음으로 고려해야하는 반면, 우리는 모든 개발자가 직면한 문제를 고심하기 위해 직종의 역사를 탐구해볼 필요가 있다.

왜 이것들이 이제 달라졌을까
10년간 컴퓨팅 연구 끝에 발견한 의미있는 지식이 하나 있다. 리액티브 프로그래밍은 소프트웨어의 새 세대를 만들기 위한 시도이다.

1999년
1999년, 내가 캐나다 임패리얼 상업은행에 있고 처음 자바를 배우기 시작할 무렵이다.
  • 인터넷은 2.8억 유저에 달성했다.
  • JSEe는 여전히 썬(Sun MicroSystems)사의 꿈으로서 가슴속에 있는 상태였다.
  • 온라인 뱅킹이 5년정도 된 유아기 시점이었다.
1999년으로 돌아가면 나는 동시성에 대한 문제에 직면하고 있었다. 그 해결책은 스레드와 락(lock)에 관련이 있었으며, 유경험자의 개발자까지도 그 문제를 해결하기 힘들어하는 상황이었다. 자바의 특징은 "한번 작성하면, 어디서든 실행할 수 있다"인데, 여기서 "어디서든"은 JVM이 설치되어있는 OS에 한에서 "어디에든"이지, 클라우드나 IoT 세대를 위해 설계한 다른 동시접속의 개념이 아니다.

2005년
2005년이 오래전은 아니지만 컴퓨팅쪽과 인터넷쪽이 크게 바뀌었다. J2EE, SOA, XML이 인기를 끌었고 루비온레일즈가 J2EE의 고통받는 컨테이너 기반 개발 문제를 해결하기 위해 탄생하였다.

이때의 인테넷에는
  • 1억유저가 있었다.
  • 페이스북이 550만 유저를 가지고 있었다.
  • 유튜브가 탄생했다.(2005년 2월)
  • 트위터가 아직 없었다.(2006년)
  • Netflix가 비디오 스트리밍을 소개했다.(2007년)

2014년
이 글을 쓰고 있는 시점인 2014년은 Internet Live Stats에 의하면 약 2,950,000,000(29억 5천만)정도의 인터넷 유저가 있다고 한다. 중국이 혼자서 6억 4천만 인터넷 유저를 보유하고 미국이 2억 8천만을 보유하고 있었다.

오늘날 가장 인기있는 웹사이트이다.
  • 페이스북—13억 유저
  • 트위터—2억 7천 유저

시간이 흐르면서 한 웹사이트의 트래픽이 지난 20년전의 인터넷 전체 트래픽보다 많다.

1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수



우리는 점점 확장과 예측에 관한 이슈가 중요해지고, 우리의 삶에서의 소프트웨어가 중요해짐을 쉽게 확인할 수 있다. 또한 과거의 패러다임이 현재까지 이어질 수는 없을 것이고, 분명 미래까지도 이어지기 힘들 것이다.

4가지 리액티브 요소
리액티브 앱은 4가지 요소로 구성되있다.

  • 반응성(responsive)있는 앱이 그 목표이다.
  • 반응성있는 앱은 확장가능(scalable)하고 탄력(resilient)있다. 반응성은 확장성과 탄력성 없이 불가능하다.
  • 메시지-주도(message-driven) 구조는 확장가능함과 탄력있음을 근간으로 하고 궁극적으로 반응성있는 시스템을 기반으로 한다.

반응성(Responsive)
"앱이 반응성 있다"는 것이 어떤 의미일까?

반응성있는 시스템은 시종일관으로 분명한 유저 경험을 보장하기위해 좋은 상황이나 나쁜  상황이나 상관없이 모든 유저에게 즉각 반응하도록 하는 것이다.

외부 시스템의 실패나 트래픽 급증과 같은 다양한 상황에서의 재빠른 처리분명한 사용자 경험탄력성확장성이라는 두 성질에 의존한다. 메시지-주도 구조는 반응성 있는 시스템을 위한 전반적인 근간을 제공해준다.

왜 반응성 있는 것이 메시지-주도 구조에 중요할까?

세상은 비동기적이다. 당신이 한 포트의 커피를 끓이는데 크림과 설탕이 없음을 깨닭았을때의 그 예시가 있다.

아래는 그 한가지 방법이다.
  • 한 포트의 커피를 끓이기 시작한다.
  • 커피가 끓는동안 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 즉시 커피를 마신다.
  • 여유를 즐기면 된다.

또 다른 방법이다.
  • 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 커피를 끓을 때까지 시계를 보면서 기다린다.
  • 카페인 금단현상을 겪는다.
  • 젠장

여러분이 볼 수 있듯, 메시지-주도 구조는 당신을 시공간으로부터 분리된 비동기 바운더리를 가능하게한다. 우리는 남은 이 포스트에서 비동기적 바운더리 개념에 대해 이야기해볼 것이다.

왈마트 캐나다(Wlamart Canada)에서의 일관성
Typesafe에 들어가기 전에, 나는 왈바트 캐나다의 플랫폼을 만든 Play and Scala 팀의 기술 리더였다.

우리의 목표는 분명한 사용자 경험의 일관성을 만드는 것이었다. 다음 것들에 관계없이 말이다.
  • 데스크탑, 테블릿, 모바일 기기등 어떤 기기에서도 walmart.ca를 서핑할 수 있다.
  • 현재 피크 트래픽이 튀어오르든 유지되든 관계없어야 한다.
  • 전체 데이터 센터의 손실과 같이 주요 기능이 실패해도 관계없어야 한다.

응답 시관과 전반적인 사용자 경험은 위 시나리오와 관계없이 일관성있다. 일관성은 당신의 웹사이트를 전달하는데 근본적으로 중요하며 오늘날 웹사이트는 당신의 브렌드임을 생각해야한다. 좋지않은 사용자 경험은 실제 상점이 아니라 온라인에서 일어나기 때문에 쉽게 잊어지거나 무시되지 않는다.

Glit에서의 반응성있는 소매(retail)
전자상거래 도매인에서의 일관성은 우연에의해 일어나지 않는다. Glit의 경우, 매일 저녁에 하루 세일을 공지하는데, 그 때 트래픽이 급증하는 플래시 스케일 사이트이다. 플래시 스케일 사이트의 사용자 경험에대해 이야기해보자. 만약 당신이 오전 11:58에 한번 Glit를 접속하고 오후 12:01에 한번 더 접속한다면 Glit는 일관성있게 정해진 응답 경험을 제공하고 이것을 리액티브를 사용하여 시행하였다. 스칼라 기반의 마이크로 서비스 구조로 마이그래이션한 Glit를 더 배우고 싶다면 interview with Eric Bowman of Gilt 여기를 보아라.

탄력있는(Resilient)
많은 앱들이 이상적 환경만을 고려하면서 설계, 개발하지만 사실 이상적이지 않을때도 많다. 이것은 매일 주요앱 기능이 실패하는 다른 보고서를 받거나, 해커에의해 서비스 멈춤, 데이터 손실, 지속적인 손상을 초례하는 다른 co-ordinated breach 시스템이 있을 수 있다.

탄력있는 시스템은 이상적이지 않는 상황에서도 반응성을 보장하기 때문에 바람직한 설계 요소와 구조 요소를 사용한다.

자바와  JVM은 다중 OS에서 한 앱을 문제없이 실행시키는 것에 대한 것이었다면, 201x년대의 상호연결된 앱은 앱단의 구성, 연결성, 보안성에 대한 것이다.

이제 한 앱은 웹사이트나 다른 네트워크 규약을 통해 통홥되어 여러 앱으로 구성되어있다. 오늘날 하나의 앱은 신뢰된 방화벽을 가진 바깥은 외부 서비스들에(10개, 20개, 혹은 더 많이) 의존하며 만든다.

이 복잡한 통합을 고려하면 얼마나 많은 개발자가 필요할까?

  • 모든 외부 의존성을 분석하고 모델링하는 사람
  • 통합된 각 서비스의 이상적인 응답 시간을 문서로 만들고 피크일때나 아닐때 모두 퍼포먼스 테스트가 초창기 기대와 일치하는지 확인하기위해 관리하는 사람
  • 모든 퍼포먼스, 실패, 핵심앱 로직으로 포함되는 다른 비기능적인 요구사항을 문서화하는 사람
  • 각 서비스의 모든 실패 시나리오를 다시 분석하고 테스트하는 사람
  • 외부 의존성의 보안성을 분석하고, 외부 시스템과 통합했을때 새로운 취약점이 있는지 알아내는 사람

탄력성은 가장 정교한 앱의 가장 약한 연결 중 하나이지만, 추가적으로 탄력있어야하는 것은 곧 끝난다.(원문: Resiliency is one of the weakest links of even the most sophisticated application, but resiliency as an afterthought will soon end.) 현대의 앱들은 이상적인 상황 보다는 다양한 현실세계에서 반응성을 유지해야하기 때문에, 앱의 핵심(core)이 반드시 탄력적이어야한다. 퍼포먼스, 내구성, 보안성은 모든 면에서 말이다. 여러분의 앱은 단지 몇 부분이 아닌 모든 부분에 걸쳐 탄력적이어야한다.

메시지-주도 탄력성
메시지-주도 핵심에서 가장 아름다운 제작방법은 여러분이 자연스럽게 작업에 필요한 것들을 한조각 한조각 얻어내는 것이다.

고립(Isolation)은 시스템의 자체 회복을 위해 필요하다. 잘 고립되있을때, 우리는 실패의 위험이나 퍼포먼스 특징, CPU와 메모리 사용 등과 같은 요인에 기반하여 여러 타입으로 나눌 수 있다.

정확한 위치는 마치 같은 VM에서 동작하게 한 것 처럼 서로다른 노드위에서 서로다른 프로세스들이 상호소통할 수 있게 해준다.

특정 목적용 에러 채널은 단지 에러 신호를 호출자에게 던저버리는게 아니라 다른 우리가 원하는 곳으로 보낼 수 있다. 

이러한 사실은 우리 앱에서 확고하게 에러 핸들링을 구체화하고 결함에 내성을 만들어준다. 이것은 아카의 감독 계층(Akka's supervisor hierarchies)처럼 구현함으로서 입증되었다.

조각(block)을 만드는 핵심은 이상적인 환경 뿐만 아니라 좋지않은 환경에서도 메시지-주도 구조가 탄력성에 기여하는 것에의해 제공하고 다음으로 반응성에 기여한다.

44억 달러의 탄력성 실수
2012년에 를 생각해보자. 소프트웨어가 향상되는동안 통합된 앱 방식은 점점 인기있고(fired up) 거래 규모가 점점 커지기 시작했다.

다음은 45분동안 일어나는 악몽같은 시나리오였다.

Knight의 자동교환 시스템이 잘못 거래하여 나스닥(NASDAQ)을 침수시켰고, 10억달러가치를 의도치않은 회사에 놓았다. 이러한 사고는 다시 반환(reverse)하는데 회사에 44억달러의 비용이 들게 되었다. 나스닥이 침수되고 거래의 범람을 고치는 동안 나스닥은 Knight 돕는 것을 중단했다. Knight의 주식은 하루만에 63%나 떨어졌으며 그들은 가까스로 살아남았고, 일부를 회복한 후에 투자자에의해 다음 인계를하고 살아갈 수 있었다. 

그때 Knight의 시스템은 동작했었지만 탄력성이 없었다. 탄력성이 없는 Knight 시스템은 문제를 확장시키는데 한 몫을 하였다. Knight는 최악의 버그가 발생했을때 그들의 시스템을 끌 수 있는 스위치(kill-switch) 매커니즘도 없는 상태였으므로, 나쁜 환경에서 그들의 자동 거래시스템이 45분만에 회사의 모든 주요 자산을 고갈시켜버린 것이다.

이것은 이상적인 환경을 위한 설계 정의이자 개발 정의였다. 이제 소프트웨어는 우리 개인 삶이나 회사에서 핵심 요소이다. 예측되지 못한 나쁜 상황의 시나리오 된다면 굉장히 많은 비용을 감당해야할 수 있다.

확장가능한(Scalable)
일관성있는 반응성 앱을 만들때 탄력성과 확장성을 잘 이용해야한다.

확장 가능한 시스템은 다양한 요구량의 상황(various load conditions)에서도 반응성을 보장하기 때문에 그에 맞춰 쉽게 업그레이드 시킬 수 있다.

온라인에서 물건을 팔아본 사람이라면 물건을 최대로 많이 팔때 가장 큰 트래픽이 생긴다는 사실을 알 것이다. 대부분의 경우(사이버상 공격을 제외하고) 트래픽이 폭발하는 것은 당신이 뭔가 잘하고 있을 때이다. 트래픽이 치솟았을대 사람들은 당신에게 돈을 주고 싶어하고 있는 것이다.

그럼 어떻게 치솟는(혹은 꾸준히 증가하는) 트래픽을 다룰까?

첫째로 당신의 패러다임을 먼저 고른다. 둘째로 그 패러다임을 구현할 수 있는 언어와 툴킷을 정한다. 많은 개발자가 종종 너무 가볍게 언어와 프레임워크를 선택한다. 한번 툴을 선택하면 그것을 다시 바꾸기 쉽지 않으므로 당신은 주요 투자가와 함께 그 결정을 내려야한다. 만약 여러분이 기술적인 선택 결정을 원칙과 분석에 기반하여 하고 있었다면 굉장히 잘하고 있는 것이다.

동시성을 위한 스레드-기반 제한
기술적인 선택에서 가장 중요한 것중 하나는 동시성 모델 프레임워크이다. 고수준에서 두개의 서로다른 동시성 모델이 있을 수 있다.
  • 전통적인 스레드-기반 동시성으로 콜스택과 공유 메모리를 기반으로 한다.
  • 메시지-주도 동시성

레일즈와같은 몇 인기있는 MVC 프레임워크는 스레드 기반이다. 이 프레임워크의 전형적인 특징은 아래와 같다.
  • 공유 가변 상태(Shared mutable state)
  • 요청당 한 스래드(A thread per request)
  • 가변 상태에 동시 접근(Concurrent access to mutable state)—이것(변수나 객체 인스턴스)은 또다른 복잡한 동기화 방법이나 락(lock)으로 관리한다.

루비와 같은 인터프리트 언어로 다이나믹 타입의 특징을 합치면 여러분은 쉽게 퍼포먼스와 확장성의 상한선에 도달할 수 있을 것이다( Combine those traits with a dynamically typed, interpreted language like Ruby and you can quickly reach the upper bounds of performance and scalability). 이것이 어떠한 스크립트 언어라도 그 본질은 같다고 말할 수 있을 것이다.

Out 혹은 Up?
앱 확장을 좀 다른 방법으로 생각해보자.

스케일업(Scale up)은 단일 CPU/서버의 리소스를 최대화 하는 것인데, 파워풀하고 희귀하고 값비싼 그런 컴퓨터를 종종 사야한다.

스케일아웃(Scale out)은 여러 저렴한 하드웨어를 연결하여 컴퓨테이션을 제공하는 것인데, 비용면에서 효과적이다. 그러나 당신의 시스템이 시공간 개념에 기반하였다면 매우 어려울지도 모르겠다. 위에서도 이야기했듯 메시지-주도 구조는 시공간으로부터 분리하기위해 필요한 비동기 바운더리를 제공하며, 필요에따라 스케일아웃 할 수 있는 유연성(elasticity)을 제공한다. 반면 스케일업은 이미 가지고있는 자원의 효율을 높히는 것이고, 유연성은 당신의 시스템이 바뀌기 원하는대로 새 자원을 추가할 수 있음에 관한 것이다. 필요에따라 스케일아웃 할 수 있는 능력은 리액티브 앱의 궁극적인 확장성의 목표이다.

공유 가변 상태, 스레드, 락 기반의 앱을 스케일아웃하는게 어렵기 때문에 리액티브 앱들을 스레드 기반으로 만드는것은 어려운 일이다. 개발자들은 한 머신에서 멀티 코어의 이점을 활용해야 할 뿐만 아니라, 특정 시점의 개발자들은 머신의 클러스터를 활용해야 한다. 그게 불가능할지라도, 공유 가변 상태 또한 스케일업하기 어렵게 만든다. 한번이라도 두개의 스레드에서 공유 가변 상태를 다뤄본 사람이라면, 스레드 세이프를 보장하는 과정이 얼마나 어려운지, 스레드 세이프를 위해 과한 작업을 하게되는 실적 패널티가 얼마나 큰지 이해할 수 있을 것이다.

메시지-주도(Message-driven)
메시지-주도 구조는 리액티브 앱의 근간이다. 메시지 주도 앱은 이벤트 주도 이거나 행위자 기반일 것이고, 혹은 이 둘 모두를 합친 것일 것이다.

이벤트 주도 시스템은 0개 혹은 그 이상의 Observer에의해 관찰된 이벤트 기반이다. 이것은 명령형 프로그래밍과는 좀 다른데, 호출자가 부른 루틴으로부터 응답을 블락된 상태로 기다릴 필요가 없기 때문이다. 이벤트는 바로 특정 장소를 지정하는게 아니라 나중에 일어날 어떤 결과를 지켜보고 있는 것이다.

행위자-기반 동시성은 메시지-전달 아키텍처의 확장이며 메시지는 수신자에게 전달된다. 메시지는 스레드 경계를 넘거나 실제 다른 서버의 다른 행위자의 메일함으로 전달 될 수 있다. 행위자가 네트워크를 통해 배포 될 수 있지만 여전히 동일한 JVM을 공유하는 것처럼 서로 통신 할 수 있으므로 요구에 맞게 스케일-아웃 할 수 있다.

메시지와 이벤트의 차이점은 메시지는 전송되는 것이고 이벤트는 일어나는 것이다. 메시지는 명확한 도착지가 있지만 이벤트는 0혹은 그 이상(0-N)의 Observer에의해 관찰되고 있을 것이다.

이벤트-주도와 행위자-기반 동시성에 대해 좀 더 세부적으로 들어가보자.

이벤트-주도 동시성
일반적인 앱들은 명령형 스타일(오퍼레이션 순서)로 개발되고 콜스택을 기반으로 개발한다. 콜스택의 주 기능은 프로세스에서 호출자가 블럭되고 리턴값과 한께 호출자에게 컨트롤을 돌려주는 동안, 주어진 루틴에서 호출자를 계속 쫓고, 호출된 루틴을 실행하는 것이다.

겉으로 보았을 땐 이벤트 주도 앱이 콜스택에 맞추는게 아니라 이벤트 트리거에 맞춘다. 이벤트는 0개 혹은 그 이상의 Observer에의해 지켜보고 있는 큐에 메시지로 인코딩 되어 있을 것이다. 명령형 스타일과 비교하여 이벤트-주도의 큰 차이점은 응답을 기다리는 동안 호출자가 한 스레드 위에서 블락되거나 멈추지 않는다. 이벤트 루프 자체는 단일 스레드 일 수 있지만, (단일 스레드 이기도 한)스레드 된 이벤트 루프가 들어오는 요청을 처리 할 수 있도록 허용하면서 호출 된 루틴이 업무를 수행하는 동안 (잠재적으로 IO 자체를 차단하면서) 동시성은 여전히 달성된다. 프로세스가 완전히 끝나지 않는한 요청을 블락시키는 대신에 호출자의 id가 요청 메시지의 바디와 함께 전달되므로 깨어있는 루틴이 이것을 선택하면 호출자는 응답과 함께 콜백될 수 있다. 

이벤트 주도 구조를 선택하는 이유는 콜백지옥(링크)이라는 것에 괴로워할 수 있기 때문이다. 콜백지옥은 메시지를 받는 놈이 정해져 있는 것이 아니라 익명의 콜백이기 때문에 발생한다. 콜백지옥을 해결하는 일반적인 방법은 이러한 문제가 생기는 이유를 잊고 코드에 표현된 이벤트 순서대로 디버깅하기 어려운것도 생각하지 않으면서 온전히 구문(aka, the Pyramid of Doom) 형태에만 초점을 맞춘다.

행위자-기반 동시성
행위자-기반 앱은 여러 행위자 사이에서 비동기 메시지를 보낸다.

행위자는 아래 속성들을 갖는다.
  • 메시지를 받기 위한 메일 박스
  • 각 타입별로 메시지를 어떻게 받는지 결정하기 위해 패턴매칭의 행위자 로직
  • 요청 사이에 컨텍스트를 저장하기 위한 고립된 상태
이벤트-주도 동시성처럼 행위자 기반 동시성에서는 콜스택은 피하고 가벼운 메시지 전달을 지향한다. 행위자는 메시지를 밖으로든 자기자신에게든 보낼 수 있다. 한 행위자는 그 큐의 첫번째 요청을 먼저 제공한 뒤에 처리가 긴 요청을 처리하기 위해 메시지를 자기자신에게 보낼 수도 있다. 행위자-기반 동시성의 큰 장점은 이벤트-주도 구조에서 얻은 장점을 얻을 수 있다는 점이다. 네트워크 경계를 통해 컴퓨테이션을 스케일-아웃하기도 쉽고, 행위자에게 직접 메시지를 전해주기 때문에 콜백 지옥을 피할 수도 있다. 이러한 강력한 컨셉은 설계, 구현, 유지보수하기 쉬우면서 확장성이 있는 앱을 만들 수 있게 해준다. 시공간에대해 생각하거나 깊게 감쌓인 콜백들에대해 생각하는것보다, 행위자 사이에 메시지 흐름이 어떻게 되는지만 고민하는게 더 낫다.

또 다른 주요 장점은 요소들끼리 느슨하게 연결된다는 점이다. 호출자는 응답을 기다리기 위해 스레드를 멈추지 않으므로 빨리 다른 일을 할 수 있다. 호출자에의해 켭슐화되있는 현재 루틴은 필요에따라 호출자를 다시 호출하면 된다. 이것은 다양한 가능성을 열어준다. 콜백 스택이 한 메모리 공간에 있기 위해 앱을 묶어버리지 않으며 행위자 모델은 프로그래밍적인 일보다 구성에 관련된 일을 추상화하여 만듦으로 여러 머신을 통해 루틴을 분배할 수 있다.

Akka는 행위자-주도 툴킷이고 타입 세이프 리액티브 플랫폼의 일부로서 JVM에서 높은 동시성, 분배, 실패에 대한 내성을 가진 행위자-기반 앱을 만들기 위한 런타임이다. Akka는 탄력을 위한 관리계층이나 확장성을 위한 일 분배와 같은 리액티브 앱 개발에 필요한 멋진 기능들을 탑재하고 있다. Akka에대해 더 깊게 보고싶으면 Let it Crash blog를 확인해보길 바란다.

또한 이 세션 일부의 자료로 사용된 Benjamin Erb’s Diploma Thesis의 글(Concurrent Programming for Scalable Web Architectures)을 읽어보기를 강력 추천한다.

결론
위의 모든 것들이 오늘날 앱 개발에 흠집을 내고, 왜 리액티브 프로그래밍이 단지 또다른 트렌드가 아닌지 이야기하며, 그러나 왜 현대 소프트웨어 개발자들이 배워야하는 패러다임인지도 이야기했다. 여러분이 선택하는 언어나 툴킷에 관계없이 반응성을 얻기위해 확장성과 탄력성 또한 탑재하는 것이 사용자의 기대를 충족시키는 유일한 방법이다. 이것은 몇년간 더욱 중요하게 떠오를 것이다.

저자에대해
Kevin Webber는 Lightbend에서 Enterprise Advocate이다. 그는 heritage 구조에서 리액티브 프로그래밍 원칙을 포괄하는 실시간 분배 시스템까지 큰 구조의 트랜지션을 돕는 것에 열정적이다. 남는 시간에 ReactiveTOProgramming Book Club Toronto를 운영한다. 그는 가끔 제 3자에서 자기 자신에 대해 글을 쓰기도 하는데, 이 단락이 그러한 순간이다. 


'그 외' 카테고리의 다른 글

(번역)Android Architecture  (0) 2016.10.01

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


이 튜토리얼은 비도오 시리즈로 나와있다.
라이브 코딩과 함께 비디오 튜토리얼을 보고 싶다면 이 글의 내용으로 녹화한 비디오 시리즈를 보아라: Egghead - Introduction to Reactive Programming

여러분은 Rx, Bacon.js, RAC등의 다양한 것들을 포함해서 리액티브 프로그래밍이라 불리는 새 것을 배우는데 관심이 있을 것이다.

이것을 배우기는 쉽지 않은데, 좋은 도구가 없다는 점이 한 몫을 더한다. 내가 처음 배우려할 때도 튜토리얼을 찾아보고자 했다. 나는 소량의 가이드를 찾아냈지만 그것도 겉핥기 식이었지 전체 구조를 리액티브로 만드는 가이드는 없었다. 또한 라이브러리는 당신이 몇몇의 기능만 이해하려 할 때 크게 도움이 되지 않을 때가 종종 있다. 아래를 보면 무슨 의미인지 공감할 수 있을 것이다.

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.

아이고..

나는 두가지 책을 읽었는데, 하나는 그냥 큰 그림을 설명하는 것이었고, 다른 하나는 리액티브 라이브러리를 어떻게 사용하는지 깊이 이야기한 책이었다. 나는 마침내 실제 만들어 보면서 리액티브 프로그래밍 배우는 것을 힘겹게 끝냈다. 나의 Futurice라는 일에서 리액티브 프로그래밍을 실제 프로젝트에 사용하였고 내가 문제가 생겼을 때 몇몇 동료로부터 도움을 얻을 수 있었다.

이것을 배우면서 가장 어려웠던 점은 리액티브하게 생각해야한다는 것이었다. 자꾸 이전의 명령형이나 상태를 가지는 전형적인 프로그래밍으로 가려는 것을 강제로 새로운 패러다임으로 작업하게 만들었다. 이 부분에 대해서는 인터넷에서 어떤 가이드도 찾지 못했다. 나는 사람들이 새로 이것을 시작할 수 있게 어떻게 리액티브하게 생각하는지에 대한 실질적인 튜토리얼이 필요하다고 생각했다. 이 생각이 조금 바뀌고 나면 라이브러리의 문서가 좀 이해가 될 것이며, 이 글이 당신에게 도움이 되길 바란다.

"리액티브 프로그래밍이 뭔가?"
인터넷 상에는 좋지 않은 설명과 정의가 있었다. 위키피티아는 너무 일반적이고 이론적이게 설명해 놓았다. 스택오버플로우의 인기있는 답변은 새내기들에게 적합하지 않아 보인다. 리액티브 매니페스트는 당신 회사에 프로젝트 매니저나 영업자에게나 보여줄 법 하다. 마이크로소프트의 Rx용어 "Rx = observables + LINQ + Schedulers"는 마이크로소프트한것이 우리에게 혼동만 남겨준다. "리액티브"나 "변화의 전파(propagation of change)"와 같은 용어는 일반적인 MV * 나 즐겨쓰는 언어가 이미 한 것과 다른 내용이 아니다. 물론 내 프레임워크의 뷰는 모델에게 리액트한다. 물론 변화는 전파된다. 그렇게 하지 않으면 아무것도 렌더링 되지 않을 것이다.

그럼 이제 잡소리는 집어치우자.

리액티브 프로그래밍은 비동기 데이터 스트림으로 프로그래밍 하는 것이다.

뭔가 새로운 것이 있는게 아니다. 이벤트 버스나 여러분의 일반적인 클릭  이벤트는 굉장히 비동기적인 이벤트 스트림인데, 이것을 옵져브 할수도 있고, 다른 추가적인 효과를 줄 수도 있다. 리액티브 스테로이드(옮긴이: 화학에 관련된 용어)로부터 떠오른 아이디어이다. 여러분은 클릭이나 마우스 호버(hover) 이벤트 뿐만 아니라 어떠한 데이터 스트림도 만들 수 있다. 변수, 유저입력, 프로퍼티, 캐시, 데이터 구조체등 어떤것도 스트림이 될 수 있으며, 스트림은 싸고 범용적이다. 예를들어 당신의 트윗 피드가 클릭  이벤트와 같은 방식으로 데이터 스트림이라고 상상해보아라. 당신은 그것을 듣고(listen)있다가 적절하게 반응을 하면 된다.

최상부에는 어떤 스트림에서도 합치고, 생성하며, 필터링할 수 있는 툴박스를 제공한다. 이것이 "함수형"의 마법 효과이다. 한 스트림은 다른 것의 입력으로 사용될 수 있다. 여러개의 스트림까지도 다른 스트림의 입력으로 사용될 수 있다. 두 스트림을 합칠 수 있다(merge). 원하는 이벤트만 골라내어 새 스트림으로 필터링할 수 있다(filter). 한 스트림을 새 스트림으로 각 값들을 매핑할 수 있다(map).

리액티브에서 스트림이 중심이 되면, 스트림을 잘 살펴보자. 우리는 친숙한 "버튼 클릭" 이벤트로 이 이야기를 시작하려한다.


한 스트림은 시간 순서로 정렬된 이벤트 순서이다. 이 스트림 값(어떤 타입), 에러, '완료'신호 3개의 상태를 내뱉을 수 있다. "완료" 지점이 되었다고 하자. 예를들어 버튼이 있는 현재 윈도우나 뷰가 닫혔을 때 이다.


우리는 발생된 이벤트를 비동기적으로 잡아내야하는데, 값을 내뱉을 때 실행되는 함수, 에러를 내뱉을 때 실행되는 함수, '완료'를 내뱉을 때 실해오디는 함수를 선언해놓고 잡아낼 수 있다. 때론 뒤에 두가지 함수는 생략하고 값을 위한 함수만 사용할 수도 있다. 스트림을 "듣고(listen)"있는 것은 '구독하고 있다'고 부른다. 우리가 정의한 함수는 옵저버이다. 스트림은 옵저베이블(observable)이다. 이것이 바로 옵저버 디자인 패턴이다.

이제는 위 그림 대신에 아스키(ASCII)로 설명을 대신하겠다.
--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
여기까지는 꽤 친숙할 것이고 여러분을 지루하게 하고 싶지 않다. 이번에는 원래의 클릭 이벤트 스트림에서 변형하여 만든 새 클릭 이벤트 스트림을 만들어보자.

먼저 숫자를 세는 스트림을 만들자. 이 스트림은 버튼이 얼마나 클릭됐는지 나타낸다. 일반적인 리액티브 라이브러리에서는 map, filter, scan과 같이 제공되는 많은 함수를 가지고 있다. 만약 당신이 clickStream.map(f)처럼 함수를 호출하면 clickStream으로부터 새 스트림을 반환받는다. 원래의 clickStream은 손대지 않고 말이다. 이러한 특징을 불변성이라 부르며, 마치 팬케잌에 시럽이 좋듯 리액티브에 좋게 해준다. 이것은 clickStream.map(f).scan(g)와 같이 함수 체이닝을 가능하게도 해준다.
  clickStream: ---c----c--c----c------c-->
               vvvvv map(c becomes 1) vvvv
               ---1----1--1----1------1-->
               vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f) 함수는 당신이 만든 f함수에 따라 만들어진 각 값으로 대체한다. 우리의 경우 각 클릭마다 숫자 1로 매핑시켰고 scan(g) 함수가 값 x=9(acculated, current)를 실행하여 스트림의 모든 이전 값을 합한다. 여기서 g함수는 단지 더하는 함수이다. 그리고 counterStream은 클릭이 일어난 수를 내뱉는다.

리액티브의 실제 힘을 보여주기 위해 여러분이 갑자기 '더블 클릭' 이벤트의 스트림이 필요하다고 해보자. 좀 더 흥미롭게 만들기 위해 더블클릭처럼 트리플클릭도 되는지 혹은 다중클릭(더블클릭 이상)까지도 되는지 보자. 깊게 숨을 들이마쉬고 생각해보자. 전통적인 명령형의 상태와 함께 사용한 방식에서는 어떻게 구현했는지 상상해보라. 아마 꽤 지저분하게 몇몇 변수는 상태를 가지고 있었을 것이고 몇몇 변수는 시간차를 세고 있을 것이다.

흠 리액티브에서는 꽤 간단하게 해결된다. 사실 로직은 4줄의 코드 밖에 안된다. 그러나 지금 당장은 코드를 보지 말자. 당신이 초보자이든 숙련자이든 스트림을 이해하게 만드는 데는 그림으로 설명하는 것이 최고이다.


회색 상자들은 한 스트림에서 다른 스트림으로 변경시키는 것이다. 먼저 250ms동안 이벤트 발생이 없으면 클릭했던 이벤트들을 리스트로 모은다. (이게 buffer(stream.throttle(250ms))가 무슨 일을 하는지의 이야기이다. 이 시점에서 굳이 깊게 이해하려고 하지 말자. 지금은 단지 리액티브로 데모를 만들고 있다.) 그리고 map()을 적용시켜 각 리스트의 크기를 정수로 매칭시키고, 매핑한 리스트의 스트림으로 결과가 나온다. 마지막으로 filter(x>=2)함수를 이용해 정수 1은 무시한다. 우리가 의도한 스트림으로 만들기 위해 사용한 것은 3개의 오퍼레이션이 다다. 이제 우리가 원하는 곳에 반응해주기위해 이것을 구독할 수 있다.

여러분이 이 아름다운 방법을 잘 음미했길 바란다. 이 예제는 사실 빙산의 일각이다. API 응답 스트림처럼 다른 스트림을 이 오퍼레이션으로 적용할 수 있고 또 수많은 다른 함수들도 사용할 수 있다.

"왜 RP 적용시키기를 고려해야할까?"
리액티브 프로그래밍은 당신 코드의 추상화 수준까지 올라왔으므로, 수많은 세부구현을 끊임없이 할게 아니라 비즈니스 로직을 정의하는 이벤트의 상호 의존에 초점을 맞출 수 있었다. RP의 코드는 더 간결해 질것이다.

데이터 이벤트에 관련한 수많은 UI 이벤트와 높게 소통하는 현대 웹앱과 모바일앱에서는 그 이점이 더욱 분명하다. 10년전에는 백엔드에 긴 형식으로 보내면 간단하게 프론트엔드에 렌더링 시키는 것이 웹페이지의 인터렉션이었다. 앱은 더 실시간으로 진화되었다. 몇 컨텐츠는 연결된 다른 사용자에게 실시간으로 반영되는 것처럼 가볍게 한 필드를 수정하는 것이 바로 백엔드에 저장하는 트리거가 될 수 있다.

오늘날의 앱은 사용자에게 높은 소통을 경험하게 해주는 수많은 종류의 실시간 이벤트가 들어가있다. 우리는 이것을 다루기위해 적절한 툴을 골라야하는데, 리액티브 프로그래밍이 그 해답이다.

예제와함께 RP처럼 생각하기
실제 예제로 들어가보자. 어떻게 RP처럼 생각하는지 단계별로 설명하는 실세계의 예제이다. 가짜 예제도 아니고 개념의 일부만 설명하기 위한 예제도 아니다. 이 튜토리얼이 끝날때쯤 우리가 왜 이것들을 했는지 아는체로 실제 함수형 코드를 만들 것이다.

나는 다음의 이유로 자바스크립트RxJS를 사용할 것이다. 자바스크립트가 현재 가장 인기있는 언어이며 Rx* 라이브러리 패밀리가 많은 언어나 플랫폼에서 가장 폭넓게 쓰이고 있다.(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy 등등) 따라서 당신이 어떤 툴을 쓰든, 이 튜토리얼을 따라오면서 구체적인 이점을 얻을 수 있을 것이다.

"Who to follow" 제안 박스 구현하기
트위터에서는 당신이 팔로우할 수 있는 다른 계정들을 제안하는 UI 요소가 있다.

우리는 이 핵심 기능을 모방해 볼것이다.
  • 시작하면 API로부터 계정 데이터를 불러오고 3가지 제안을 띄운다.
  • "Refresh"를 클릭하면 3개의 또다른 계정을 띄운다.
  • 계정중에 'x' 버튼을 누르면 그 계정은 사라지고 다른 계정을 띄운다.
  • 각 줄은 계정의 아바타를 띄우고, 누르면 그들 페이지로 이동한다.
다른 기능이나 버튼은 부수적이기 때문에 남겨 둘 것이다. 그리고 트위터 API는 최근들어 승인이 필요하게 되었으므로 대신에 깃헙의 팔로잉 사람들을 위한 UI를 만들어보자. 여기에 사용자를 얻어내기 위한 Github API(링크)가 있다.

완성된 코드는 http://jsfiddle.net/staltz/8jFJH/48/ 여기에 준비되있다.

요청과 응답
어덯게 Rx로 이 문제를 접근할까? 흠, 시작하기 앞서, (대부분) 모든 것은 스트림이다. 이것이 Rx의 주문이다. 가장 쉬운 "시작하면 API로부터 3가지 제안을 띄운다" 기능부터 시작해보자. 특별한 것은 없어 보이며 간단하게 (1)요청을 날리고 (2)응답을 받고 (3)그 응답을 표시하면 된다. 그럼 이제 우리의 요청을 스트림으로 표현해보자. 처음 봤을때는 좀 힘들어 보이지만 우리는 기초부터 시작해야 한다.

시작하면 한 요청만 날리면 되고, 데이터 스트림으로 모델링하면 하나의 값만 스트림이 될 것이다. 그 후에는 우리도 알듯 많은 요청을 보내게 될테지만, 지금은 하나만 해보자.
--a------|->

Where a is the string 'https://api.github.com/users'
이것은 우리가 요청할 URL의 스트림이다. 요청 이벤트가 발생하면 언제, 무엇이 발생했는지 알려준다. 요청이 "언제" 호출될지는 이벤트가 언제 발생되는지와 같은 시점이다. 그리고 "무엇이" 요청됐는지는 발생된 값이다.(URL을 담고 있는 문자열)

Rx* 한 값으로 이런 스트림을 만드는 것은 굉장히 간단하다. 스트림에서 공식적인 용어는 "Observable"이다. 이것을 Observe 할 수 있다고 하지만 이렇게 바보처럼 말하지 말고, 이것을 스트림이라 부르겠다.

var requestStream = Rx.Observable.just('https://api.github.com/users');

이제부터는 이것이 문자열의 스트림이고 다른 동작을 하지 않으며, 값이 나올때 우리가 필요한대로 어떻게 처리할 수 있다. 이것을 subscribing으로 스트림에 할 수 있다.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  jQuery.getJSON(requestUrl, function(responseData) {
    // ...
  });
}

우리는 요청 오퍼레이션의 비동기 처리를 위해 jQuery Ajax 콜백(여러분은 이미 알고 있다고 가정한다)을 사용한다. 그런데 가만보자. Rx는 비동기 데이터 스트림을 다루기위해 있다. 그 요청에 대한 응답이 곧 있다가 받을 데이터를 담은 스트림일 수는 없을까? 음, 개념상으로는 가능해보이니 한번 시도해보자.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  var responseStream = Rx.Observable.create(function (observer) {
    jQuery.getJSON(requestUrl)
    .done(function(response) { observer.onNext(response); })
    .fail(function(jqXHR, status, error) { observer.onError(error); })
    .always(function() { observer.onCompleted(); });
  });

  responseStream.subscribe(function(response) {
    // do something with the response
  });
}

Rx.Observable.create()가 하는 일은 각 Observer(혹은 다른말로 '구독자')에게 데이터 이벤트(onNext())나 에러(onError())를 명시적으로 알리는 커스텀 스트림을 만든다. 우리가 한 일은 그냥 jQuery Ajax Promise을 감싼 것이다. 잠시만요, 이게 Promise이란게 Observable을 의미하는 건가요?




그렇다.

Observable은 보장++이다. Rx에서 당신은 var stream = Rx.Observable.fromPromise(promise)하여 쉽게 Promise를 Observable로 변환할 수 있다. 그러니 이것을 사용해보자. 다지 다른 점은 Observable이 Promise/A++를 따르지 않지만 개념적으로 충돌은 없다. 간단하게 한 보장은 하나의 발생된 값과 함께 한 Observable이다. 꽤 좋아보인다. 적어도 Observable이 Promise만큼 강력한지 보여준다.

여러분이 Promise의 속임수라 믿는다면 Rx Observable이 어떤 것이 유능한지 눈으로 지켜보자.

이제 예제로 다시 돌아와서, 새 subscribe()를 또 하나더 그 안에서 호출하면 콜백지옥 같은 형태가 되버린다. 또한 responseStream 생성은 requestStream에의해 결정된다. 이전에도 들었듯 Rx에서는 새 스트림을 변형, 생성해내는 간단한 매커니즘이 있으므로 이것을 시행해보자.

이제부터는 알아야할 기본함수인 map(f)가 있다. 이것은 스트림A의 각 값을 받아서 f()를 적용시키고 스트림B로 만든다. 이것을 우리 요청과 응답 스트림에 하려면 요청 URL을 응답 Promise(스트림처럼 만든)에 매핑할 수 있다.
var responseMetastream = requestStream
  .map(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

그다음 우리는 스트림의 스트림인 "metastream"을 하나 만들 것이다. 아직 당황하지 마라. metastream은 각각 발생했던 값이 있는 또다른 스트림이다. 여러분은 이것을 '포인터'라 생각할 수도 있겠다. 각 발생한 값은 다른 스트림을 가리키는 포인터이다. 우리 예제에서는 각 요청 URL이 Promise 스트림을 가리키는 포인터로 매핑되는데, 이 promise 스트림은 해당되는 응답을 가지고 있다.


응답을 위한 한 metastream은 혼란스러워 보이며 우리에게 크게 도움이 되지 않는 것 같다. 우리는 각 발생된 값이 'promise'의 JSON 객체가 아닌 그냥 간단한 응답 스트림이 필요할 뿐이다. Flatmap씨(링크)에게 인사하자. "branch" 스트림에서 나오는 모든 "trunk" 스트림을 내보냄으로서 동작하는, flatmap은 한 metastream을 "flattens"하게 만드는 버전의 map()이다. Flatmap은 "고정"된 것이 아니고 metastream은 일종의 버그가 아니다. 이것들은 Rx에서 비동기로 응답을 다루기위한 실제 툴이다.
var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });


좋다 응답 스트림이 요청 스트림에 맞춰 정의되었으므로, 후에 요청 스트림에서 이벤트가 발생하면 예상한대로 응답 스트림에서 발생한 응답 이벤트들을 가질 수 있을 것이다.

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(lowercase is a request, uppercase is its response)

이제 마침내 응답 스트림을 가지게 되었다. 우리가 받은 데이터를 화면에 띄우면 된다.

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

지금까지의 코드를 모두 합쳐보자.

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});
새로고침 버튼
나는 아직 응답의 JSON이 100명의 유저라는 것에 대해 말하지 않았다. 이 API는 한 페이지 크기를 설정할 수 있는게 아니라 페이지 번호를 설정할 수 있게 해놓았다. 따라서 우리는 오직 3개의 데이터만 쓰고 나머지 97개의 데이터는 버려진다. 우선은 이 문제를 무시할 것이다. 그리고 나중에 이 응답을 어떻게 캐신할 수 있는지 살펴볼 것이다.

새로고침 버튼을 누를때마다 요청 스트림은 새 URL을 발생시킬 수 있으므로 새 응답을 얻어낼 수 있다. 우리는 다음 두가지가 필요하다. 새로고침 버튼의 클릭 이벤트 스트림(주문: 모든것이 스트림이 될 수 있다). 그리고 새로고침 클릭 스트림에 따라 요청 스트림을 바꿀 수 있는 것도 필요하다. 기꺼히 RxJS는 이벤트 리스너로부터 Observable을 만들 수 있는 툴을 제공한다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

새로고침 클릭 이벤트가 그 자신의 API URL을 관리하지 않으므로 우리는 각 클릭을 실제 URL로 매핑해주어야한다. 이제 요청 스트림을 새로고침 클릭 스트림으로 바꾸었다. 새로고침 클릭 스트림은 매번 랜덤의 페이지 값으로 API endpoint를 바꾸어 매핑한다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

내가 좀 어리석고 자동 테스트를 못해서 이전에 만든 기능 중 하나를 가져왔다. 한 요청은 더이상 시작할 때 일어나지 않고 새로고침을 눌렀을때만 일어난다. 아아. 나는 이 요청이 새로고침을 누를때나 사이트를 켰을때나 둘 다 동작하게 하고 싶다. 


우리는 이 상황별 스트림을 어떻게 분리하는지 알고있다.
var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

그러나 이제 '쪼개진' 두 스트림을 어떻게 하나로 합칠까? merge()가 있다. 아래 다이어그램에서 어떻게 되는건지 설명했다.

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
          vvvvvvvvv merge vvvvvvvvv
          ---a-B---C--e--D--o----->

이제 쉬워졌다.

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

var requestStream = Rx.Observable.merge(
  requestOnRefreshStream, startupRequestStream
);

매개 스트림 없이 만들 수 있는 깔끔한 방법의 대안이다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .merge(Rx.Observable.just('https://api.github.com/users'));

더 짧아지고 가독성도 더 좋아졌다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .startWith('https://api.github.com/users');

startWith() 함수는 당신이 생각하는데로 정확히 그렇게 동작할 것이다. 입력 스트림이 어덯게 생겼든 상관없이 startWith(x)로부터 나온 결과 스트림은 시작부분에서 x를 가질 것이다. 그러나 나는 아직 DRY 하지 못하다. 나는 API endpoint 문자열을 반복에서 쓰고 있다. 이것을 고치기 위한 한가지 방법은 startWith()refresgClickStream에 붙이는 것이다. 이것은 시작 시점에 새로고침 클릭을 강제로 시행하기 위함이다.

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

좋다. 만약 내가 자동 테스트를 쪼개었다는 곳으로 돌아가보면 이전 것과 다른 점이 startWith()만 추가한 것 밖에 없다.


스트림으로 3가지 제안을 모델링하기
이제부터 터치된 제안 UI 요소만 가지고 있는데, responseStream의 subscribe에서 일어난 렌더링 단계에서 일어났다. 이제 새로고침 버튼에 문제가 있다. '새로고침'을 누르자마자 현재 3개의 제인이 명확하지 않다. 새로운 제안이 응답이 도착한 후에 화면에 나타나는데, UI적으로 좀더 좋게 만들기 위해 새로고침을 누를 때 현재 있던 제안들을 지울 필요가 있다.
refreshClickStream.subscribe(function() {
  // clear the 3 suggestion DOM elements 
});

아니다.. 빠르지 않다. 우리는 제안의 DOM 요소에 영향을 주는 두 구독자가 있기때문에 그렇다. 그리고 이것은 일을 쪼개는 것(sepration of concerns)처럼 보이지도 않는다. 리액티브의 주문이 기억나는가?


이제 제안을 스트림으로 모델링할것인데, 각각 발생한 값은 제안 데이터를 가지고 있는 JSON 객체이다. 우리는 3가지 각 제안마다 이 일을 할것이다. 이것은 1번 제안을 위한 스트림이 어떻게 생겼는지 보여준다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  });

그리고 suggestion2Streamsuggestion3Stream은 그냥 suggestion1Stream을 복사하여 붙여 넣은 것이다. 이것은 DRY는 아니나 튜토리얼의 예제를 간단하게 만들기위해 이렇게 하였고, 추가로 이 경우에는 어떻게 중복을 피할수 있을지 생각해볼수 있는 좋은 기회라 생각된다.

responseStreamsubscribe()에서 렌더링이 일어나게 하는 것 대신에 이렇게 했다.
suggestion1Stream.subscribe(function(suggestion) {
  // render the 1st suggestion to the DOM
});

"새로고침을 누르면 제안들을 지운다"로 돌아가서, 우리는 간단하게 새로고침 클릭을 제안 데이터에 null로 매핑하여 suggestion1Stream에 넣는다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  );

그리고 화면에 띄울때는 null은 따로 분기처리하여 "데이터가 없음"이라하고 그 UI 요소를 숨긴다.

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

여기 큰 그림이다.

refreshClickStream: ----------o--------o---->
     requestStream: -r--------r--------r---->
    responseStream: ----R---------R------R-->   
 suggestion1Stream: ----s-----N---s----N-s-->
 suggestion2Stream: ----q-----N---q----N-q-->
 suggestion3Stream: ----t-----N---t----N-t-->

Nnull을 의미한다.


보너스로, 시작할때 '빈' 제안들이 화면에 나타날 것이다. 이제 제안 스트림에 startWith(null)을 추가하면 된다.
var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

그 결과이다.

refreshClickStream: ----------o---------o---->
     requestStream: -r--------r---------r---->
    responseStream: ----R----------R------R-->   
 suggestion1Stream: -N--s-----N----s----N-s-->
 suggestion2Stream: -N--q-----N----q----N-q-->
 suggestion3Stream: -N--t-----N----t----N-t-->
제안을 닫고 캐싱된 응답을 사용하기
이 기능이 마지막 남은 구현이다. 각 제안은 그것을 닫을 수 있는 'x'버튼을 하나씩 가지고 있고, 그것을 누르면 그 자리에 다른 제안이 들어온다. 닫기 버튼이 눌러지면 새 요청을 만들어야 한다고 생각해볼 수 있다.
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button

var requestStream = refreshClickStream.startWith('startup click')
  .merge(close1ClickStream) // we added this
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

위의 것은 제대로 동작하지 않는다. 이것은 하나만 눌렀는데 모두 닫으면서 모든 제안을 다시 갱신할 것이다. 이 문제를 해결하기 위한 방법에는 여러개가 있을 것이다. 우리는 이전 응답을 재사용 함으로서 이 문제를 해결할 것이다. API 응답의 페이지 크기는 100명의 사용하지만 우리는 딱 3명만 사용하고 있으므로 남은 데이터를 사용할 수 있을 것이다. 새로운 요청 없이 말이다.


다시 말하자면, 스트림으로 생각해보자. 'close1' 클릭 이벤트가 발생하면 우리는 가장 최근에 responseStream에서 가져올 랜덤 유저 한명을 사용하고 싶다.
    requestStream: --r--------------->
   responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->

Rx*에서는 우리에게 필요한 것 처럼 보이는 결합함수, combineLatest가 있다. 이 함수는 인풋으로 A, B 스트림을 받고, 어떤 스트림이 값을 발생시키든 그때마다 combineLatest는 가장 최근에 두 스트림으로부터 a, b 값을 합친다. 아웃풋 값은 c=f(x,y)인데, f는 여러분이 정의한 함수이다. 다이어그램으로 이해하는게 더 좋을 것이다.

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

where f is the uppercase function

우리는 close1ClickStreamresponseStreamcombineLatest()를 적용시킬 수 있으므로, 닫기버튼1이 클릭될 때 마다 마지막에 발생된 응답을 얻어와서 suggestion1Stream에 새 값으로 만든다. 한편 combineLatest()는 대칭적인데, responseStream에서 새 응답이 나올 때 마다 새 제안을 만들어내기위해 마지막 '닫기1' 클릭을 합친다. 이 부분이 재미있는데, 이전에 suggestion1Stream 코드를 간단하게 만들어준다.

var suggestion1Stream = close1ClickStream
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

아직 한 부분이 빠졌다. combineLatest()는 두 소스 중 가장 최신의 것만 사용한다. 그러나 이 소스 중 하나가 아직 아무것도 발생시키지 않았다면, combineLatest()는 아웃풋 스트림에 데이터 이벤트를 만들 수 없을 것이다. 위의 아스키 다이어그램을 보면 첫번째 스트림이 a를 만들었을때 아웃풋에 아무것도 없음을 확인할 수 있을 것이다. 두번째 스트림에서 b 값을 만들었을때 비로소 아웃풋 값이 만들어질 수 있다.


다른 해결 방법이 있는데, 시작할 때 '닫기버튼1'을 모의로 눌러보는 것이다.

var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
  .combineLatest(responseStream,             
    function(click, listUsers) {l
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
합쳐보기
이제 끝났다. 우리가 만든 코드의 완성본이다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var responseStream = requestStream
  .flatMap(function (requestUrl) {
    return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
  });

var suggestion1Stream = close1ClickStream.startWith('startup click')
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

http://jsfiddle.net/staltz/8jFJH/48/ 여기서 예제가 돌아가는 것을 확인할 수 있을 것이다.

이 코드 조각은 작지만 빽빽하다. 여기에는 적당히 일을 분배하여 다중 이벤트를 관리하고 응답을 캐싱하는 것까지 기능이 들어있다. 함수형  스타일은 명령형 스타일보다 더 서술적인 코드를 만든다. 우리는 실행되야할 인스트럭션의 순서를 만든게 아니라, 스트림 사이의 관계를 정의함으로서 그냥 어떤것인지 말했을 뿐이다. 예를 들어 if, for, while과같은 컨트롤 플로우 요소들이 없고 자바스크립트에서 주로 쓰는 일반적인 콜백기반 컨트롤 플로우가 없다는 것을 기억에 남기자. 위의 subscribe()에서 당신이 원하면 filter()를 이용하여 ifelse를 없앨수도 있다. (구체적인 구현은 여러분의 연습 과제로 남기겠다) Rx에서는 map, filter, scan, merge, combineLatest, startWith 등과 같이 이벤트 주도 프로그램의 흐름을 컨트롤하는 많은 스트림 함수를 가지고 있다. 이 함수 툴셋은 작은 코드로 더 강한 영향력을 제공할 것이다.

다음으로 해야할 것은 무엇인가
만약 Rx*가 당신의 리액티브 프로그래밍의 주 라이브러리가 된다면, Observable을 변형, 결합, 생성하기위한 많은 함수들을 익힐 필요가 있다. 이 함수들을 스트림 다이어그램으로 이해하고 싶으면 RxJava's very useful documentation with marble diagrams에 들어가 보아라. 당신이 뭔가 시도하다가 문제가 생기면 다이어그램을 그려 생각해보고 그 많은 함수들을 본 다음 다시 생각해 보아라. 내 경험에는 이 방법이 가장 효율적이었다.

한번 Rx와의 프로그래밍을 파악하기 시작하면 Cold vs Hot Observable 개념을 반드시 이해해야한다. 그렇지 않으면 당신을 계속 괴롭힐 것이다. 여러분은 경고받아왔다. 함수형 프로그래밍을 배우고 Rx에 영향을 미치는 사이드 이팩트같은 이슈를 익히면서 당신의 기술을 더 갈고 닦아라.

하지만 리액티브 프로그래밍이 단지 Rx만 있는게 아니다. Bacon.js라는 것이 있는데, 이것은 가끔 Rx에서 마주칠 수 있는 관습(quirks)없이 직관적으로 작업한다. Elm언어는 그 자신만의 카테고리 안에 있다. 이 언어는 자바스크립트+css+HTML로 컴파일하고 타임 트레블링 디버거(time travelling debugger)기능이 있는 함수형 리액티브 프로그래밍 언어이다. 꽤 멋지다.

Rx는 이벤트가 중심인 프론트엔드와 앱에서 잘 동작한다. 그러나 이게 클라이언트단에서만 그런 것이 아니라 백엔드와 데이터베이스 접근에도 잘 동작한다. 사실 RxJava는 Netflix API에서 서버단 동시성을 가능하게 해주는 핵심 요소이다. Rx는 특정 타입의 앱이나 언어에 종속된 프레임워크가 아니다. Rx는 이벤트 주도 소프트웨어를 사용할 수 있게 해주는 패러다임이다.

이 튜토리얼이 마음에 들었다면 트윗해 달라. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,