개발
리액티브 프로그래밍 한방에 이해하기
(원문 링크)
왜 이글을 썼을까? (가급적 원문을 번역해보았어요)
요즘 Rx, Bacon.js, RAC 등과 같은 리액티브 프로그래밍에 관심들이 많아요.
그런데, 그걸 배우기가 어렵고 심지어 좋은 자료도 부족해요. 제가 처음 공부할때도, 튜토리얼들을 많이 찾아보았어요. 그리고 쓸만한 가이드를 찾아보려고 하기도 했고요. 그런데 대부분 본질을 건드리지 못하고 피상적이었죠. 아래처럼 공식 라이브러리 문서는 어떤 기능을 이해하는데 사실 별로 도움이 안되는 경우가 많죠.
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
2권의 책을 읽고, 결국 어렵게 리액티브 프로그래밍을 이해하게 되었어요. Futurice라는 회사에 근무 중인데요 실무에서 사용하고 있어요. 그리고 어려운 상황에 처하면 동료들의 도움도 많이 받았구요.
공부하는 중에 제일 어려웠던건 리액티브하게 생각하기에요. 그건 기존 명령형, 상태를 가지는 프로그래밍에 익숙해져 있어 다른 패러다임을 수용 하기가 쉽지 않았기 때문이에요. 이런 관점에서 좋은 가이드를 찾지 못했어요. 그래서 리액티브하게 생각하는게 어떤것인지 이해할 수 있는 실용적인 튜토리얼이 있으면 좋을 것이라 생각했어요. 아마 라이브러리 문서는 그 이후 도움이 될거에요.
리액티브 프로그래밍이란?
인터넷 대부분의 설명은 좀 이해하기 어려움
리액티브나 변화의 전달이란 용어는 기존 MV* 모형이나 언어와 비교해서 사실 크게 다르진 않음
리액트 프로그래밍은 비동기 데이터 스트림을 사용하여 프로그래밍 하는 것
클릭 이벤트 등 사실 대부분 비동기 이벤트 스트림이고 이들을 잘 처리하는게 생각보다 쉽지 않음
리액티브는 일종의 스테로이드 같은 아이디어 (좋은 의미겠죠?)
모든 데이터는 스트림으로 만들 수 있고 스트림은 싸고 흔한 것임
여기에 스트림을 잘 처리할 수 있게 생성하고(create) 변환하고(map) 필터링하고(filter) 합치는(merge, combine) 등의 놀라운 툴박스가 함께 제공됨 (이게 사실 마법임!!)
이 툴박스는 함수형(functional) 방식이어서 이벤트 기반의 처리에서 발생될 수 있는 다양한 부작용을 말끔하게 해결해줌
즉 툴박스의 모든 함수는 새로운 스트림을 반환하여 불변성을 보장함(immutability)
아무튼 스트림이 리액티브 프로그래밍에 핵심임
스트림이란?
스트림은 다가올 이벤트를 시간순으로 나열한 것
스트림에는 3가지 시그널이 존재함 - 어떤 값 받음(onNext), 에러 발생(onError), 완료(onCompleted)
스트림의 이벤트를 받으려면 가입(subscribing) 필요
스트림 이베트를 수신하는 것을 옵저버라고 부르며 옵저버 디자인 패턴을 따름
(옵저버 디자인 패턴)
스트림 처리 간단 예시1: 클릭 카운트 하기
클릭 이벤트(c) -> map으로 클릭 이벤트 올때마다 숫자 1로 전환 -> scan으로 누적해서 더함 counterStream = clickStream.map(f).scan(g)
스트림 처리 간단 예시2: 멀티 클릭 이벤트 찾기
250ms 마다 이벤트를 buffer -> 각 버퍼의 길이 계산하여 map -> 2보다 큰 것 filter
왜 리액티브 프로그래밍을 고려해야 할까?
코드의 추상화 수준을 끌어올리고, 이벤트의 상호 의존성에 집중할 수 있게 함
코드가 간결해짐
최근 앱들이 극도로 인터렉티브해지고 이벤트도 매우 많고 즉각적임
예전에는 페이지 단위 렌더링 수준이라면 요즘은 타이핑 할때마다 반응하는 수준임
이러한 환경에서는 리액티브 프로그래밍이 정답
예제와 함께 리액티브 프로그래밍 방식으로 생각하기 (RxJS 기반)
- 요청과 응답
아래와 같이 트위터 팔로워 3명을 임의로 추천하고 맞팔할 수 있는 화면을 제공할 것임
- 시작시 3명 임의 추천
- 리프레쉬 버튼 클릭시 3명 재추천
- x(close) 버튼 클릭시 1명만 재추천
아래와 같이 문자열 기반으로 requestStream 만들고 그 URL에 요청을 보내고 응답을 onNext, onError, onCompleted와 연계하여 처리하는 옵저버를 가입(subscribe) 시킴
여기서 보면 jQuery Ajax Promise를 볼 수 있는데 Promise가 Observable과 유사함을 느낄 수 있음
Observable은 Promise의 발전된 형태라고 봐도 됨
따라서 아래처럼 Promise는 쉽게 Observable로 전환 가능함
단, map은 value를 리턴해야 하는데 위와 같이 하면 Observable이 리턴되므로 정상동작하지 않음
이런 경우를 위해서 flatMap이 존재함 (flatMap은 구글링하면 쉽게 이해 가능)
결과적으로 코드는 아래와 같이 될 것임
- 리프레쉬 버튼
리프레쉬 버튼은 아래와 같이 스트림화 할 수 있음
그리고 map을 사용하여 팔로워 추천 리스트로 변환 (랜덤하게 오프셋을 주어서 임의 팔로워 목록을 가져옴)
여기서 문제는 시작시에는 아무런 이벤트가 없기 때문에 처음에는 아무것도 보이지 않음
따라서 아래와 같이 시작시 자동으로 요청하는 스트림을 생성해서 머지 해야 함
이걸 startWith를 사용해서 더 간단히 하면
여기서 API URL이 반복 사용된 것을 해결하려면 startWith를 위로 옮기고 시작시 리프레쉬 버튼이 클릭된 것처럼 에뮬레이트하면 됨
- 3명 추천 모델링
리프레쉬 버튼 클릭시 바로 반응하지 않고 새로운 추천을 받을때 갱신되는 문제 있음
아래와 같이 리프레쉬 버튼 클릭시 화면을 초기화(cleaning) 가능하지만 subscriber가 두개가 되는 문제 발생 (또다른 하나는 responseStream.subscribe())
그리고 추천 모델을 자세히 보면 3명의 추천이 분리되어 동작해야 함(x버튼 존재)
그래서 아래와 같이 3개로 분리할 필요가 있음 (suggestion2Stream, suggestion3Stream은 suggestion1Stream 복붙)
리프레쉬 버튼 클릭시 초기화 문제로 다시 돌아와서 리프레쉬 클릭시 빈(null) 추천을 하게하고 이것을 suggestion1Stream에 포함(merge)시키면 됨
렌더링도 빈추천에 대해서 처리하도록 함
지금까지 스트림의 그림은 다음과 같음 (N은 null 즉 빈추천 의미)
보너스로 시작시 빈추천으로 시작하게 startWith(null) 추가
스트림 그림은 다음과 같음
- 추천 닫기(x버튼) 및 저장된 응답 이용하기
마지막으로 추천 닫기(x버튼)은 버튼 클릭시 다른 1명을 추천해야 함
아마 아래와 같이 새로 요청을 보내면 될 것이라고 생각할 것임
그러나 이렇게 하게되면 1명이 아니라 모든 추천이 재로딩됨
문제를 정확히 풀기 위해, close1 클릭시 새로운 요청을 보내지 않고 아래와 같이 responseStream의 가장 최근의 응답에서 다른 유저를 랜덤 선택하여 추천하려고 함
이를 위해서는 단순한 merge로는 어렵고 combineLatest를 사용해야 함
combineLatest는 아래와 같이 두 스트림을 조인하여 항상 최신의 값을 유지시켜줌
close1ClickStream과 responseStream을 combineLatest 하게되면 close1클릭시에 마지막으로 응답받은 추천 리스트에 접근가능해지며 이중에서 다시 랜덤 선택하도록 하면됨
마지막으로 combineLatest는 두개의 스트림 모두 마지막 값이 있어야만 값을 반환할 수 있음
그러므로 startWith를 사용하여 시작시에 close1 버튼이 클릭된 것처럼 에뮬레이션해주어야 함
전체 코드
(전체 코드 링크)
함수형 스타일이란 명령적인것이 아니라 선언적인것임 (<- 이해하기 어려움 :D)
연속된 명령문의 전달이 아닌 스트림들 간의 관계를 정의하는 형태로 전달하려 함
즉, suggestion1Stream은 “최근 응답에서 한명의 추천을 선택하는 것과 리프레쉬 클릭시 또는 시작시에 빈추천으로 초기화 하는것을 결합한 close1 스트림”으로 컴퓨터에게 말해주는 것과 같음
if, for, while과 전형적인 callback 기반 코드로부터 탈출할 수 있음
map, filter, scan, merge, combineLatest, startWith 등과 같은 툴박스 함수들이 최소의 코드로 더 많은 기능을 실현가능하게 함