[RxJS] rxjs-fruits 사이트 문제 풀어보기
RxJS 사이트 문제를 풀어보았습니다.
전 RxJS
를 몰랐습니다 🤭 😣 무엇인지도, 왜 쓰는지도 잘 모르지만, 입사하여 회사의 몇 개 repository를 살펴보니 RxJS
를 쓰더라고요!? 동공지진 두둥 👀
그래서 RxJS
의 사용법을 익혀볼 겸 rxjs-fruits 사이트 문제를 풀어보았습니다!
RxJS는 왜 쓰는건가요?
제가 이해한 언어로 쓴 것이라 좀 더 정확한 개념을 알고 싶다면 여러 공식문서들을 참고하시면 좋을 것 같습니다 🙂
RxJS는 Reactive Programming(리액티브 프로그래밍)이라는 프로그래밍 기법으로 JavaScript 코드를 짜기 위해 사용하는 라이브러리 입니다.
RxJS
는 Reactive Programming(리액티브 프로그래밍)
이라는 프로그래밍 기법으로 JavaScript 코드를 짜기 위해 사용하는 라이브러리 입니다. 리액티브 프로그래밍은 자바스크립트에만 해당하는 것은 아니고, 여러 언어로 프로그래밍을 할 때 쓰는 패러다임(방법/도구) 중 하나입니다. 바로 이 리액티브 프로그래밍을 하기 위해서 사용하는 라이브러리가 마이크로소프트에서 만든 ReactiveX(Rx)
라는 라이브러리입니다. JavsScript
에서는 RxJS
라는 이름의 라이브러리로, Java
는 RxJava
, Python
은 RxPY
, Swift
는 RxSwift
로 리액티브 프로그래밍을 할 수 있습니다. (👉 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)
});
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));
문서에서 소개할 때에도 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));
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개로 불어나는 마법!