오늘 소개할 부분은 작성 중인 책의 “부록” 중 일부이다.
이 장에서는 RxJS Scheduler를 잘 사용하기 위해서 이해해야할 자바스크립트 비동기 처리 과정을 살펴본다.

RxJS를 모르는 사람이라도 자바스크립트의 비동기 처리과정을 이해하면 자바스크립트를 개발하는데 정말 많은 도움을 준다.
더 자세한 내용은 다음 URL을 참고하여 꼭! 읽어보기 바란다

참고 URL


부록. RxJS Scheduler와 자바스크립트 비동기 처리 과정의 이해

RxJS Scheduler는 RxJS에서 자바스크립트의 비동기 작업을 효과적으로 처리할수 있도록 도와주는 역할을 한다.
따라서, RxJS Scheduler를 잘 활용하기 위해서는 기본적으로 자바스크립트 엔진이 어떻게 비동기 작업을 처리하는 지를 알면 RxJS Scheduler를 사용하는데 많은 도움이 된다.

이 장에서는 자바스크립트 엔진이 어떻게 비동기 작업을 처리하는 지를 살펴보고, 각 비동기 작업에 해당되는 RxJS scheduler는 어떤 것이 있는지 살펴보기로 하자.

Continue reading

오늘 소개할 부분은 작성 중인 책 2부의 개론에 해당하는 내용이다.
이 장을 통해 RxJS의 개발과정의 큰 그림을 다시한번 살펴보기 바란다


1부에서는 RxJS의 본질을 알아가기 위해 RxJS가 고민했던 문제들을 살펴보았다. 2부에서부터는 RxJS 라이브러리에 대해 자세히 알아보자. 이 장을 통해서는 RxJS로 간단한 소스를 구현해보면서 RxJS의 사용법을 익혀보도록 하자.

RxJS

RxJS의 공식 사이트에서는 RxJS에 대해 다음과 같이 정의하고 있다.

RxJS is a library for composing asynchronous and event-based programs by using observable sequences.
RxJS는 Observable를 사용하여 비동기 및 이벤트 기반 프로그램을 작성하기 위한 라이브러리이다.

1부에서 필자가 정의한 범용적인 데이터 플로우 솔루션을 지향하는 라이브러리의 국소적인 표현이라고 할수 있다. 특이한 것은 공식 홈페이지에는 RxJS에 대해 이벤트용 lodash 정도라고 생각해라라는 말도 있다.

Think of RxJS as Lodash for events.

앞의 용어가 RxJS의 철학에 대한 정의라면, 뒤의 정의는 실제 사용에 대한 정의라고 볼수 있다.
RxJS가 어렵다면 지금은 그냥 비동기 Array/Collection 데이터를 다루는 라이브러리 정도로 생각하고 접근해보자.

Continue reading

오늘 소개할 부분은
웹어플리케이션 개발시 발생할 수 있는 로직 오류에 대한 문제를 RxJS는 어떻게 접근했는지에 대한 이야기이다.


웹어플리케이션의 로직

웹어플리케이션은 로직에 근거하여 전달받은 입력값을 이용하여 새로운 결과를 반환하거나 표현한다.
여기서 로직은 산술적인 로직이 될 수 있고 비즈니스적인 로직이 될수 있다. 또는 if문과 같이 간단한 프로그램의 흐름을 담당하는 부분일 수도 있다.

화면에 사용자 정보를 표현하는 UI 작성하는 예를 생각해보자. (예제에서 사용하는 API는 스타워즈 등장인물을 조회한다)
DB로부터 조회한 사용자 목록 데이터가 입력값이라면 이 값을 바탕으로 우리는 다양한 처리를 한다.

  • 성별이 “남”과 “여”인 사용자만 추출한다 (스타워즈 등장인물은 로봇과 같이 성별이 없는 사용자도 있다)
  • 사용자의 이름, 키, 몸무게를 표시한다.
  • 사용자의 성별에 맞게 아이콘을 화면에 표시한다.
  • 사용자의 표준 체중을 계산하여 표시한다.

    BROCA 방식

    • 남자 표준체중 = (키 - 100) × 0.9
    • 여자 표준체중 = (키 - 105) × 0.9

    BMI 방식

    • 남자 표준체중 = 키/100 _ 키/100 _ 22
    • 여자 표준체중 = 키/100 _ 키/100 _ 21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
const jsonData = JSON.parse(xhr.responseText);
document.getElementById("users").innerHTML = process(jsonData);
}
};
xhr.open("GET", "http://swapi.co/api/people/");
xhr.send();

// 데이터를 처리하는 함수
function process(people) {
const html = [];
for (const user of people.results) {
if (/male|female/.test(user.gender)) {
let broca;
let bmi;
if (user.gender == "male") {
broca = (user.height - 100 * 0.9).toFixed(2);
bmi = ((((user.height / 100) * user.height) / 100) * 22).toFixed(2);
} else {
broca = (user.height - 100 * 0.9).toFixed(2);
bmi = ((((user.height / 100) * user.height) / 100) * 21).toFixed(2);
}
const obesityUsingBroca = (((user.mass - broca) / broca) * 100).toFixed(
2
);
const obesityUsingBmi = (((user.mass - bmi) / bmi) * 100).toFixed(2);

html.push(`<li class='card'>
<dl>
<dt>${user.name} <i class="fa fa-${user.gender}"></i></dt>
<dd><span>키 : </span><span>${user.height} cm</span></dd>
<dd><span>몸무게: </span><span>${user.mass} kg</span></dd>
<dd><span>BROCA 표준체중 : </span><span>${broca} kg</span></dd>
<dd><span>BROCA 비만도 : ${obesityUsingBroca}</span></dd>
<dd><span>BMI 표준체중 : </span><span>${bmi} kg</span></dd>
<dd><span>BMI 비만도 : ${obesityUsingBmi}</span></dd>
</dl>
</li>`);
}
}
return html.join("");
}

원래 조회했던 데이터는 온데 간데 없고 그 데이터로부터 생산된 새로운 정보들을 사용하고 있다.
위 예제는 사용자 체중과 키를 이용하여 성별에 따라 비만도 값을 계산하고 성별 아이콘을 화면에 표시한다.

우리는 알게 모르게 데이터를 추출하고 변환하는 작업을 빈번하게 하고 있다.
복수 데이터를 처리하기 위해서는 반복문을 사용하고, 상황에 따라 데이터를 추출하거나 접근하기 위해서 분기문을 사용한다.
또한, 상황을 기억하거나 추출된 정보를 임시로 기억하기 위해서 변수를 사용한다.

로직의 복잡성 그리고 오류

반복문과 분기문 그리고 변수는 우리 코드를 복잡하게 만든다. 반복문은 우리 코드의 가독성을 떨어뜨리고 분기문은 우리가 확인해야할 프로그램의 흐름을 여러 개로 만든다. 더군다나 우리가 기억해 놓은 변수의 값은 누군가에 의해 변경될 수 있다. 변수의 값이 변하면 우리가 의도했던대로 흐름으로 프로그램이 동작하지 않을 수 있다.
이렇게 반복문과 분기문 그리고 변수는 우리 코드의 복잡도를 높이고 가독성을 떨어뜨리고, 결국에는 오류의 발생 빈도를 높인다.

반복문과 분기문

로직의 복잡성을 줄이는 가장 간단한 방법으로는 기능을 쪼개는 것이다. 기능별로 쪼갠다는게 단순히 구역별로 쪼개게 되면 기능의 의미를 명확하게 드러내지 못한다. 더불어 이런 코드는 재사용성을 떨어뜨린다.

이렇게 기능을 쪼개는 일이 쉬운 일이 아닌 이유는 코드의 대다수는 다음과 같이 로직과 반복문, 분기문의 결합으로 구성되어 있기 때문이다.

