scan()은 무한 데이터에 대한 reduce이다
Source: Dev.to
RxJS를 어느 정도 사용해 본 적이 있다면 scan()을 한 번쯤은 만나봤을 것입니다.
아마 다음과 같은 코드를 본 적이 있을 겁니다:
interval(1000)
.pipe(
scan(
(count) => count + 1,
0
)
)
Enter fullscreen mode
Exit fullscreen mode
그리고 아마 이렇게 생각했을지도 모릅니다:
음...
뭔가를 누적하네.
쓸모 있어 보이네.
Enter fullscreen mode
Exit fullscreen mode
그 뒤로는 넘어갔겠죠.
저도 그랬습니다.
오랫동안 scan()을 또 다른 RxJS 연산자 중 하나로만 여기고 있었습니다.
외워야 할 또 하나의 연산자.
그런데 어느 순간 깨달았습니다.
scan()은 특별한 RxJS 연산자가 아니라,
끝이 없는 데이터에 맞게 변형된 reduce()일 뿐이라는 것을.
이것을 이해하면 반응형 프로그래밍의 큰 부분이 훨씬 쉬워집니다.
The Limitation Of reduce()
익숙한 예제로 시작해 보겠습니다.
const total =
[1, 2, 3, 4]
.reduce(
(sum, n) => sum + n,
0
)
console.log(total)
Enter fullscreen mode
Exit fullscreen mode
출력:
10
Enter fullscreen mode
Exit fullscreen mode
간단합니다.
리듀서는 다음을 차례로 처리합니다:
1
2
3
4
Enter fullscreen mode
Exit fullscreen mode
그리고 최종적으로 다음을 만들어 냅니다:
10
Enter fullscreen mode
Exit fullscreen mode
하지만 중요한 점을 눈여겨 보세요.
reduce가 동작하는 이유는 컬렉션이 끝나기 때문입니다.
Enter fullscreen mode
Exit fullscreen mode
어느 순간:
Array Finished
↓
Emit Final Result
Enter fullscreen mode
Exit fullscreen mode
이것이 근본적인 전제입니다.
reduce는 종료 시점을 필요로 합니다.
What Happens When Data Never Ends?
다음과 같은 경우를 생각해 보세요:
interval(1000)
Enter fullscreen mode
Exit fullscreen mode
출력:
0
1
2
3
4
5
...
Enter fullscreen mode
Exit fullscreen mode
언제 끝날까요?
아마 절대 끝나지 않을 수도 있습니다.
그래서 만약
reduce()
을 시도한다면 어떻게 될까요?
답은:
아무 일도 일어나지 않는다.
Enter fullscreen mode
Exit fullscreen mode
왜냐하면 reduce는 완료를 기다리고 있기 때문이며, 완료가 절대 오지 않으니까요.
Enter scan()
RxJS는 바로 이 문제를 해결하기 위해
scan()
을 도입했습니다.
Enter fullscreen mode
Exit fullscreen mode
예시:
interval(1000)
.pipe(
scan(
(sum, value) =>
sum + value,
0
)
)
Enter fullscreen mode
Exit fullscreen mode
출력:
0
1
3
6
10
15
21
...
Enter fullscreen mode
Exit fullscreen mode
흥미롭죠.
scan()은 끝까지 기다리는 대신 모든 중간 상태를 방출합니다.
Enter fullscreen mode
Exit fullscreen mode
Visualizing The Difference
Reduce:
1
2
3
4
↓
10
Enter fullscreen mode
Exit fullscreen mode
Scan:
1
↓
1
2
↓
3
3
↓
6
4
↓
10
Enter fullscreen mode
Exit fullscreen mode
모든 중간 상태가 눈에 보이게 됩니다.
The Formula Is Identical
Reduce:
(accumulator, value) =>
nextAccumulator
Enter fullscreen mode
Exit fullscreen mode
Scan:
(accumulator, value) =>
nextAccumulator
Enter fullscreen mode
Exit fullscreen mode
함수 시그니처는 동일합니다.
개념도 동일하지만 타이밍만 다릅니다.
그래서 저는 scan을 “무한 데이터용 reduce”라고 설명하는 것을 좋아합니다.
The Hidden Relationship To Redux
이제 흥미로운 부분이 나옵니다.
다음과 같은 코드를 보세요:
const reducer = (
state,
action
) => {
switch (action.type) {
case "INCREMENT":
return state + 1
default:
return state
}
}
Enter fullscreen mode
Exit fullscreen mode
익숙하지 않나요?
바로 Redux입니다.
이제 시간이 흐르면서 액션이 들어온다고 상상해 보세요.
INCREMENT
INCREMENT
INCREMENT
Enter fullscreen mode
Exit fullscreen mode
무슨 일이 일어날까요?
0
↓
1
↓
2
↓
3
Enter fullscreen mode
Exit fullscreen mode
그것이 바로 scan입니다.
말 그대로 Redux는 액션 스트림에 대한 scan이라고 볼 수 있습니다.
State Management Is Just scan()
간단한 상태 저장소를 만들어 봅시다.
const actions$ = new Subject()
Enter fullscreen mode
Exit fullscreen mode
리듀서:
const reducer = (
state,
action
) => {
switch (action.type) {
case "ADD":
return state + action.value
default:
return state
}
}
Enter fullscreen mode
Exit fullscreen mode
스토어:
const state$ =
actions$.pipe(
scan(reducer, 0)
)
Enter fullscreen mode
Exit fullscreen mode
디스패치:
actions$.next({
type: "ADD",
value: 5
})
actions$.next({
type: "ADD",
value: 10
})
Enter fullscreen mode
Exit fullscreen mode
출력:
5
15
Enter fullscreen mode
Exit fullscreen mode
우리는 scan을 이용해 반응형 상태 관리를 구현한 것입니다.
Event Sourcing Is Also scan()
은행 계좌를 생각해 보세요.
이벤트:
Deposit 100
Deposit 50
Withdraw 25
Enter fullscreen mode
Exit fullscreen mode
리듀서:
const accountReducer =
(balance, event) => {
switch (event.type) {
case "DEPOSIT":
return balance +
event.amount
case "WITHDRAW":
return balance -
event.amount
}
}
Enter fullscreen mode
Exit fullscreen mode
전통적인 방식:
events.reduce(
accountReducer,
0
)
Enter fullscreen mode
Exit fullscreen mode
결과:
125
Enter fullscreen mode
Exit fullscreen mode
하지만 이벤트가 실시간으로 들어온다고 상상해 보세요.
Deposit 100
(wait)
Deposit 50
(wait)
Withdraw 25
Enter fullscreen mode
Exit fullscreen mode
갑자기 **scan()**이 자연스러운 선택이 됩니다.
Enter fullscreen mode
Exit fullscreen mode
이벤트 소싱은 연속적인 축소 과정이 됩니다.
Why scan() Feels Magical
두 가지 강력한 아이디어가 결합됐기 때문입니다.
첫 번째:
State
Enter fullscreen mode
Exit fullscreen mode
두 번째:
Time
Enter fullscreen mode
Exit fullscreen mode
그 결과:
시간에 따른 상태 변화
Enter fullscreen mode
Exit fullscreen mode
이는 대부분의 애플리케이션이 수행하는 바로 그 일입니다.
Real World Example: Shopping Cart
액션:
Add Product
Add Product
Remove Product
Enter fullscreen mode
Exit fullscreen mode
scan 사용:
cartActions$
.pipe(
scan(
cartReducer,
[]
)
)
Enter fullscreen mode
Exit fullscreen mode
출력:
Cart Version 1
Cart Version 2
Cart Version 3
Enter fullscreen mode
Exit fullscreen mode
각 상태 변화가 자동으로 방출됩니다.
Real World Example: Live Analytics
방문자가 들어옵니다.
Visitor
Visitor
Visitor
Visitor
Enter fullscreen mode
Exit fullscreen mode
scan 사용:
visitors$
.pipe(
scan(
count => count + 1,
0
)
)
Enter fullscreen mode
Exit fullscreen mode
출력:
1
2
3
4
5
...
Enter fullscreen mode
Exit fullscreen mode
실시간 메트릭을 손쉽게 구현할 수 있습니다.
Real World Example: Game Score
플레이어 점수:
10
20
50
Enter fullscreen mode
Exit fullscreen mode
scan 사용:
score$
.pipe(
scan(
(total, score) =>
total + score,
0
)
)
Enter fullscreen mode
Exit fullscreen mode
출력:
10
30
80
Enter fullscreen mode
Exit fullscreen mode
대시보드에 안성맞춤입니다.
Performance Considerations
자주 떠오르는 오해: