[RxJS] rxjs-fruits 사이트 문제 풀어보기

RxJS 사이트 문제를 풀어보았습니다.

FlyingSquirrel
15 min readOct 1, 2020
와아 — 다풀었어요. 짝짝짝짝 👏

RxJS를 몰랐습니다 🤭 😣 무엇인지도, 왜 쓰는지도 잘 모르지만, 입사하여 회사의 몇 개 repository를 살펴보니 RxJS를 쓰더라고요!? 동공지진 두둥 👀

그래서 RxJS의 사용법을 익혀볼 겸 rxjs-fruits 사이트 문제를 풀어보았습니다!

RxJS는 왜 쓰는건가요?

제가 이해한 언어로 쓴 것이라 좀 더 정확한 개념을 알고 싶다면 여러 공식문서들을 참고하시면 좋을 것 같습니다 🙂

RxJS는 Reactive Programming(리액티브 프로그래밍)이라는 프로그래밍 기법으로 JavaScript 코드를 짜기 위해 사용하는 라이브러리 입니다.

RxJSReactive Programming(리액티브 프로그래밍)이라는 프로그래밍 기법으로 JavaScript 코드를 짜기 위해 사용하는 라이브러리 입니다. 리액티브 프로그래밍은 자바스크립트에만 해당하는 것은 아니고, 여러 언어로 프로그래밍을 할 때 쓰는 패러다임(방법/도구) 중 하나입니다. 바로 이 리액티브 프로그래밍을 하기 위해서 사용하는 라이브러리가 마이크로소프트에서 만든 ReactiveX(Rx)라는 라이브러리입니다. JavsScript에서는 RxJS라는 이름의 라이브러리로, JavaRxJava, PythonRxPY, SwiftRxSwift로 리액티브 프로그래밍을 할 수 있습니다. (👉 ReactiveX languages)

그렇다면 Reactive Programming(리액티브 프로그래밍)은 무엇이냐?

Data Stream(데이터 흐름)과 데이터가 변경된 것을 전파하기 위한 선언형 프로그래밍입니다. 데이터로 볼 것들을 먼저 정의해주고, 그 데이터들에게 변경이 일어나면 어떤 처리를 해줄 것인지 프로그래밍할 수 있습니다.

느낌적인 느낌으로, 마치 React에서 props가 변경이 되면 그 props와 관련된 컴포넌트들이 re-render를 하는 과정이 떠올랐습니다. 어떤 데이터(props)가 변경되고 이게 전파되는 과정을 React에서 처리해주고, 변경된 사항이 반영(re-render)되는 흐름이요.

처음 배울 땐 무조건 재미있게 배워야하니까, rxjs-fruits 사이트에서 게임으로 배워봅시다

이 게임은 주어진 레시피에 맞춰서 코드를 잘 짜서 과일주스를 만드는 게임입니다.

레시피는 화면 왼쪽에 있고, 코드를 작성하고 Play 버튼을 누르면 오른편에서 재미난 애니메이션과 함께 컨베이너 벨트가 움직이고 하늘에서 과일이 떨어집니다.

rxjs에서 사용되는 주요 API를 익히는 데 도움되는 것 같습니다 :)

from
distict
take
filter
map
distinctUntilChanged
skip
merge
takeLast
skipLast
zip
concatMap
repeat

문제1. Data stream에 변경이 생기면 subscribe가 호출됨

/* 내가 푼 방식 */const conveyorBelt = EMPTY;conveyorBelt.subscribe();

리액티브 프로그래밍에서는 Observer 패턴, Iterator 패턴이 사용됩니다. 이 말이 어렵더라도, 처음에 적었던 것처럼 데이터가 변경되면 변경된 것을 알아차려야하니까 이를 위해 subscribe() 함수가 필요합니다.

16개의 문제 중 개인적으로 1번 문제가 제일 오래 걸렸습니다.. 뭘 적으라는거지..? 싶어서요. conveyorBelt는 이미 Observable 객체로 타입이 정의되어 있기 때문에 바로 subscribe()를 해주면 문제1번은 해결됩니다!

문제2. 컨베이너 벨트에 과일을 하나씩 놓기

/* 내가 푼 방식 */
const fruits = from([
"apple",
"banana",
"cherry"]);
fruits.subscribe(fruit => {
// toConveyorBelt 함수는 이 게임에서 자체적으로 만든 함수입니다.
toConveyorBelt(fruit)
});
출처: rxjs
import { from } from 'rxjs';const array = [10, 20, 30];
const result = from(array);
result.subscribe(x => console.log(x));
// Logs:
// 10
// 20
// 30