코드에서 반복문과 분기문을 모두 제거한다는 것은 사실상 불가능하다. 하지만 기능 단위로 분리 할 수 있다면 기능을 추상화 할 수 있고, 이로 인해 로직의 복잡성을 줄일 수 있다.

변수는 오류의 시작

변수를 사용한다는 의미는 오류를 발생시킬 수 있는 확률을 높일 수 있다. 변수는 변경될 수 있는 값이기 때문에 유용하다. 반면, 의도치 않게 이 값이 바뀔 경우에 우리는 오류에 직면하게 된다.
브라우저 환경의 자바스크립트에서는 싱글 쓰레드 구조이기 때문에 Mutil Thread의 사용으로 인한 동시성 문제는 자주 발생하지 않는다. 하지만 DOM에 등록된 이벤트 핸들러로 인해 변수의 값이 변경되거나 비동기 행위로 인해 외부로 노출된 변수의 값들이 변경 될 수 있다.

WebWorker와 같은 기술 스펙을 사용하면 Mutil Thread 기술을 사용할 수 있지만 브라우저는 기본적으로 하나의 메인 스레드에서 모든 작업이 이루어 진다.

따라서, 우리는 변수의 노출 범위를 제한하거나 제거함으로써 변수의 값이 외부에 의해 변경되지 않고 개발자의 의도에 따라 정확하게 변경될 수 있도록 보장하여만 한다.

자바스크립트의 솔루션

다행히도 자바스크립트는 이런 면에서는 꽤나 훌륭한 솔루션을 제공하고 있다.
함수형 프로그래밍의 특성을 가진 자바스크립트 함수를 이용하면 실제 로직과 상관이 없는 반복문, 분기문을 분리할 수 있다. 더불어 변수 또한 제거해 나갈 수 있다.
이렇게 함으로써 로직의 의미를 더욱 명확히 할 수 있으며 재사용성을 더욱 높일 수 있다.

자바스크립트 함수는 일급객체이다.
일급 객체(First-class object)는 다음과 같은 특성을 가지고 있다.

  • 변수 혹은 데이터 구조에 저장할 수 있다
    1
    var savedFunction = function() {};
  • 파라미터로 전달할 수 있다.
    1
    2
    3
    4
    function foo(f, value) {};
    foo(function() {
    console.log("함수를 파라미터로 전달 할 수 있다");
    }, "값");
  • 반환값으로 사용할 수 있다.
    1
    2
    3
    4
    5
    function foo() {
    return function() {
    console.log("함수를 반환할 수 있다");
    };
    }

로직의 분리

앞의 process 함수를 기능 단위의 로직과 반복문, 분기문으로 분리해보자.
process 함수는 다음과 같은 구조로 되어 있다.

여기에서 우리의 주요 관심사는 성별에 따라 비만도를 구하는 로직과 사용자별 HTML을 만드는 로직이다.
이 부분을 별도의 함수로 만들어보자.

표준 체중과 비만도를 계산하는 함수는 height, mass, gender을 입력값으로 받아서 BROCA와 BMI 방식의 비만도와 표준 체중을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 표준 체중과 비만도를 계산하는 함수
function logic(height, mass, gender) {
let broca = (height - (gender === "male" ? 100 : 105)) * 0.9;
let bmi = height / 100 * height / 100 * (gender === "male" ? 22 : 21);
if (gender == "male") {
broca = (height - 100 * 0.9).toFixed(2);
bmi = (height / 100 * height / 100 * 22).toFixed(2);
} else {
broca = (height - 100 * 0.9).toFixed(2);
bmi = (height / 100 * height / 100 * 21).toFixed(2);
}
const obesityUsingBroca = ((mass - broca) / broca * 100).toFixed(2);
const obesityUsingBmi = ((mass - bmi) / bmi * 100).toFixed(2);
return {
broca,
bmi,
obesityUsingBroca,
obesityUsingBmi
};
}

사용자 정보별 HTML을 만드는 함수는 user 정보를 받아서 string 형태의 html을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 사용자 정보를 표현하기 위해 HTML을 만드는 함수
function makeHtml(user) {
return `<li class='card'>
<dl>
<dt>${user.name} <i class="fa fa-${user.gender}"></i></dt>
<dd><span>키 : </span><span>${user.height} cm</span></dd>
<dd><span>몸무게: </span><span>${user.mass} kg</span></dd>
<dd><span>BROCA 표준체중 : </span><span>${user.broca} kg</span></dd>
<dd><span>BROCA 비만도 : ${user.obesityUsingBroca}</span></dd>
<dd><span>BMI 표준체중 : </span><span>${user.bmi} kg</span></dd>
<dd><span>BMI 비만도 : ${user.obesityUsingBmi}</span></dd>
</dl>
</li>`;
}

logic, makeHtml함수를 이용하면 다음과 같이 process 함수를 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
function process(people) {
const html = [];
for (const user of people.results) {
if (/male|female/.test(user.gender)) {
const result = logic(user.height, user.mass, user.gender);
Object.assign(user, result);
html.push(makeHtml(user));
}
}
return html.join("");
}

logic, makeHtml 함수를 만듦으로서 우리는 핵심 로직을 작성는데 집중할 수 있게 되었다.
더불어 logic, makeHtml 함수도 재사용 할 수 있는 단위 함수가 되었다.

반복문, 분기문, 그리고 변수와의 이별

위에 개선한 process도 좋은 코드이다. 우리는 구현 로직에 더 집중할 수 있게 되었다.
하지만, 흐름을 제어하는 반복문과 분기문은 여전히 process에 존재한다.
코드가 크면 클수록 process에 존재하는 반복문과 조건문은 우리 코드의 가독성을 떨어뜨릴 것이다. 더불어 html, result 같은 변수가 여전히 존재하기 때문에 우리는 항상 오류에 노출될 것이다.

이번에는 ES5에서 제공하는 Array의 filter, map, reduce와 같은 고차함수(High-order function)를 이용하여 process를 개선해 보자.

고차함수 (Higher-order function)

  • 다른 함수를 인자로 받거나 그 결과로 함수를 반환하는 함수.
    출처: wikipedia https://en.wikipedia.org/wiki/Higher-order_function
  • 고차 함수는 변경되는 주요 부분을 함수로 제공함으로서 동일한 패턴 내에 존재하는 문제를 손쉽게 해결할 수 있는 고급 프로그래밍 기법이다.
  • 고차 함수를 이용하면 함수의 합성, 변형과 같은 작업을 손쉽게 할수 있다. 더불어 Currying, Memoization과 같은 기법도 사용할 수 있다.
    1
    2
    3
    const twice = (f, v) => f(f(v));
    const fn = v => v + 3;
    console.log(twice(fn, 7)); // 13
1
2
3
4
5
6
7
8
9
10
11
12
13
function process(people) {
return people.results
.filter(user => /male|female/.test(user.gender))
.map(user => Object.assign(
user,
logic(user.height, user.mass, user.gender)
))
.reduce((acc, user) => {
acc.push(makeHtml(user));
return acc;
}, [])
.join("");
}

if문은 filter로 변환하고, 값을 변환해야하는 경우에는 map을 이용하고, 축적된 데이터를 반환해야하는 경우에는 reduce를 이용하였다.
각각의 고차함수에 전달되는 함수는 외부의 변수에 영향을 미치지도 않고, 영향을 받지도 않는 함수이다.
전달된 함수는 항상 같은 입력이 주어지면, 항상 같은 출력을 반환한다.
이런 함수를 함수형 프로그래밍에서는 순수함수라고 한다.

순수함수와 함수형 프로그래밍에 대한 내용은 부록. 함수형 프로그래밍 (Functional Programming)을 참조하기 바란다.

