scan()은 무한 데이터에 대한 reduce이다

발행: (2026년 6월 6일 PM 01:11 GMT+9)
9 분 소요
원문: Dev.to

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

자주 떠오르는 오해:

0 조회
Back to Blog

관련 글

더 보기 »

모바일 한여름 열풍

!Cover image for Mobile Midsommer Madnesshttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploa...