1부 Chapter 03. RxJS가 해결하려고 했던 문제 3 - 로직오류
1.1. 웹 어플리케이션의 로직
웹애플리케이션은 전달받은 입력값을 로직을 통해 새로운 결과를 반환하거나 표현
로직은 산술적인 로직이나 비즈니스적인 로직이 될 있고 또는 if문과 같이 간단한 프로그램의 흐름을 담당하는 부분일 수도 있음
사용자 정보를 표현하는 ui 예(스타워즈 등장인물을 조회 하는 예)
- 성별이 ‘남’과 ‘여’인 사용자만 추출(스타워즈는 로봇과 같이 성별이 없는 경우도 있음)
- 사용자의 이름, 키, 몸무게를 표시
- 사용자의 성별에 맞게 아이콘을 화면에 표시
- 사용자의 표준 체중을 계산하여 표시
BROCA 방식
- 남자 표준 체중 = (키 - 100) x 0.9
- 여자 표준 체중 = (키 - 105) x 0.9
BMI 방식
- 남자 표준 체중 = 키/100 * 키/100 x 22
- 여자 표준 체중 = 키/100 * 키/100 x 21
fetch("https://swapi.co/api/people/?format=json").then(res => {
if (res.status === 200) {
return res.json();
} else {
throw new Error("Error");
}
}).then(jsonData => {
document.getElementById("users").innerHTML = process(jsonData);
}).catch(e => {
console.error(e);
});
// 데이터를 처리하는 함수
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 - 105 * 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("");
}
fetch를 사용해서 받아온 json의 데이터는 아래와 같음
이 데이터를 재가공해서 만든 페이지는 아래와 같음
원래 조회 했던 데이터를 그대로 출력 하는 것이 아니라 데이터로부터 생산된 새로운 정보들을 사용하고 있음
1.2. 로직의 복잡성 그리고 오류
반복문과 분기문 그리고 변수는 우리의 코드를 복잡하게 만들고 가독성을 떨어뜨림
결국 오류 발생의 빈도를 높이게 됨
1.2.1. 반복문과 분기문
로직의 복잡성을 줄이는 가장 간단한 방법은 기능을 쪼개는 것
하지만 단순히 구역별로 쪼개게 되면 기능의 의미를 명확하게 드러내지 못하고 코드의 재사용성을 떨어뜨림
기능을 쪼개는 일이 쉽지 않은 이유는 코드 대다수가 로직과 반복문, 분기문의 결합으로 구성되어 있기 때문
코드에서 반복문과 분기문을 모두 제거한다는 것은 사실상 불가능
하지만 기능 단위로 분리할 수 있다면 기능을 추상화할 수 있고 로직의 복잡성을 줄일 수 있음
1.2.2. 변수는 오류의 시작
변수를 사용한다는 의미는 오류의 확률을 높인다는 것
변수는 변경될 수 있는 값이기 때문에 유용하지만 의도치 않게 이 값이 바뀔 경우에는 오류가 발생하게 됨
브라우저 환경의 자바스크립트는 싱글 스레드 구조이기 때문에 멀티 스레드의 사용으로 인한 동시성 문제는 자주 발생하지 않음
하지만 DOM에 등록된 이벤트 핸들러에 의해 변수의 값이 변경되거나 비동기 행위로 인해 외부로 노출된 변수의 값들이 변경될 수 있음
따라서 변수의 노출 범위를 제한하거나 제거함으로서 변수의 값이 외부에 의해 변경되지 않고 개발자의 의도에 따라 정확하게 변경될 수 있도록 보장해야 함
WebWorker와 같은 기술 스펙을 사용하면 멀티 스레드 기술을 사용 할 수 있지만 기본적으로 하나의 메인 스레드에서 모든 작업이 이루어짐
1.3. 자바스크립트의 솔루션
자바스크립트 함수를 이용하면 실제 로직과 상관없는 반복문, 분기문을 분리 할 수 있고 변수도 제거해 나갈 수 있음
자바스크립트 함수는 일급 객체
일급 객체(first-class object)는 다음과 같은 특성을 가지고 있음
- 변수 혹은 데이터 구조에 저장 가능
var savsdFunction = function(){}
- 파라미터로 전달 가능
function foo(f, value){};
foo(function(){
console.log("함수를 파라미터로 전달 할 수 있음")
},"값")
- 반환값으로 사용 가능
function foo(){
return function(){
console.log("함수를 반환할 수 있음")
}
}
1.3.1. 로직의 분리
앞에서 만든 예제에서 process 함수를 기능 단위의 로직과 반복문, 분기문으로 분리 진행
process 함수는 다음과 같은 구조로 되어 있음
성별에 따라 비만도를 구하는 로직과 사용자별 html을 만드는 로직 이 부분을 별도의 함수로 작성
표준 체중과 비만도를 계산하는 함수는 height, mass, gender을 입력값으로 받아 broca와 bmi 방식의 비만도화 표준 체중을 반환
// 표준 체중과 비만도를 계산하는 함수
function logic(height, mass, gender) {
let broca = ((height - ((gender === "male" ? 100 : 105)) * 0.9)).toFixed(2);
let bmi = (height / 100 * height / 100 * (gender === "male" ? 22 : 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을 반환
// 사용자 정보를 표현하기 위해 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>`;
}
login, makeHtml 함수를 사용하여 다음과 같이 process 함수를 작성
// 데이터를 처리하는 함수
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("");
}
login, makeHtml 함수를 만듦으로서 핵심 로직을 작성하는데 집중 할 수 있음
login, makeHtml 함수도 재사용 가능한 단위 함수로 작성됨
1.3.2. 반복문, 분기문, 그리고 함수와의 이별
코드가 크면 클수록 process 함수에 존재하는 반복문과 조건문은 우리 코드의 가독성을 떨어뜨릴 것
더블어 html과 result 같은 변수가 여전히 존재하기 때문에 오류의 가능성에 노출되어 있음
ES5에서 제공하는 Array의 filter, map, reduce와 같은 고차함수를 이용해서 process 함수를 개선
고차 함수(higher-order function)
- 다른 함수를 인자로 받거나 그 결과로 함수를 반환하는 함수
- 고차 함수는 변경되는 주요 부분을 함수로 제공함으로서 동일한 패턴 내에 존재하는 문제를 손쉽게 해결할 수 있는 고급 프로그래밍 기법
- 고차 함수를 이용하면 함수의 합성, 변형과 같은 작업을 쉽게 가능. 더블어 커링(currying), 메모이제이션(memoization)과 같은 기법도 사용 가능
const twice = (f, v) => f(f(v));
const fn = v => v+3;
console.log(twice(fn,7)); // 13
다음은 고차 함수를 이용하여 process 함수를 개선한 예
// 데이터를 처리하는 함수
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를 이용
각 고차 함수에 전달되는 함수는 외부 변수에 영향을 미치지도 않고 영향을 받지도 않는 함수
전달된 함수는 같은 입력이 주어지면 항상 같은 출력을 반환
이런 함수를 함수형 프로그래밍에서는 “순수 함수” 라고 함
개선된 process에서는 반복문, 분기문, 변수가 존재하지 않음
핵심로직은 분리되었고 코드의 흐름은 단일화 됨
변수가 없으므로 오류 발생 가능성도 줄었음
1.4. RxJS는 어떻게 개선하였나?
1.4.1. RxJS가 제공하는 오퍼레이터
RxJS 또한 ES5의 Arrar의 고차 함수와 같은 오퍼레이터를 제공함으로서 로직에 존재하는 분기문, 반복문, 변수를 제거하려 함
다음은 공식 홈페이지에서 제공하는 오퍼레이터 목록
카테고리 | 오퍼레이터 |
---|---|
생성 오퍼레이터 | ajax, bindCallback, bindNodeCallback, create, defer, empty, from, fromEvent, fromEventPattern, fromPromise, generate, interval, never, of, repeat, repeatWhen, range, throw, timer |
변환 오퍼레이터 | 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 |
추출 오퍼레이터 | debounce, debounceTime, distinct, distinctKey, distinctUntilChanged, distinctUntilKeyChanged, elementAt, filter, first, ignoreElements, audit, auditTime, last, sample, sampleTime, single, skip, skipLast, skipUntil, skipWhile, take, takeLast, takeUntil, takeWhile, throttle, throttleTime |
결합 오퍼레이터 | combineAll, combineLatest, concat, concatAll, exhaust, forkJoin, merge, mergeAll, race, startWith, switch, withLatestFrom, zip, zipAll |
멀티캐스팅 오퍼레이터 | cache, multicast, publish, publishBehavior, publishLast, publishReplay, share |
에러 처리 오퍼레이터 | catch, retry, retryWhen |
유틸리티 오퍼레이터 | do, delay, delayWhen, dematerialize, finally, let, materialize, observeOn, subscribeOn, timeInterval, timestamp, timeout, timeoutWith, toArray, toPromise |
조건, 참거짓 오퍼레이터 | defaultIfEmpty, every, find, findIndex, isEmpty |
수학, 누적 오퍼레이터 | count, max, min, reduce |
RxJS에서 제공하는 오퍼레이터를 이용하면
- Observable을 생성, 전달된 데이터를 변환, 필요한 데이터만 추출
- 여러 개의 Observable을 합성
- 하나의 Observable을 다른 여러 개의 Observable로 분할
1.4.2. 불변객체 Observable
RxJS의 오퍼레이터(operator)는 항상 새로운 Observable을 불변객체로 반환
불변객체는 생성 후 그 상태를 바꿀 수 없음
외부에서 값을 변경할 수 없기 때문에 불변 객체를 사용하는 것 만으로도 프로그램의 복잡도가 줄어듬
Array의 리턴 객체는 새로운 레퍼런스 객체지만 Observable과 달리 객체 자체가 불변 객체는 아님
var arr = [1,2,3];
var mappedArr = arr.map(v=>v);
console.log(arr===mappedArr); // false
Array와 다른 점이 있다면 Array는 새로운 객체 생성 작업만을 함
Observable은 새로운 Observable을 만들고 그 Observable이 오퍼레이터를 호출한 원래의 Observable을 내부적으로 구독
즉 링크드 리스트 형태로 기존 Observable 객체와 새롭게 만든 Observable 객체를 오퍼레이터로 연결
map 오퍼레이터 구현의 예
map = function(transformationFn) {
const source = this;
const result = new rxjs.Observable(observer=>{
// 새로운 Observable은 현재의 Observable을 구독 한다.
source.subscribe(
// 현재의 Observable에서 전달된 데이터를 변경하여 전달
function (x) { observer.next(transformationFn(x)) },
function (error) { observer.error(err) },
function () { observer.complete() }
);
});
return result;
}
source 부터 전달된 데이터, 에러, 종료 여부가 Observable의 오퍼레이터들을 통해 전달되거나 변형되어 구독한 Observer에게 전달
앞에서 작성한 표준 체중과 비만도 조회 예를 Observable로 변경
ajax$
.pipe(
switchMap(data => of(...data.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;
}, []),
map(htmlArr => htmlArr.join(""))
)
.subscribe(v => {
document.getElementById("users").innerHTML = v;
});
ajax$Observable을 통해 전달 받은 데이터가 switchMap, reduce, map을 거쳐 observer에게 전달되는 구조로 바뀜
1.5. 정리
웹 애플리케이션의 로직은 반복문, 분기문, 변수에 의해 복잡도가 증가
복잡도 증가는 코드의 가독성을 떨어뜨리고 오류의 가능성을 높임
ES5의 Array의 고차 함수를 이용하면 반복문, 분기문, 변수를 로직으로부터 분리하고 제거 가능
RxJS는 고차 함수과 같은 오퍼레이터를 제공
오퍼레이터는 불변한 Observable를 항상 생성함으로서 외부의 영향을 받지 않음