개선된 process에서는 반복문, 분기문, 변수가 존재하지 않는다.
핵심 로직은 분리되었고 코드의 흐름은 단일화되었다. 더불어 변수를 사용하지 않음으로서 오류의 발생 빈도도 크게 줄었다.

RxJS는 어떻게 개선하였나?

RxJS 또한 ES5 Array의 고차 함수와 같은 operator를 제공함으로써 로직에 존재하는 분기문과 반복문, 그리고 변수를 제거하려고 하였다.

Immutable 객체 Observable

ES5 Array의 고차함수들이 반환값으로 새로운 Array 객체를 반환하여 각각에 영향을 미치지 않도록 하는 것과 같이 RxJS의 operator는 항상 새로운 Observable을 반환함으로써 Array의 고차함수와 같이 불변 객체 (Immutable Object)를 반환한다.
불변 객체는 생성 후 그 상태를 바꿀 수 없는 객체이다. 불변 객체는 외부에서 값을 변경할 수 없기 때문에 불변 객체를 사용하는 것만으로도 프로그램의 복잡도가 줄어든다.

1
2
3
4
var arr = [1, 2, 3];
var mappedArr = arr.map(v => v);

console.log(arr === mappedArr); // false

Array와 다른점이 있다면 Array의 경우는 새로운 Array 객체 생성 작업만 하지만 Observable은 새로운 Observable를 만들고, 그 Observable이 operator를 호출한 Observable을 내부적으로 subscribe 하는 형태를 유지한다. 즉, Linked list 형태로 기존 Observable 객체와 새롭게 만든 Observable 객체를 operator로 연결하고 있다.

한 예로 Observable.map은 다음과 같이 구현되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Observable.prototype.map = function(transformationFn) {
const source = this;
const result = new Rx.Observable(observer => {
// 새로운 Observable은 현재의 Observable을 subscribe 한다.
source.subscribe(
// 현재의 Observable에서 전달된 데이터를 변경하여 전달한다.
function(x) {
observer.next(transformationFn(x));
},
function(err) {
observer.error(err);
},
function() {
observer.complete();
}
);
});
return result;
};

이런 구조를 취함으로서 source부터 전달된 데이터, 에러, 종료여부가 Observable의 operator들을 통해 전달되거나 변경되어 구독한 Observer에게 전달할 수 있게 된다.

앞에서 설명한 표준체중과 비만도 조회 예를 Observable로 변경해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const ajax$ = new Rx.Observable(observer => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
const jsonData = JSON.parse(xhr.responseText);
// user 데이터를 user 단위로 변환하여 전달
jsonData.results.forEach(v => observer.next(v));
observer.complete();
}
};
xhr.open("GET", "http://swapi.co/api/people/");
xhr.send();
});

ajax$
.filter(user => /male|female/.test(user.gender))
.map(user => Object.assign(user, logic(user.height, user.mass, user.gender)))
.reduce((acc, user) => {
acc.push(makeHtml(user));
return acc;
}, [])
.subscribe(v => {
document.getElementById("users").innerHTML = v;
});

이전 우리가 개선한 process 소스와 큰 차이가 없다.

원리 설명을 위해 부득이하게 사용자 리스트를 사용자로 바꿔 전달하였다.
일반적으로는 Ajax를 통해 전달 받은 정보를 바탕으로 operator를 적용한다.

차이라면 다음과 같이 ajax$ Observable를 통해 전달 받은 데이터가 filter, map, reduce를 거쳐 observer에게 전달되는 구조로 되어있다는 것이다.

RxJS가 제공하는 Operator

다음은 공식 홈페이지에서 제공하는 operator 목록이다.
http://reactivex.io/rxjs/manual/overview.html#categories-of-operators

카테고리 operator
생성 operator ajax, bindCallback, bindNodeCallback, create, defer, empty, from, fromEvent, fromEventPattern, fromPromise, generate, interval, never, of,repeat ,repeatWhen, range ,throw ,timer
변환 operator buffer, bufferCount, bufferTime, bufferToggle, bufferWhen, concatMap, concatMapTo, exhaustMap, expand, groupBy, map, mapTo, mergeMap, mergeMapTo, mergeScan, pairwise, partition, pluck, scan, switchMap, switchMapTo, window, windowCount, windowTime, windowToggle, windowWhen
추출 operator debounce, debounceTime, distinct, distinctKey, distinctUntilChanged, distinctUntilKeyChanged, elementAt, filter, first, ignoreElements, audit, auditTime, last, sample, sampleTime, single, skip, skipUntil, skipWhile, take, takeLast, takeUntil, takeWhile, throttle, throttleTime
결합 operator combineAll, combineLatest, concat, concatAll, exhaust, forkJoin, merge, mergeAll, race, startWith, switch, withLatestFrom, zip, zipAll
멀티캐스팅 operator cache, multicast, publish, publishBehavior, publishLast, publishReplay, share
에러 처리 operator catch, retry, retryWhen
유틸리티 operator do, delay, delayWhen, dematerialize, finally, let, materialize, observeOn, subscribeOn, timeInterval, timestamp, timeout, timeoutWith, toArray, toPromise
조건.참거짓 operator defaultIfEmpty, every, find, findIndex, isEmpty
수학,누적 operator count, max, min, reduce

RxJS에서 제공하는 operator를 이용하면 Observable을 생성 할 수도 있고 전달된 데이터를 변환하거나 필요한 데이터만을 추출할 수 도 있다.
더불어 여러 개의 Observable을 합성하기도 하고, 하나의 Observable을 다른 여러개의 Observable로 나눌 수도 있다.

RxJS는 정말 많은 operator를 제공한다. operator의 의미에 대해 잘 아는 사람에게는 굉장히 편리하다. 반면 그 의미를 잘 알지 못하는 사람에게는 오히려 진입 장벽이 되기도 한다.
이 operator의 기본적인 철학은 함수형 프로그래밍에 그 근간을 두고 있다.
아마도 함수형 프로그래밍 언어를 배운 독자라면 꽤 익숙한 이름의 operator들이 있는 것 을 알 수 있다.

RxJS의 operator는 어휘와 같다.
내가 많은 단어와 문장을 알아서 사용하게 되면 나의 편의성이 증가하고 더불어 나의 품격도 높아질수 있다. 하지만, 다른 한편으로는 다른 사람이 내 말을 이해하기 어려워 할 수도 있다.
반면 내가 알고 있는 단어와 문장이 적다고 하더라도 의사소통이 될 정도의 단어와 문장을 사용한다면 생활하는데 부족함이 없다. 마찬가지로 RxJS의 모든 operator를 다 알 필요는 없다.
카테고리별로 자주쓰는 operator 몇 개를 잘 알고 적용할 수 있다면 충분히 RxJS의 장점을 극대화 할수 있다.
따라서, 이 책에서도 RxJS의 모든 Operator를 다루지는 않는다. 주요 카테고리별로 꼭! 알아야하는 RxJS의 Operator를 몇 개를 기준으로 설명을 할 예정이다.
자세한 내용은 다음 2부에서 진행하는 실제 프로젝트를 통해 조금씩 익혀나가 보자.

정리

웹어플리케이션의 로직은 반복문, 분기문, 변수에 의해 복잡도가 증가한다. 복잡도가 증가하게되면 이로 인해 코드의 가독성이 떨어지고 결국에는 오류에 직면하게 된다.
ES5 Array의 고차함수를 이용하면 반복문, 분기문, 변수를 로직으로부터 분리하고 제거할수 있다. 마찬가지로 RxJS는 ES5 Array의 고차함수와 같은 operator를 제공한다.
operator는 Immutable한 Observable를 항상 생성함으로써 외부나 내부에 영향을 미치지 않는다. 이런 구조는 오류의 발생 빈도를 낮추는 역할을 한다.

Comment and share

오늘 소개할 부분은
웹어플리케이션 개발시 발생할 수 있는 상태 전파 문제를 RxJS는 어떻게 접근했는지에 대한 이야기이다.