from()이라는 함수는 인자로 Array, Array-like object, Promise, iterable object, Observable-like-object를 받아서 Observable 객체를 리턴합니다. rxjs-fruits 게임에서는 계속 Array를 인자로 넘겨줍니다.

from([10,20,30])을 실행하면 Observable 객체가 생성되고, 이 객체를 subscribe하는 곳에 인자의 요소(10, 20, 30)을 하나씩 전달합니다.

게임을 예로 들자면, fruits.subscribe(fruit => {})에서 fruit는 처음에 “apple”가 들어오고, 그 다음에 “banana”, 마지막으로 “cherry”가 들어옵니다. 값을 하나씩 받게 되니 이 과일들을 컨베이너 벨트에 올려주면 하나씩 올려지고 과일주스를 만들게 됩니다!

문제3. 중복된 Data 제거해주는 distinct

/* 내가 푼 방식 */
const fruits = from([
"apple",
"apple",
"banana",
"apple"]);
fruits.pipe(
distinct(),
).subscribe(fruit => {
toConveyorBelt(fruit)
});

Data stream에서 중복되는 값을 제거해주는 method가 distinct()함수입니다.

문제4. Data stream 중 앞에서 부터 몇 개만 쓰게 해주는 take

/* 내가 푼 방식 */
const fruits = from([
"banana",
"banana",
"banana",
"banana"]);
fruits.pipe(
take(2),
).subscribe(fruit => toConveyorBelt(fruit));

take는 영어로 가지다(취하다)라는 뜻이 있는데, 여기서는 그런 의미로 사용됩니다. Data stream으로 전달되는 갚들 중에서 몇 개만 취해서 활용할 수가 있습니다.

take(2)는 앞에서부터 2개를 취할 것이다라는 뜻입니다.

문제5. 필요한 것만 filter

/* 내가 푼 방식 */
const fruits = from([
"apple",
"apple",
"old-apple",
"apple",
"old-apple",
"banana",
"old-banana",
"old-banana",
"banana",
"banana"]);
fruits.pipe(
filter(fruit => !fruit.includes("old")),
).subscribe(fruit => toConveyorBelt(fruit));

Array.prototype.filter와 비슷한 역할을 합니다. filter(함수)이런 식으로 적어주고, 함수의 조건을 만족하는 값들만 subscribe에 전달합니다.

문제6. 데이터를 순회하며 변경하는 map

/* 내가 푼 방식 */
const fruits = from([
"dirty-apple",
"apple",
"dirty-banana",
"banana"]);
fruits.pipe(
map(x => x.replace('dirty-','')),
).subscribe(fruit => toConveyorBelt(fruit));
출처: rxjs

문서에서 소개할 때에도 map은 Array.prototype.map과 유사하다고 설명합니다. 전달된 data를 하나씩 돌며 return 되는 값을 subscribe에 전달합니다.

문제7. pipe에 여러 rxjs API를 적용하기(filter, map, take 응용)

/* 내가 푼 방식 */
const fruits = from([
"old-banana",
"apple",
"dirty-banana",
"apple"]);
fruits.pipe(
filter((name, index) => index === 1 || index === 2),
map(fruit => fruit.replace('dirty-','')),
take(2),

).subscribe(fruit => toConveyorBelt(fruit));

Data Stream으로 전달되는 값들을 변경도 하고, filter도 하고 이것저것 하고 싶은 것이 많다면, pipe에 순차적으로 실행하는 함수를 넘겨줍니다.

예를 들어 pipe에 아래 함수를 넣어주면, 차례대로 funcA → funcB → funcC가 함수가 실행됩니다.

fruits.pipe(
funcA(),
funcB(),
funcC(),
)

이 게임에서는 fruits.pipe(어쩌구 저쩌구)로 실행하게 되면 최종적으로 ["apple","banana"]라는 data가 subscribe 함수에 전달됩니다.

문제8. 이전 값과 비교해서 중복인 경우에 쓰는 distinctUntilChanged

/* 내가 푼 방식 */
const fruits = from([
"banana",
"apple",
"apple",
"banana",
"banana"]);
fruits.pipe(
distinctUntilChanged(),
).subscribe(fruit => toConveyorBelt(fruit));

distict와 다른 점은 바로 앞의 값과 비교해서 중복인 경우에만 중복값을 제거해준다는 것입니다.

