요즘은 회사일이 바뻐서 점심시간을 쪼개고 쪼개서 스터디 아닌 스터디를 하고 있다.
1시간이 채 안되는 시간동안 이야기를 했지만, 그래도 꽤 재미있는 이야기들이 오고가서 잠담이라고 치부하기에는 아까운 내용들이다.

이 글은 Object 책의 두번째 모임의 후기로 책 3~4장에 대한 내용을 토대로 이야기했던 내용과 내가 생각했던 내용들을 정리했다.

역할, 책임, 협력

이 책이 새롭지 않으면서도 새로운 이유는 그동안 익히 알고 있었던 똑같은 내용을 다른 시각으로 바라보게 해준다는 것이다.
꼰대(?) FE 개발자들이 읽기에 충분히 흥미로운 내용이었다.
이 책에서는 주구장창 역할, 책임, 협력에 대해 이야기하고 있다.

Continue reading

개인적으로 개발을 하면서 꼭 필요한 것 중의 하나가 개발에 대한 경험이고, 또 다른 하나가 그 경험을 통한 사고의 정리와 확장에 있다. 개발경력은 단지 숫자에 불과할 뿐이다. 그 시간동안 얼마나 다양한 경험과 깊은 고민을 했는지가 현재 자신의 개발 역량을 만들어 간다.

하지만, 어디 개발 뿐이겠는가? ㅋㅋ

물론, 천재적인(?) 감각과 통찰력으로 책 만보고도 잘하는 사람도 드물게 있긴 하다. 하지만 나 같은 범인들은 당하지 않고는 느끼지 못하고, 고민하지 않으면 통찰력을 얻을 수 없다.

그래서 마침 좋은 책도 나오고 해서 FE개발 좀 했다는 노친네(?) 분들과 오브젝트 책으로 생각을 정리할 목적으로 스터디를 시작했다.

스터디라고해서 뭐 거창한 것은 아니고 그냥 책을 읽어와서 이런 저런 잡담을 주기적으로 하는 시간을 갖기로 했다.

오늘은 그 첫번째로 이 책의 1~2장에 대한 내용을 토대로 이야기했던 내용. 그리고 내 생각들의 정리를 위한 기록들이다.

Continue reading

팀회식때 팀원이 나보고 ‘90년대생이 온다’ 라는 책을 봐보란다.
회식때 흘러들은 이야기들은 가급적이면 새겨(?)듣는 편이기에 바로 책을 사서 집에서 곰곰히 읽어봤다.
사실 별 공감이 되지는 않았다.
내가 이 책을 보면서 느낌 감정은 2가지 였다.
첫째, 우선 내가 공감하지 못하는 것을 안 순간부터 아 나 진짜 꼰대였구나.라는 생각이 들었다.
둘째, 아 이렇게 행동하고 생각하는게 90년대생들에게는 정상이구나 라는 생각의 이해 정도.

Continue reading

개발자를 어떻게 학습시킬것 인가?에 대한 자기성찰을 좀 해보려고 한다.

이 성찰의 결정적인 계기는
애자일 컨설팅 대표로 있는 김창준님이 내놓은 책을 읽으면서 느꼈던 내 수치심으로 부터 시작되었다.

연말이라 좀 시간도 있고 해서 책을 하나 사서 봤다. 김창준님이 내놓은 신간 책이다.
책 내용은 사실 지금까지 이 분이 블로그를 통해 이야기한 내용과 별반 다르지 않다.
좀 더 구체적으로 친절하게 정리해 놓은 책이다. 결론은 좋은 책이다.

Continue reading

이번으로 2번째 책을 탈고 했다.

첫번째 책은 정말로 얼떨결에 썼던 것같다.

첫번째 책이 ‘와 내가 저자가 되는거야?’ 라는 호기심과 영웅 심리(?)로 했었던 것이라면 이번 건은 진짜 써보고 싶어서 쓴 책이다. 그 만큼 시간도 많이 들어간 것 같고 열정도 많이 쏟았던 것 같다.
다만 아쉬움이 있다면. 생활고(?)에 힘들어서 책을 쓰는 기간이 너무 늘어났다는 것이다.
원래는 7개월 정도에 마무리 하려고 했지만 질질끌다보니 1년이 훌쩍 지나가 버렸다.
아마 처음 계획했던 것을 하려고했으면 아마 지금도 못 끝냈을 지도 모른다.

책을 쓰는 것은 많이 시간이 들고 집필의 압박감에 꽤나 고단하고 피곤한 작업이다.
돈을 벌수 있는 수단도 아니다.

물론, 초급자 대상의 책을 쭉쭉~ 뽑아내면 금전적으로 이득을 볼 수 있겠지만 나 같이 전문적인 기술 서적으로 돈을 번다는 것은 쉬운 일이 아니다.

특히나 RxJS와 같이 이해하고 어렵고 특수한 분야의 책으로 돈을 벌기는 더더구나 힘들다.

Continue reading

아~ 드디어 원고를 탈고했다.

인쇄를 했더니 A4 한박스가 훌러덩 다 나갔다.
많은 양을 써놓은게 뿌듯하기도 했지만 한 편으로는 무슨 부귀영화를 누리려고 이렇게까지 살았나? 라는 생각도 들었다.

사실 책은 다 썼지만 아직 제목을 정하지는 못했다. 예제 위주로 RxJS를 익힐 수 있도록 작성한 책이긴 한데… 뭔가 임팩트 있는 제목을 아직까지 못찾았다.
이러다보면 결국 출판사에서 제안하는 이름을 선택 택하겠지 ^^;

책을 읽는 사람이 읽기만 해도 이해를 쉽게 할 수 있도록 가급적이면 상세히 쓰려고 노력했지만 잘 녹아들어갔는지는 원고 교정이 끝난 이후에 피드백을 좀 받아봐야겠다.

아~ 사실 더 다루고 싶은 내용도 정말 많았다. 하지만 욕심을 많이 버렸다.
RxJS 6.0이 나와서 전체적으로 소스도 바꾸고 하다보니 욕심부리다 보면 올해도 책을 못낼 것 같은 생각이 들어 마무리를 지었다.

담에 기회가 되면 더 많은 내용을 다루는 것을 숙제로 남기고… 그동안 하고싶었던 일들을 좀 여유롭게 해봐야겠다.

마지막으로 지금까지 묵묵히 바쁜척하는 남편을 도와준 우리 와이프와 아이들에게 마지막으로 감사를 보낸다.
이제 넷플릭스로 미드도 보고 게임기로 게임좀 원없이 해보자 ㅋㅋ

Comment and share

오늘 소개할 부분은 작성 중인 책의 “부록” 중 일부이다.
이 장에서는 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

Author's picture

sculove

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


FrontEnd Developer


대한민국/서울