웹어플리케이션의 상태

우리가 만드는 웹어플리케이션은 하나의 큰 상태 머신이고 이를 구성하고 있는 크고 작은 단위들 또한 하나의 상태머신이다.
각각의 상태 머신들은 각자의 상태를 가지고 있고, 상태 머신들은 각자의 역할에 따라 서로 유기적으로 연결되어 있다.

A라는 작은 상태 머신의 상태값은 B의 입력값이 될수 있고, B의 상태값은 C와 D에 관련 있는 상태값 일수 있다.
따라서, A의 상태 값은 B로 전달되어야하고 B의 상태값은 다시 C와 D에 전달되어야만 한다.
이렇게 A의 상태 변화 정보가 B에 전달되어야하고 B의 상태값이 다시 C와 D에 전달되어야하는 이유는 바로 A, B, C, D 모듈간에 의존성이 있기 때문이다.

간단한 예를 들어보자.
사용자 정보(상태)를 System 클래스가 check() 함수에서 사용하고 있는 예제이다.
System과 User간에는 다음과 같은 의존성이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class User {
constructor() {
this._state = {
name: "손찬욱",
isLogin: false
};
}
getName() {
return this._state.name;
}
isLogin() {
return this._state.isLogin;
}
login(name) {
this._state.name = name;
this._state.isLogin = true;
}
logout() {
this._state.name = "";
this._state.isLogin = false;
}
}

class System {
constructor(user) {
this._token = null;
this._id = "System";
this._user = user;
}
check() {
const username = this._user.getName();
if (this._user.isLogin()) {
this._token = [...username].reduce((acc, v) => acc + v.charCodeAt(0), 0);
console.log(`[${this._id}] ${username} 의 토큰은 ${this._token} 입니다`);
} else {
this._token = null;
console.log(`[${this._id}] 로그인 되지 않았습니다`);
}
}
}

let user = new User();
let system = new System(user);

// System 작업
system.check(); // [System] 로그인 되지 않았습니다

// User의 상태변화 발생
user.login("sculove");

// System 작업
system.check(); // [System] sculove 의 토큰은 769 입니다

// User의 상태변화 발생
user.logout();

// System 작업
system.check(); // [System] 로그인 되지 않았습니다

System은 User의 로그인 정보에 의해 System의 출력(상태)이 결정된다.

웹어플리케이션의 상태변화로 인한 문제점.

System과 User 코드는 굉장히 간단하면서도 훌륭한 예제이다. 하지만, 실프로젝트에서 사용된다면 많은 문제점이 발생할 수 있는 코드이다.

첫째, User의 인터페이스가 변경되면, System도 함께 변경을 해주어야한다.

System에서는 user의 메소드인 getName, isLogin을 사용하고 있다.

1
2
3
4
5
6
7
8
9
10
11
class System {
// ...
check() {
const username = this._user.getName();
if (this._user.isLogin()) {
// ...
} else {
// ...
}
}
}

위 코드는 작은 코드 조각이기 때문에, 크게 문제가 되지 않을수 있지만, 클래스의 크기가 커지면 커질수록 변경에 대한 영향도는 점점 커질 것이다.
뿐만 아니라, 지금은 System이 User를 사용하고 있는 상황이지만, 다른 클래스 A, B, C, … 등이 모두 User에 의존도를 가지고 있을 경우에는 더욱 더 변경에 대한 영향도는 커질 것이다.

둘째, User 상태를 확인하기 위한 인터페이스에 대한 의사소통 비용이 발생한다.

User를 개발한 개발자와 User와 의존관계가 있는 class A, class B, class C,… 을 개발한 개발자 사이에는 의사소통에 따른 비용이 발생하게 된다.
지금은 User의 인터페이스가 getName, isLogin, login, logout 정도이지만, 인터페이스가 많아 질 경우 이에 대한 비용이 증가하게 될 것이다.

셋째, 다수의 클래스가 User에 의존 관계가 있는 경우라면, User의 변경여부를 반영하기 위해 다수의 클래스들이 직접 User의 상태를 모두 반영해야만 한다.

즉, 변경에 대한 전파가 원활하게 이루어지지 않는다.
이는 꽤나 번거로운 작업일 뿐만 아니라, User와 의존성이 있는 다수의 클래스들과의 의존 관계를 항상 염두에 두고 개발을 해야만 하기 때문에, 잦은 오류가 발생하기 쉽다.

1
2
3
4
5
6
7
8
9
10
11
// User 상태변화 발생
user.login("sculove");

// User와 의존관계가 있는 classA
// User와 의존관계가 있는 classB
// User와 의존관계가 있는 classC
// User와 의존관계가 있는 classN ...
classA.process();
classB.process();
classC.process();
classN.process();

우리가 이미 알고 있는 솔루션 - Observer Pattern

앞에서 이야기했던 문제점은 이미 우리가 익히 알고 있는 패턴으로 대부분 해결이 가능하다.
바로 Observer Pattern이다.
자 그럼 Observer Pattern에 대해 잠시 살펴보자.

Loose Coupling

Observer Pattern에서는 상태가 변경될 대상을 Subject라고 한다. 그리고, 그 상태 변화를 관찰하는 대상을 Observer라고 한다.
Observer Pattern에서는 Subject와 Observer가 서로 느슨하게 연결되어 있다.
여기서 느슨하게 연결되었다 (Loose Coupling)는 의미는 Subject와 Observer가 서로 상호작용을 하지만, 서로 잘 모른다는 의미이다.
Subject가 Observer 에 대해서 아는 것은 Observer가 특정 인터페이스(update)를 구현한다는 것 뿐이다.
Observer는 언제든지 추가, 삭제할 수 있으며, 새로운 타입의 Observer를 추가하려고 해도 Subject를 변경할 필요가 전혀 없다.
또한, Subject와 Observer는 서로 독립적으로 사용이 가능하며, Observer 가 바뀌더라도 서로한테 영향을 미치지 않는다.

자동 상태 전파

기존 방식과 같이 데이터를 얻고자 하는 대상이 데이터를 직접 가져오는 방식은 매번 요청을 하여 변경 사항을 확인해야만 한다.
하지만 Observer Pattern은 이와 다르게 의존 관계의 대상(Subject)으로 부터 데이터를 제공 받는 방식이다.
전자의 경우를 Pull 방식이라고 후자를 Push 방식이라고 한다.

Push 방식은 Pull 방식에 비해 상태 전파 문제를 효과적으로 처리 할 수 있다.
Push 방식으로 구성된 Observer Pattern은 Subject의 상태가 변경되었을 경우 관찰하는 Observer에게 자동으로 알려준다. 특히, Subject와 Observer가 1:n의 상황에서는 더욱 유효하다.
다수의 Observer를 Subject에 등록하기만하면 Subject의 변경사항이 등록된 다수의 Observer에게 자동으로 전달된다.
개발자는 데이터 변경 시점을 매번 확인할 필요도 없고 신경쓸 필요도 없다. 단지 변경되었다는 신호가 왔을 경우 처리만 해주면된다.

인터페이스의 단일화

앞에서 살펴본 바와 같이 인터페이스가 있다는 것은 많은 비용을 수반한다. 인터페이스가 증가 할때마다 개발자간의 의사소통 비용이 증가하고 변경 영향도도 커진다.
사실 이 문제는 인터페이스를 줄이는 것 만으로도 비용을 줄일 수 있다. 하지만 더욱 좋은 방법은 인터페이스가 있어도 없게 만드는 것이다. 어려운 이야기지만 사실 간단하다. 인터페이스를 특정 몇개로 통일하는 것이다.
모든 객체가 쓰는 인터페이스는 methodA, methodB, methodC 이다라고 정의하면 서로의 의사소통 비용이 줄어든다. 더불어, 변경사항이 생기더라도 영향도는 기존 보다 훨씬 작아진다.
Observer Pattern은 Observer.update만 존재하기 때문에 Subject에서는 Observer 인터페이스에 대한 별도의 비용이 존재하지 않는다.