// distict
of(1, 1, 2, 2, 2, 1, 2, 3, 4, 3, 2, 1).pipe(
distinct(),
).subscribe(x => console.log(x)); // 1, 2, 3, 4
// distictUntilChanged
import { of } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
of(1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4).pipe(
distinctUntilChanged(),
).subscribe(x => console.log(x)); // 1, 2, 1, 2, 3, 4

문제9. 넘어가는 skip

const fruits = from([
"apple",
"apple",
"banana",
"apple"]);
fruits.pipe(
skip(2),
).subscribe(fruit => toConveyorBelt(fruit));
출처: rxjs

Data stream으로 [사과,사과,바나나,사과] 값이 들어올 때 파이프에서 skip(2)를 하면 앞에부터 2개(사과,사과)를 별다른 데이터 처리를 하지 않고, subscribe에는 3번째부터(바나나, 사과)를 넘겨주게 됩니다.

문제10. skip, take, map을 같이 쓰는 문제

const fruits = from([
"dirty-apple",
"apple",
"dirty-banana",
"dirty-banana",
"apple"]);
fruits.pipe(
skip(3),
take(1),
map(x => x.replace('dirty-','')),

).subscribe(fruit => toConveyorBelt(fruit));

문제11. 여러 Observable 객체를 합치는 merge

const apples = from([
"apple",
"apple",
"old-apple",
"apple",
"old-apple"]);
const bananas = from([
"banana",
"old-banana",
"old-banana",
"banana",
"banana"]);
const mergedData = merge(apples, bananas);mergedData.pipe(
filter(x => x === 'apple' || x === 'banana'),
).subscribe(fruit => toConveyorBelt(fruit));

apples와 bananas라는 2개의 Observable 객체를 합쳐서 처리하고 싶을 때는, merge를 사용하면 됩니다.

그러면 여기서 mergedData는 아래와 같습니다.

[
"apple",
"banana",
"apple",
"old-banana",
"old-apple",
"old-banana",
"apple",
"banana",
"old-apple",
"banana",
]

문제12. 뒤에서 몇 개의 Data만 취하고 싶을 땐 takeLast

const fruits = from([
"apple",
"apple",
"banana",
"apple",
"banana"]);
fruits.pipe(
takeLast(3), // "banana", "apple", "banana"
).subscribe(fruit => toConveyorBelt(fruit));

문제13. 뒤에 데이터 몇 개는 빼고 처리하고 싶을 때 skipLast

const fruits = from([
"apple",
"apple",
"banana",
"apple",
"banana"]);
fruits.pipe(
skipLast(2), // "apple", "apple", "banana"
).subscribe(fruit => toConveyorBelt(fruit));

문제14. skipLast, skip, merge를 활용한 문제

const apples = from([
"apple",
"dirty-apple",
"apple",
"old-apple",
"apple"]);
const bananas = from([
"old-banana",
"old-banana",
"dirty-banana",
"dirty-banana",
"dirty-banana"]);
const freshApples = apples.pipe(
skipLast(2),
map(x => x.replace('dirty-','')),
);
const freshBananas = bananas.pipe(
skip(2),
map(x => x.replace('dirty-','')),
);
const mergedFruits = merge(freshApples, freshBananas);
mergedFruits.subscribe(fruit => toConveyorBelt(fruit));

문제15. 여러 Observable을 하나의 Observable로 합치는 zip, Array를 하나씩 return해주는 concatMap

const apples = from(["apple", "apple"]);const bananas = from(["banana", "banana"]);const zippedFruit = zip(apples, bananas);zippedFruit.pipe(
concatMap(x => x),
).subscribe(fruit => toConveyorBelt(fruit));

zip을 이용하면 여러 Observable 객체를 하나의 Observable객체로 만들 수 있습니다. 이 경우에 zippedFruit은 아래와 같은 모양이 됩니다.

[
["apple", "banana"],
["apple", "banana"],
]

이 Observable객체가 pipe를 통해 concatMap 메소드를 만나면, concatMap은 요소를 하나씩(apple,banana) 리턴하며 subscribe에 전달합니다.

문제16. 몇 배로 불려주는 repeat

const fruits = from(["apple"]);fruits.pipe(
repeat(3),
).subscribe(fruit => toConveyorBelt(fruit));

python에서 abc * 3을 하면 abcabcabc가 나오는 것 같이 편리해보이는 메소드였습니다. 과연 실무에서 쓸 일이 많이 있을지는 잘 모르겠지만..?

사과 하나를 넣었는데, repeat(3)을 만났더니 사과가 3개로 불어나는 마법!

--

--