Observer Pattern의 흔한 예

실제 Observer Pattern으로 작성된 예제를 통해 Observer Pattern을 이해해보자.
이런 Observer pattern의 가장 흔한 예로는 뉴스를 발행하는 신문사와 이를 구독하는 고객들이 있는 경우가 있다.

뉴스를 발행하는 신문사(Subject)는 고객들(Observer)를 등록하고, 신문이 발행될 때, 각각의 고객들에게 신문이 발행되었다고 알려준다.(notify)
각 고객들은 신문이 발행되었을때, 어떤 고객은 뉴스를 스크랩하거나, 어떤 고객은 뉴스를 읽기 시작한다.

이를 다이어그램으로 표현하면 다음과 같다.

이제 간단하게 코드로 작성해보자.

뉴스 정보를 저장하는 클래스를 작성해보자. 이 클래스는 뉴스 정보를 변경할 수 있는 setNews() 메소드를 제공한다.

1
2
3
class NewsPaper {
setNews(news) {}
}

NewsPaper 클래스를 Subject 역할을 할수 있도록 구현해보자.
각각의 Observer를 등록, 삭제 할수 있는 add, remove 메소드를 추가하고,
상태 변경이 일어났을 때 각각 Observer의 update 메소드를 호출하는 notify 메소드를 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class NewsPaper {
constructor() {
this._observers = [];
}
setNews(news) {
this.notify(news);
}
add(observer) {
this._observers.push(observer);
}
remove(observer) {
let idx = this._observers.indexOf(observer);
if (idx !== -1) {
this._observers.splice(idx,1);
}
},
notify(news) {
this._observers.forEach( v => {
v.update(news);
});
}
}

이제는 뉴스를 구독하고자 하는 각각의 NewsScrapper와 NewsReader Obsever를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
class NewsScrapper {
update(news) {
console.log(`뉴스를 스크랩하자 - ${news}`);
}
}

class NewsReader {
update(news) {
console.log(`뉴스를 읽자 - ${news}`);
}
}

자 이제 Observer Pattern을 이용하여 Subject와 Observer들을 만들어 보았다.
잘 동작하는지 확인해보자.

NewsPaper에 구독을 원하는 Observer(NewsScrapper, NewsReader)를 등록하고 NewsPaper의 setNews()를 통해 뉴스의 내용을 변경해보자.

1
2
3
4
5
6
7
8
9
10
11
let newsPaper = new NewsPaper();

// 구독하기
newsPaper.add(new NewsScrapper());
newsPaper.add(new NewsReader());

// 상태 변경
newsPaper.setNews("북한 미사일 발사!!!");
newsPaper.setNews("코스피 최저점 이탈!!!");
newsPaper.setNews("남북평화회담 성사");
newsPaper.setNews("남북통일");

새로운 뉴스가 구독자들에게 잘 전달이 되는가?

1
2
3
4
5
6
7
8
뉴스를 스크랩하자 - 북한 미사일 발사!!!
뉴스를 읽자 - 북한 미사일 발사!!!
뉴스를 스크랩하자 - 코스피 최저점 이탈!!!
뉴스를 읽자 - 코스피 최저점 이탈!!!
뉴스를 스크랩하자 - 남북평화회담 성사
뉴스를 읽자 - 남북평화회담 성사
뉴스를 스크랩하자 - 남북통일
뉴스를 읽자 - 남북통일

상태 변경이 잘 전파되고 있다.
좋다.

Observer 패턴 적용 하기

Observer 패턴을 우리가 고민했던 문제의 예제에 적용해보자.
상태 변화를 관찰할 User를 Subject로 만들고, System을 Observer로 변경해보자.

Subject는 각각의 Observer를 관리하는 기능 자제가 별도로 존재하기 때문에, 이를 상위 클래스로 만들고,
User를 하위 클래스로 변경해보자.

User의 상위 클래스로 사용될 Subject를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Subject {
constructor() {
this._observers = [];
}
add(observer) {
this._observers.push(observer);
}
remove(observer) {
let idx = this._observers.indexOf(observer);
if (idx !== -1) {
this._observers.splice(idx, 1);
}
}
notify(status) {
this._observers.forEach(v => {
v.update(status);
});
}
}

Subject를 User의 부모 클래스로 지정하고, Observer인 System은 User와의 의존 관계를 제거 한다.
check 메소드는 Subject로 부터 데이터를 전달받을 상태정보(status)를 파라미터로 받고, 메소드명을 update로 변경한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class User extends Subject {
constructor() {
this._state = {
name: "손찬욱",
isLogin: false
};
},
getName() {
return this._state.name;
},
isLogin() {
return this._state.isLogin;
},
login(name) {
this._state.name = name;
this._state.isLogin = true;
this.notify(this._state);
},
logout() {
this._state.name = "";
this._state.isLogin = false;
this.notify(this._state);
}
}

class System {
constructor() {
this._token = null;
this._id = "System";
},
update(status) {
if (status.isLogin) {
this._token = Array.prototype.reduce.call(
status.username,
(acc, v) => acc + v.charCodeAt(0), 0);
console.log(`[${this._id}] ${status.username} 의 토큰은 ${this._token} 입니다`);
} else {
this._token = null;
console.log(`[${this._id}] ${status.username} 은(는) 로그인 되지 않았습니다`);
}
}
}

let user = new User();
let system = new System();

user.add(system);

// User의 상태변화 발생
user.login("sculove");
user.logout();
user.login("crazymonlong");

System은 생성자에서 더이상 User의 인스턴스를 받지 않는다. 기존에 비해 의존성은 느슨해졌고 변경 상태도 의존성을 가진 모든 객체에 즉시 전파가 된다.

Login 클래스에 생성자의 파라미터로 구분자를 받는다면, 다수의 Login 모듈을 User에 등록하여 사용할 수도 있다.

1
2
3
4
5
6
7
class System {
// ...
constructor(id) {
this._id = id;
}
// ...
}

이제는 User의 add를 통해 등록만 하면, User의 상태 변화를 모두 감지할 수 있다.

1
2
3
4
5
6
7
8
let user = new User();
let observer1 = new System("observer1");
let observer2 = new System("observer2");
let observer3 = new System("observer3");

user.add(observer1);
user.add(observer2);
user.add(observer3);

변경에 대한 인터페이스도 update 하나로 간결해졌다.

RxJS는 무엇을 해결하고자 했는가?

RxJS는 상태 변화에 대한 문제를 Observer Pattern을 기반으로 해결하려고 하였다. 다만 기존 Observer Pattern에서 아쉬웠던 몇가지를 개선하였다.
앞에서 살펴본 신문사와 구독자의 예를 기준으로 살펴보자.

RxJS가 개선하려고했던 Observer Pattern

1. 상태 변화는 언제 종료되는가?

만약, 뉴스 서비스 종료로 더이상 뉴스를 전달하지 않게 되었다면 우리는 어떻게 구독자들(NewsScrapper, NewsReader)에게 이 내용을 전달할 수 있을까?
아마 다음과 같이 뉴스서비스 종료라는 특정 문자를 각 구독자들에게 보내고,
구독자는 뉴스서비스 종료라는 상태가 전달이 되면 구독이 중지되었다고 생각하고 별도의 처리를 해야만 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class NewsScrapper {
update(news) {
if (news === "뉴스서비스 종료") {
console.log("뉴스 스크랩 서비스가 종료되었음");
} else {
console.log(`뉴스를 스크랩하자 - ${news}`);
}
}
}

class NewsReader {
update(news) {
if (news === "뉴스서비스 종료") {
console.log("뉴스 읽는 서비스가 종료되었음");
} else {
console.log(`뉴스를 읽자 - ${news}`);
}
}
}

// NewsScrapper 구독
newsPaper.add(new NewsScrapper());
// NewsReader 구독
newsPaper.add(new NewsReader());

// 상태 변경
newsPaper.setNews("북한 미사일 발사!!!");
//...

// 종료시
newsPaper.setNews("뉴스서비스 종료");

이는 우리가 처음에 고민했던, 의사소통 비용에 대한 문제를 완벽하게 해결하지 못한 경우라고 이야기할 수 있다.
Observer Pattern은 상태를 전달하는 Subject의 데이터가 언제 종료되는지를 Observer들은 알 수가 없다.
이를 해결하기 위해서는 위와 같이 Observer와 Subject간에 별도의 규칙을 정해야만 한다. 결국 우린 또 다른 의사소통 비용을 쏟아야만하고, 결국 이런 의사소통의 산출물로 코드에 if문을 만들어야만 한다.
개발자로서 꽤 찝찝하고, 아쉬운 부분이다.

2. 상태 변화에서 에러가 발생하면?

프로그램에서 기능이 정상 동작하는 경우가 대다수이지만 꼭 고민해야할 부분 중의 하나가 바로 에러 처리이다.
프로그래머가 예측한 경우에 대한 에러 일수도 있고 예측 불가능한 에러 일 수도 있다.
하지만, 분명한 것은 우리가 개발한 서비스는 항상 정상적인 상황에서 돌아가지만은 않는다는 것이다.

NewsPaper의 setNews 메소드에서 다음과 같이 에러가 발생하게 되면 어떻게 될까?

1
2
3
4
5
setNews(news) {
throw new Error("NewsPaper Error"); // 에러 발생.

this.notify(news);
}

우선, Subject 자체적으로 에러처리를 할 수 있다.
try-catch 문을 사용하면, 에러 발생 시 다시 한번 상태 변경을 시도 하거나, 상태 변경 작업 자체를 무시하는 등의 작업을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
setNews(news) {
try {
throw new Error("NewsPaper Error"); // 에러 발생.

this.notify(news);
} catch(e) {
// 다시 시도~!
this.notify(news);
}
}

위와 같이 Subject 쪽에서 에러에 대한 처리를 별도로 하면, 등록된 Observer들은 상태 변경시 에러가 발생했는지 정상 동작했는지 모른다. 물론, 이 방법도 좋다.
하지만, 경우에 따라서는 등록된 Observer 쪽에서 에러 발생 여부를 인지하고, 이에 대한 별도의 처리를 해야하는 경우가 필요하다.
Observer 패턴은 update 인터페이스만을 통해서 Subject의 상태를 Observer에게 전달하기 때문에, 이 상황을 처리하기에는 어렵다.
아쉽지만, Observer 패턴은 에러 발생 여부를 Observer들에게 전달할 방법이 딱히 없다.

3. Observer에 의해 Subject의 상태가 변경되는 경우는?

신문사 기자이면서 동시에 구독자인 사람이 있는 경우라면 어떻게 될까?
만약 이 기자가 구독받은 기사의 내용을 조금 변경하여 다시 뉴스를 만드는 기레기 기자라면 우리는 생각보다 심각한 상황에 빠지게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class WriterAndReader {
constructor(newsPaper) {
this._newPaper = newsPaper;
}
update(news) {
console.log(`전달 받은 뉴스 - ${news}`);
this._newPaper.setNews(`변형된 뉴스 - ${news}`);
}
}

// WriterAndReader 구독
newsPaper.add(new WriterAndReader(newsPaper));

// 상태 변경
newsPaper.setNews("북한 미사일 발사!!!");

뉴스는 계속 반복적으로 생성되어 결국 브라우저는 뻗게 될 것이다.
이와 같이 Subject를 관찰하는 Observer가 Subject의 상태를 변경하는 경우에는 우리가 예상하지 못하는 복잡한 상황에 직면하게 될 수 있다.
이 예는 다소 과한 면이 있지만 우리가 프로그램을 하다보면 이렇게 데이터가 양방향으로 흐르게하는 경우가 종종 있다.
데이터의 흐름이 양방향으로 흐르게 함으로써 편한 면도 있지만 궁극적으로는 코드의 복잡도를 증가시키는 경우가 많다.

RxJS는 어떻게 개선하였나?

RxJS 역시 Observer 패턴으로 상태 전달 문제를 해결하려고 하였다.

우선 RxJS에 녹아 있는 Observer 패턴을 살펴보자.
RxJS에서 전달되는 데이터는 모두 Observable 형태로 반환된다.
Observable은 subscribe 과정 후부터 데이터를 전달받기 시작한다.

1
2
3
4
const click$ = Rx.Observable.fromEvent(document, "click");
click$.subscribe(v => {
console.log(v);
});

반면, Observer 패턴에서는 Subject와 Observer가 add 과정 후 부터 데이터를 전달받기 시작한다.

1
2
3
4
5
6
let newsPaper = new Subject();
newsPaper.add({
update: function(v) {
console.log(v);
}
});

둘의 관계는 닮았다. RxJS의 Observable은 Observer 패턴의 Suject와 닮았다.
그런데 Observer 패턴의 Observer에 해당하는 subscribe의 파라미터는 정작 객체가 아니라 함수이다.

이런 차이가 발생하게 된 배경은 RxJS가 기존 Observer 패턴의 아쉬웠던 점을 개선하고자 했기 때문이다.
RxJS가 개선한 Observer 패턴에 대해 살펴보도록 하자.

RxJS가 개선한 Observer Pattern

인터페이스의 확장

1장에서 살펴본 바와 같이 RxJS는 시간의 축으로 데이터를 보기 때문에 데이터의 연속적인 변화를 Observer에서 표현할수 있도록 기존 update 메소드를 next로 바꾸었다.
또한 Observer 패턴은 종료시점, 에러시점에 대한 인터페이스가 존재하지 않기 때문에, 종료를 나타내는 complete 메소드와 에러시점을 나타내는 error 메소드를 추가하였다.

즉, Observer패턴에서는 하나의 메소드를 갖는 Observer를 사용하였다면 RxJS의 Observer는 next, complete, error의 3개 메소드를 갖는다. 그런데 객체가 아닌 왜 함수를 사용하는 것인가?
물론, Observable.subscribe는 다음과 같이 객체, 함수 모든 형태로든 다 전달 받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const numbers$ = Rx.Observable.of([1, 2, 3, 4, 5, 6, 7, 8]);

// next, error, complete가 있는 객체를 받음
numbers$.subscribe({
next(v) {
console.log(v);
},
error(e) {
console.error(e);
},
complete() {
console.log("complete");
}
});

// next 함수만 받음
numbers$.subscribe(v => {
console.log(v);
});

// next, error 함수만 받음
numbers$.subscribe(
v => {
console.log(v);
},
e => {
console.error(e);
}
);

// next, error, complete 함수를 받음
numbers$.subscribe(
v => {
console.log(v);
},
e => {
console.error(e);
},
() => {
console.log("complete");
}
);

하지만, RxJS의 subscribe는 특별한 경우를 제외하고는 가급적 함수 형태를 사용한다.
그 이유는 객체는 상태를 가질 수 있기 때문이다.
객체가 상태를 가진다는 의미는 또 다른 상태 머신이 될 수 있다는 의미이기도 하다. 반면 함수는 상태가 존재하지 않는 기능만을 담당하기 때문에 상태에 관한 문제에서는 보다 자유롭다.
이와 관련된 내용은 3장에서 다룰 로직 오류에 대한 문제에서 다시 한번 다루도록 하겠다.

Observable은 Read-only

“기자이면서 구독자인 사례”와 같이 데이터가 양방향으로 흐르는 문제를 RxJS는 구조적으로 개선하려고 하였다.
Observable은 subscribe를 통해 데이터를 전달할 대상(Observer)에게 데이터를 전달할 수 있지만, 반대로 전달 대상(Observer)에게는 데이터를 전달할 수 없다. Observable은 전달하는 데이터를 설정할 수 있는 그 어떤 메소드도 제공하지 않는다. Observable은 데이터를 얻거나 변경 또는 머지만 할 수있을 뿐 데이터를 생성하지는 못한다. 즉, Observable에서 전달된 데이터는 Observer 한 방향으로만 흐르게 된다

이렇게한 근본적인 이유는 데이터 흐름을 단순화함으로 복잡도를 낮추고 오류 발생 빈도를 줄이기 위해서 이다.
물론, 데이터가 양방향으로 흐르게 되면 사용상 편리할 수는 있다. 하지만, 어플리케이션의 규모가 커지게 되면 양방향으로 흐르는 데이터의 복잡도는 통제하기 어려울 정도로 복잡해진다.

이러한 이유로 최근 등장한 프레임워크들은 모두 단방향 데이터 흐름을 지향한다.

React와 Angular2+, Vue 모두 단방향 데이터 흐름을 지향한다.

Observable은 리액티브하다.

RxJS는 Observer 패턴과 마찬가지로 데이터가 발생하게되면 Observer에게 자동으로 그리고 빠르게 변경된 데이터를 전달한다.
이를 보고 리액티브하다고 이야기한다.
리액티브하다라는 의미를 이해하기 위해서는 우선 리액티브 프로그래밍(Reactive Programming)에 대한 정의 부터 살펴보자. 위키피디아에서는 다음과 같이 정의되어 있다.

리액티브 프로그래밍은 데이터 흐름과 상태 변화 전파에 중점을 둔 프로그램 패러다임이다. 사용되는 프로그래밍 언어에서 데이터 흐름을 쉽게 표현할 수 있어야하며 기본 실행 모델이 변경 사항을 데이터 흐름을 통해 자동으로 전파한다는 것을 의미한다.

출처 : https://en.wikipedia.org/wiki/Reactive_programming

위 정의에서 가장 핵심이 되는 단어는 데이터 흐름자동으로 전파이다. 즉, 상태 변화의 흐름이 자동으로 전파되는 것을 리액티브하다고 이야기한다.

리액티브의 가장 흔하게 드는 예로 ‘엑셀’을 이야기할 수 있다.

A열의 값과 B열의 값의 합을 나타내는 C열은 A열이나 B열의 값이 변화되는 경우 자동으로 C열의 값이 변경된다.
이렇게 A나 B열의 변경사항이 데이터 흐름을 통해 자동으로 C열에 전파되도록 구조화하는 프로그래밍의 패러다임을 리액티브 프로그래밍이라고 한다.

이 말은 앞에서 살펴 보았던 상태 변화에 대한 우리의 고민들과 일맥 상통한다고 이야기할 수 있다.
이런 고민의 해결책이 Observer 패턴이고, RxJS는 이런 Observer 패턴을 개선하여 어플리케이션에서 발생하는 모든 데이터를 리액티브하게 전달 할 수 있게 해준다. 따라서, RxJS는 리액티브 프로그래밍(Reactive Programming)을 지향하는 라이브러리이다.

정리

이 장에서는 웹어플리케이션의 상태가 어떻게 전파되는지를 살펴 봄으로써 상태 전파로 인해 발생할 수 있는 문제점을 살펴보았다. 또한, 이러한 문제를 효과적으로 해결했던 Observer 패턴에 대해서도 살펴보았다.
Observer 패턴은 느슨하게 연결되어 Subject와 Observer간의 의존도를 줄였으며, Push 방식으로 데이터를 전파함으로써 상태 전파에 대한 많은 문제를 해결했다.
RxJS에서는 이런 Observer 패턴을 개선하여 상태 전파 문제를 해결하려고 하였다.
에러상황과 종료상황에 대한 인터페이스를 확장하였고, 데이터를 단방향으로 흐를 수 있도록 개선함으로써 코드의 복잡도를 낮추었다. 이런 결과 RxJS는 궁극적으로 Reactive Programming을 지향하는 라이브러리가 되었다.

다음장에서는 RxJS가 고민한 로직 오류에 대해 살펴보기로 하자.

Comment and share

몇일 전에 요즘 내가 쓰고 있는 책의 초안을 일부를 공개하기로 셀프 선언한 이후, 사실 너무 바빴다 ㅠㅠ
1부 탈고일이 좀 남았지만… 마음이 급하다.
지인과는 과감히 탕수육 내기도 했으니. 더 급하다.

처음이니깐 간단히 책소개만 해보면.

RxJS가 무엇을 위해 준비된 라이브러리인지에 대한 답을 구하는 책이다. 더불어 RxJS의 활용법도 학습하는 책이다. 참고로 rxjs5 기준으로 쓰고 있다

오늘 소개할 부분은
웹어플리케이션 개발시 발생할 수 있는 입력 오류를 RxJS는 어떻게 접근했는지에 대한 이야기이다.


웹어플리케이션의 입력 데이터

웹어플리케이션의 동작 과정을 되돌아보면 사실 몇 개의 큰 과정으로 나눌 수 있다.

간단한 게시판을 예로 생각해보자.
게시판은 서버에 저장된 글을 보여주는 목록화면과 게시글의 내용을 보여주는 상세화면으로 구성되어 있다.
서버로부터 저장된 글에 대한 정보를 받고, 받은 정보를 바탕으로 화면과 관련된 UI작업을 한다.
게시글의 종류나 카테고리를 셀렉트 박스로 표현할 수도 있고, 작성된 글의 내용 일부를 화면에 표현하기도 한다.

또한 사용자가 게시글을 등록. 수정하는 편집화면도 있다. 편집화면에서는 사용자가 셀렉트 박스를 선택하기도 하고, 글을 입력하기도 한다. 사용자의 입력이 잘못된 경우에는 사용자에게 메시지를 전달하기도 하기도 한다.
글의 작성 및 수정이 끝나면 등록한 정보를 서버에 저장한다.

이 과정을 데이터가 흐르는 관점으로 살펴보면

    1. 목록화면과 조회화면은 서버로부터 데이터를 불러와 브라우저에게 전달한다.
    1. 브라우저에 전달된 정보를 브라우저의 UI객체에 전달한다.
    1. 편집화면은 브라우저 UI객체를 통해 사용자 입력정보를 전달받고 이를 다시 브라우저의 다른 UI객체나 브라우저 객체에 전달한다.
    1. 사용자가 작성한 정보를 브라우저 UI객체나 브라우저 객체를 이용하여 서버로 전달한다.

이 과정을 다시 상태머신 관점에서 살펴보자.
1)과 2)의 과정에서 입력값은 서버로부터 전달 받은 게시글 데이터가 된다. 두 과정의 입력값이 동일 할지라도 입력값을 받는 브라우저와 브라우저 UI 객체는 서로 다른 시점에 입력값을 전달받는다.
예를 들어 1)과정이 Ajax로 JSON 데이터를 받아와 브라우저의 객체로 저장하는 경우라면 Ajax는 비동기(Asynchronous) 호출로 데이터를 받기 까지 시간이 걸린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// XMLHttpRequest에 의해 입력된 데이터
let result;
const xhr = new XMLHttpRequest();
xhr.onload = function(e) {
/*
* Ajax를 통해 얻은 데이터를 result 변수에 저장한다.
*
* {
* list: [
* "게시글1번. 안녕하세요.",
* "게시글2번. 반갑습니다.",
* "게시글3번. RxJS에 대해 알아봐요."
* ]
* }
*/
result = JSON.parse(xhr.responseText);
};
xhr.open("GET", url);
xhr.send();

반면, 브라우저 UI객체는 이미 브라우저에 존재하는 JSON 데이터를 받기 때문에 동기(Synchronous) 호출로 바로 결과 데이터를 얻을수 있다.

1
2
3
4
5
<ul>
<li></li>
<li></li>
<li></li>
</ul>
1
2
3
4
Array.from(document.querySelectorAll("li")).forEach((v, i) => {
// Ajax의 결과인 result 변수를 이용하여 DOM에 정보를 표현한다.
v.innerText = result.list[i];
});

마찬가지로 3)과 4)의 과정도 입력값은 사용자의 입력이 되지만, 전달된 입력값을 처리하는 시점은 상황에 따라 각각 다르다.

앞에서 설명한 내용을 간단히 정리하면 다음과 같다.

과정 데이터 데이터 흐름 전달 시점 예제
1) 게시글 서버 -> 브라우저 비동기 Ajax 통신으로 JSON 데이터를 받는다.
2) 게시글 브라우저 -> 브라우저 UI 객체 동기 JSON 데이터를 UI에 반영한다.
3) 사용자가 입력한 내용 사용자 -> 브라우저 UI객체 비동기 <textarea>를 통해 사용자 입력을 받는다.
4) 사용자가 입력한 내용 브라우저 UI 객체 -> 브라우저 -> 서버 동기, 비동기 <textarea>에 있는 데이터를 JSON 객체로 저장 후, 서버로 Ajax 요청을 한다

입력 데이터의 전달 시점이 다양하다.

앞에서 살펴본 바와 같이 입력데이터가 같을지라도 실제 각 객체들 사이로 데이터가 전달되는 시점은 다르다. 어떤 상황에서는 동기(Synchronous) 방식으로 데이터를 주고 받고, 어떤 상황에서는 비동기(Asynchronous) 방식으로 데이터를 주고 받는다.
이런 구조는 비단 웹어플리케이션 뿐만이 아니다. 소프트웨어 전반적으로 이와 같은 상황이 발생한다.
이런 이유는 두 방식의 차이점을 살펴보면 보다 명확히 알수 있다.

동기(Synchronous)

동기방식은 작업이 들어온 순서에 맞게 차근차근 하나씩 진행되는 것을 의미한다. 호출하는 함수가 호출되는 함수의 작업 완료를 기다린 후 그 다음을 진행하는 방식이다. 이 방식의 장점은 순차적으로 진행되기 때문에 개발이 쉽다. 반면, 처리하는 작업이 많을 경우에는 전체 작업 속도가 느려진다. 특히, 웹브라우저와 같이 단일 UI쓰레드를 사용하는 경우에는 해당 작업이 끝날때까지 브라우저는 대기하고 있어야만 한다.

비동기(Asynchronous)

반면, 비동기 방식은 작업이 들어온 순서에 상관없이 산발적으로 진행되는 것을 의미한다. 호출하는 함수가 호출되는 함수의 작업 완료를 기다리지 않고, 그 다음을 진행하고 호출되는 함수의 작업이 완료되면 별도의 이벤트나 callback 함수를 통해 결과를 전달하는 방식이다. 이 방식의 장점은 효과적으로 작업을 진행할 수 있다는 점이다. 설사 먼저 실행된 작업의 처리시간이 오래걸리더라도 다른 작업을 진행할 수 있다. 반면, 개발은 더욱 복잡해지고 오류 확률은 높아질 우려가 있다.

동기와 비동기를 함께 사용할 수 밖에 없는가?

동기 방식은 명확한 순서를 보장하면서 코드의 흐름과 프로그램의 흐름을 동일하게 처리하는 장점이 있지만, 비동기에 비해 효과적인 작업을 할 수 없다.
반면, 비동기 방식은효과적으로 작업을 할 수 있지만 호출의 순서를 보장하기가 어렵다. 이를 보장하기 위해서는 우리는 많은 작업들을 추가적으로 해야만 한다.

그렇다면 모두 동기 방식으로 개발하면 개발 생산성도 높이고, 오류의 발생빈도도 더 낮출수 있기 때문에 다른 한편으로는 더 좋은 선택이지 않는가?

틀린말은 아니다. 하지만 결론부터 이야기하면 그렇게 할수가 없다.
기반 플래폼이 제공하는 API자체가 동기 또는 비동기 방식을 제공하고 있기 때문에 본질적으로 우리는 이 문제를 피해갈 수가 없다.
특히 단일 쓰레드 기반의 브라우저 환경에서는 비동기 방식을 사용하지 않으면 성능 문제를 해결할 수 없기 때문에 우리에게는 선택의 여지가 없다.

RxJS는 어떻게 개선하였나?

RxJS가 주목했던 부분은 바로 이 입력 데이터에 대한 구조적 문제를 개선하고자 하였다.

RxJS는 이런 구조적인 문제를 개선하기 위해 단 하나의 방식을 사용할 수 있는 구조를 제공한다. 이런 구조의 일원화는 개발을 단순화시킨다. 이런 단순화는 결국에는 오류 발생 빈도를 낮추고, 생산성 향상을 도와준다.

RxJS는 동기(Synchronous)와 비동기(Asynchronous)의 차이점을 시간이라는 개념을 도입함으로써 해결하려고했다.

대표적인 비동기(Asynchronous) 처리방식인 이벤트(Event)를 살펴보자.
Button에서 발생하는 click 이벤트를 처리하기 위해서는 다음과 같이 버튼에 이벤트 핸들러를 등록한다.

1
2
3
button.addEventListener("click", event => {
// @todo
});

한번 이벤트 핸들러가 등록되면 사용자가 버튼을 눌렀을 때마다 등록된 이벤트 핸들러가 호출된다.
이런 과정에 시간이라는 개념을 도입함으로써 우리는 다른 시각을 얻을 수 있다.

버튼을 누르는 행위가 우리가 원하는 데이터라면 시간 축을 기준으로 이 데이터는 다음과 같은 모습을 보일 것이다.

이벤트가 아닌 동기(Synchronous) 방식인 함수호출도 시간이라는 개념을 도입하면 다음과 같이 표현될 수 있다.

결국 동기와 비동기는 시간의 축으로 봤을때는 같은 형태인 것이다.
또한, 이런 형태는 시간을 인덱스로 둔 컬렉션으로 생각할 수도 있다. RxJS에서는 이를 스트림(Stream)이라 표현한다.

RxJS에서는 이런 Stream을 표현하는 Observable 클래스를 제공한다.

Observable

Observable은 시간을 인덱스로 둔 컬렉션을 추상화한 클래스이다.
이 클래스는 동기나 비동기의 동작 방식으로 전달된 데이터를 하나의 컬렉션으로 바라볼 수 있게 해준다.
이렇게 함으로써 개발자는 데이터가 어떤 형태로 전달되는지에 대해 더이상 고민할 필요가 없어진다.
단지, Observable을 통해 데이터를 전달 받기만 하면 된다.

Observable의 표준화

RxJS의 Observable은 Rx에서 만든 라이브러리이기도 하지만, ECMAScript에 표준으로 제안된 스펙이기도하다.
https://github.com/tc39/proposal-observable

이 책에서 다루는 RxJS5는 ECMAScript에 제안된 표준 스펙을 기반으로 작성된 라이브러리이다.

모든 데이터는 Observable로 만들 수 있다.

Observble은 모든 데이터를 다룬다.

  • 키보드를 눌러서 입력된 데이터
  • 마우스를 이동하거나 클릭해서 입력된 데이터
  • Ajax/fetch 요청을 통해 얻은 데이터
  • Web socket을 통해 전달된 데이터
  • Message를 통해 전달된 데이터

Comment and share

  • page 1 of 1
Author's picture

sculove

아내와 아들 그리고 딸밖에 모르는 남편


FrontEnd Developer


대한민국/서울