RxDart vs FxDart

같은 실전 과제를 두 번 풉니다: 왼쪽은 RxDart, 오른쪽은 FxDart입니다. 두 버전 모두 브라우저에서 바로 실행되며 정확히 같은 결과를 출력합니다 — 두 모델을 비교하고, 여러분의 문제가 실제로 어느 모델의 문제인지 직접 판단해 보세요.

이 두 라이브러리는 경쟁자라기보다 상호 보완재에 가깝습니다. RxDart는 Dart의 Stream을 확장합니다 — 값이 언제 도착할지 생산자가 결정하는 push 모델로, 실제 시간 기준 연산자(debounceTime, combineLatest, switchMap)와 멀티캐스트(BehaviorSubject)가 자연스럽습니다. FxDart는 이터러블 위에서 동작합니다 — 언제 요청할지 소비자가 결정하는 pull 모델로, 지연 평가, 타입 있는 에러 처리, 순서를 지키는 제한된 동시성 (.concurrent(n))이 자연스럽고, 배압은 아예 문제가 되지 않습니다: 끌어오지 않는 것 자체가 배압이기 때문입니다. 두 라이브러리는 브리지 — fromStream / toStream — 에서 만나며, 아래 예제 중 몇 개는 일부러 두 라이브러리를 함께 사용합니다.

목록을 보기 전에 짚어 둘 습관이 하나 있습니다. 흔하지만, 잘못된 습관이기 때문입니다. Stream에는 풍부한 연산자 어휘가 딸려 있어서, 이미 손에 쥐고 있는 데이터를 Stream.fromIterable(orders)로 감싸 오직 map, where, distinct, expand와 유연한 체인을 쓰려 하고, 마지막에 결과를 await로 되받는 유혹이 생깁니다. 그 문제에는 비동기적인 구석이 하나도 없습니다. 값은 메모리에 있고, 질문에는 지금 당장 답이 있으며, 끝의 await가 바로 그 증거입니다. 동기적인 질문을 문법을 빌리려고 전달 메커니즘으로 바꿔 놓은 것이죠. 그렇게 얻는 것은 어휘이고, 치르는 값은 원소 하나하나마다 붙는 구독, 이벤트 루프 한 바퀴, 그리고 전달 단계입니다.

아래 벤치마크가 그 대가를 숫자로 보여 줍니다. 소스가 이미 메모리에 있는 예제 25개를 AOT 컴파일해 N = 1,000,000에서 측정하면, pull 파이프라인이 25개 전부에서 더 빠릅니다 — 중앙값 2.7배, 조기 종료 검색에서는 88배까지 벌어지고 (#1, 78.9 ms vs 0.9 ms), 무거운 리포트에서는 그 차이가 초 단위가 됩니다 (#11, 3.4 s vs 175 ms). 반대로 일이 정말로 비동기적인 경우에는 같은 측정에서 두 모델이 대등합니다: 그 16개 예제의 격차 중앙값은 1.05배이고, 대부분 무승부 배지를 답니다. 이 대비가 이 섹션을 한 줄로 요약합니다 — 스트림이 느린 것이 아니라, 동기적인 데이터를 스트림에 통과시키면 애초에 필요하지 않았던 전달 비용을 치르게 되는 것입니다.

반대쪽도 그만큼 분명히 말해 두겠습니다: 문제가 정말 시간에 따라 도착하는 이벤트에 관한 것이라면 — 사용자 입력, 시세 틱, 소켓 — 스트림이 그 문제에 맞는 형태이고, 파이프라인 어휘를 아무리 쌓아도 그것을 대신하지 못합니다. FxDart는 그 아이디어를 흡수하는 것으로 이를 인정합니다: 이벤트 레이어 (fxEvents)가 Rx 스타일의 push 연산자들 — debounce, throttle, sample, combineLatest, switchMap, race, 그리고 LiveValue — 를 평범한 Dart 스트림 위에 올려 주므로, Part 4의 시간 형태 쌍들은 연산자 대 연산자로 대등하게 만납니다. RxDart에는 여전히 Subject 클래스 계층과 인자 개수 접미사 combineLatest2…9 오버로드가 있고, fxdart는 같은 파일에서 rxdart와 충돌하지 않으면서 평범한 Dart 스트림 위의 을 다룹니다 — 윈도, 라이브 groupsBy, shareReplay, 셀렉터 구동 debounce, combine, 네 가지 fromStream* pull 정책. 이 쌍들이 드러내는 것은 이야기의 나머지 절반 — 스트림으로 풀리는 문제가 사실은 스트림 옷을 입은 데이터 파이프라인인 경우가 얼마나 많은가 — 입니다: 유한한 fetch, 배치 변환, 페이지네이션 크롤링 같은 것들이죠. 그런 문제에서는 pull 버전이 더 짧고, 순서가 보장되고, 타입이 있으며, 구독 라이프사이클이 아예 필요 없습니다.

FxDart 승 — 이 문제에는 pull 모델이 더 잘 맞습니다 · 우열 없음 — 두 모델 모두 깔끔하게 표현합니다 · async — 비동기 파이프라인을 사용합니다

과제가 처리량 형태인 페이지에는 Benchmark 섹션도 함께 실려 있습니다 — 두 구현 모두 AOT 컴파일해 N=100과 큰 N 헤드라인(동기 과제는 1M; 모든 요소가 이벤트 루프를 건너는 경우는 10,000)에서 측정합니다. 실제 시간 기준 예제(#38–#46)는 일부러 벤치마크하지 않았습니다: 디바운스 윈도와 샘플 틱은 파이프라인이 아니라 시계를 측정하기 때문입니다.

예제마다 두 가지 관점을 보여줍니다. 채워진 배지는 코드에 대한 판정 — 어느 쪽이 더 쓰고 읽기 좋은가입니다. 테두리만 있는 ⚡ 배지는 측정 결과 — 가장 큰 벤치마크 크기에서 AOT 컴파일로 실측한 실행 시간 승자이며, 전체 그래프는 각 페이지의 벤치마크 섹션에 있습니다.

다섯 개만 본다면, 이 예제들을 보세요: #1 예산을 넘는 첫 거래 · #11 이동마다의 재고 수준 · #25 모든 검증 오류 보고하기 · #35 4개씩 가져오되 결과는 순서대로 · #50 소스 하나, 독립 리더 둘

1부 · 같은 일, 두 라이브러리

겹치는 영역입니다: 두 어휘 모두에 존재하는 변환·필터·슬라이스 — 풀 체인과 스트림 트랜스포머를 나란히 놓고 봅니다.

  1. 1 예산을 넘는 첫 거래 우열 없음 ⚡ FxDart가 더 빠름 100을 넘는 첫 거래를 찾고 멈추기 — Rx firstWhere는 구독을 취소하고, fxdart find는 끌어오기를 멈춥니다. 양쪽 모두 8건 중 4건만 검사합니다.
  2. 2 입출금 피드의 누적 잔액 우열 없음 ⚡ FxDart가 더 빠름 입금과 출금의 피드를 누적 잔액으로 접기 — Rx의 scan과 fxdart의 scan, 양쪽 모두 이동 한 건당 누산 한 번.
  3. 3 주문을 라인 단위로 펼치기 우열 없음 ⚡ FxDart가 더 빠름 네 건의 주문을 열 개의 order/sku 라인 아이템으로 펼치기 — Stream.expand와 fxdart flatMap은 일대다를 가리키는 같은 단어이고, 소스 순서를 지킵니다.
  4. 4 유효한 짝수 금액 합산하기 FxDart 승 ⚡ FxDart가 더 빠름 파싱 실패는 버리고, 짝수만 남겨 합산하기 — async main이 필요한 Stream 파이프라인과 같은 고정 리스트 위의 동기 pull 체인 하나를 비교합니다.
  5. 5 고유 방문자, 첫 방문만 남기기 우열 없음 ⚡ FxDart가 더 빠름 방문 로그를 피드 전체에 걸쳐 중복 제거하고 각 사용자의 첫 방문만 남기기 — equals+hashCode가 필요한 distinctUnique와 키 함수 하나면 되는 uniqBy를 비교합니다.
  6. 6 마지막 에러 세 건 우열 없음 ⚡ FxDart가 더 빠름 ERROR 줄만 남겨 마지막 세 건을 출력하기 — takeLast는 done 이벤트를 기다리고, takeRight는 이터러블을 끝까지 소진합니다. 둘 다 정확히 세 개를 버퍼링합니다.
  7. 7 빈 리포트를 위한 기본 한 줄 우열 없음 ⚡ FxDart가 더 빠름 일치하는 항목이 없는 카테고리로 필터링해도 무언가는 출력하기 — 스트림의 defaultIfEmpty와 pull 체인의 ifEmpty, 두 모델에 담긴 같은 아이디어.
  8. 8 null은 버리고 값만 남기기 우열 없음 ⚡ FxDart가 더 빠름 null이 섞인 센서 피드를 정리하고 남은 값을 포맷하기 — whereNotNull은 이름만 다른 compact이고, 둘 다 double?을 double로 정적으로 좁힙니다.
  9. 9 예열 구간 판독값 건너뛰기 우열 없음 ⚡ FxDart가 더 빠름 프로브의 앞쪽 낮은 판독값은 버리고 그 뒤는 전부 남기기 — skipWhile과 dropWhile은 같은 단방향 게이트이고, 연산자마저 코어에 있습니다.
  10. 10 체크리스트에 번호 매기기 FxDart 승 ⚡ FxDart가 더 빠름 여섯 단계를 1.로 시작하는 번호 목록으로 — 스트림에는 인덱스 있는 map이 없어 Rx는 scan에 카운터를 밀반입하고, fxdart는 zipWithIndex라고 말합니다.

2부 · 윈도우, 상태, 순서

버퍼, 쌍, 누적 상태, 인접 원소 — 푸시와 풀 사이의 미묘한 의미 차이가 드러나기 시작하는 지점입니다.

  1. 11 이동마다의 재고 수준 우열 없음 ⚡ FxDart가 더 빠름 창고 입고와 출고를 누적 재고 수준으로 접고 백오더를 표시하기 — 양쪽 모두 scan, 시드를 재생하는 방식만 다릅니다.
  2. 12 일별 시리즈에서 주간 합계 FxDart 승 ⚡ FxDart가 더 빠름 21일치 지출을 주 번호가 붙은 세 개의 합계로 말아 올리기 — 카운터로 차출된 scan을 곁들인 bufferCount vs chunk + zipWithIndex.
  3. 13 감사 로그에 값과 실패를 함께 FxDart 승 ⚡ FxDart가 더 빠름 여덟 줄의 설정을 파싱하는데 셋이 실패하는 상황에서 값과 실패 수를 출력하기 — 에러를 데이터로 되밀반입하는 쪽 vs 평범한 partition.
  4. 14 열림과 닫힘 마커 우열 없음 ⚡ FxDart가 더 빠름 세션 피드를 OPEN/CLOSE 줄로 감싸기 — 스트림의 startWith와 endWith vs pull 체인의 prepend와 append.
  5. 15 상태가 바뀔 때만 보고하기 우열 없음 ⚡ FxDart가 더 빠름 반복투성이 헬스 피드를 런당 한 줄로 접기 — Stream.distinct vs uniqAdjacent, 전역 사촌으로는 distinctUnique와 uniq.
  6. 16 4개씩 배치로 업로드하기 우열 없음 ⚡ FxDart가 더 빠름 대기 중인 파일 열 개, 요청당 최대 네 개 — 스트림의 bufferCount(4)와 pull 체인의 chunk(4), 짧은 마지막 배치는 양쪽 모두 같습니다.
  7. 17 카테고리별 지출 합계 FxDart 승 ⚡ FxDart가 더 빠름 처음 등장한 순서의 카테고리별 합계 — GroupedStream들의 스트림을 접고 다시 병합해야 하는 쪽 vs 그냥 Map을 돌려주는 groupBy.
  8. 18 두 피드를 엄격히 순서대로 우열 없음 ⚡ FxDart가 더 빠름 어제 로그의 꼬리 뒤에 오늘 로그를 이어 번호 목록 하나로 — 구독을 순서대로 세우는 concatWith vs pull을 순서대로 세우는 concat.
  9. 19 틱 사이의 변화량 우열 없음 ⚡ FxDart가 더 빠름 각 가격 틱을 직전 틱과 나란히 — 두 라이브러리 모두 pairwise, 스트림 쪽은 리스트 쌍, pull 쪽은 타입 있는 레코드.
  10. 20 예보와 실측값 맞추기 우열 없음 ⚡ FxDart가 더 빠름 고정된 두 시리즈를 위치별로 짝지어 날마다의 차이를 출력하기 — 스트림의 zipWith vs 이터러블의 zip, 정렬은 어느 쪽이든 같습니다.
  11. 21 판독값 3개 이동 평균 FxDart 승 ⚡ FxDart가 더 빠름 센서 판독값의 이동 평균 — 꼬리 부분 윈도우를 막는 길이 필터가 필요한 bufferCount(3, 1) vs 뜻하는 바를 그대로 말하는 windowed(3).
  12. 22 종료 마커까지, 마커 포함 우열 없음 ⚡ FxDart가 더 빠름 SHUTDOWN까지의 모든 이벤트를 마커 포함해 남기고 낙오자들은 버리기 — takeWhileInclusive vs takeUntilInclusive, 같은 절단의 두 표기.
  13. 23 페이지 피드를 id로 중복 제거 우열 없음 ⚡ FxDart가 더 빠름 겹치는 세 페이지를 펼치고 각 상품 id를 도착 순서대로 한 번씩만 남기기 — expand + distinctUnique vs flatMap + uniqBy.
  14. 24 가장 빠른 요청과 가장 느린 요청 우열 없음 async ⚡ FxDart가 더 빠름 엔드포인트 여덟 개를 비동기로 프로브해 최소·최대 지연을 출력하기 — 양쪽 모두 Future를 반환하는 축약, 각각 새 패스 한 번씩.

3부 · 오류와 복원력

타입 없는 오류 채널 vs 타입 있는 값으로서의 오류 — 재시도, 타임아웃, 폴백, 리소스 수명을 두 모델에서 비교합니다.

  1. 25 모든 검증 오류 보고하기 FxDart 승 ⚡ FxDart가 더 빠름 폼마다 첫 번째만이 아닌 모든 규칙 위반 — 동기 체인의 평범한 오류 값 vs 오류를 하나만 싣고 닫혀 버리는 오류 채널.
  2. 26 성공과 실패 나누기 FxDart 승 async ⚡ FxDart가 더 빠름 비동기 검증 일곱 건, 두 건 실패 — 타입 있는 partition에 이어지는 항목별 try/catch vs 오류 채널을 다시 데이터로 되돌리는 내부 스트림들.
  3. 27 세 번 실패하면 포기하기 FxDart 승 async ⚡ FxDart가 더 빠름 scan으로 실패를 세어 세 번째에서(포함하여) 멈추기 — 매퍼 안의 try/catch 하나 vs scan이 보기 전에 오류를 마커 값으로 바꾸는 작업.
  4. 28 점점 길어지는 백오프로 재시도하기 FxDart 승 async ⚡ FxDart가 더 빠름 시도 사이의 대기를 늘려 갑니다 — 각 오류를 손수 타이머 스트림으로 매핑하는 retryWhen vs Duration을 반환하는 delay 훅.
  5. 29 불안정한 fetch 재시도하기 우열 없음 async ⚡ FxDart가 더 빠름 두 번 실패한 뒤 성공하는 fetch — Rx.retry는 스트림 팩토리를 재구독하고, fxdart retry는 Future를 다시 실행합니다. 양쪽 다 호출 하나.
  6. 30 멈춰 버린 읽기에 시간 제한 걸기 우열 없음 async ⚡ FxDart가 더 빠름 멈추는 센서 읽기에 150 ms 예산 — 스트림 timeout은 이벤트 사이의 간격을 재고, 풀 timeout은 요구부터 항목까지의 시간을 잽니다.
  7. 31 불안정한 행을 각각 따로 재시도하기 FxDart 승 async ⚡ 속도 동일 불안정한 임포트 행 여섯 개, 행마다 두 번 시도, 세 개 동시 진행 — flatMap은 완료 순서로 내보내고, concurrent 아래의 mapRetry는 소스 순서를 지킵니다.
  8. 32 읽기를 감싸는 커서의 수명 우열 없음 async ⚡ 속도 동일 커서를 열고, 행 다섯을 읽고, 닫힘을 보장 — 스트림을 감싸는 Rx.using vs 지연 풀을 감싸는 usingAsync, 한 아이디어의 두 포팅.
  9. 33 프로모 가격, 없으면 정가 FxDart 승 async ⚡ FxDart가 더 빠름 프로모 가격이 있으면 그 가격, 없으면 정가 — 내부 스트림으로 하는 항목별 복구 vs 호출 바로 옆의 try/catch.
  10. 34 소스가 죽으면 캐시로 이어가기 우열 없음 async ⚡ 속도 동일 라이브 피드가 업데이트 세 건 뒤에 죽습니다 — 캐시된 꼬리를 갈아 끼우는 onErrorResumeNext vs concat에 이어 붙이는 명시적 풀 루프.

4부 · 동시성, 시간, 푸시

두 모델이 갈라지는 지점입니다: 한쪽은 수요 기반 동시성, 다른 쪽은 실제 시간과 멀티캐스트 — RxDart가 그야말로 제격인 예제들도 여기 있습니다.

  1. 35 4개씩 가져오되 결과는 순서대로 FxDart 승 async ⚡ FxDart가 더 빠름 fetch 여덟 건, 네 개 동시 진행, 소스 순서로 출력 — mapConcurrent는 구조적으로 순서를 지키고, flatMap(maxConcurrent)은 태그를 붙여 다시 정렬해야 합니다.
  2. 36 가장 빠른 결과부터 우열 없음 async ⚡ 속도 동일 각 결과를 도착하는 순간 출력 — 완료 순서는 flatMap의 본래 동작이고, fxdart는 전용 concurrentPool 연산자로 이에 맞섭니다.
  3. 37 스트림이 타입 있는 파이프라인으로 흘러들 때 우열 없음 async ⚡ 속도 동일 라이브 로그 스트림이 fxStream을 거쳐 타입 있는 풀 파이프라인으로 흘러갑니다 — 경고만 남기고, 대문자로 바꾸고, 세기. 다리의 양쪽에서.
  4. 38 검색창 디바운스하기 우열 없음 async 타이핑이 잠잠해질 때까지 기다렸다가 검색 — 이벤트 스트림 위의 debounceTime vs fxdart 이벤트 레이어의 동일한 debounce 체인.
  5. 39 늦게 온 리더를 위한 라이브 현재 값 우열 없음 async 늦게 접속한 대시보드도 현재 온도를 즉시 받습니다 — BehaviorSubject와 LiveValue 모두 최신 값을 리플레이한 뒤 라이브로 스트리밍합니다.
  6. 40 최신 검색만 중요할 때 우열 없음 async 더 새 쿼리가 진행 중인 검색을 버립니다 — rxdart와 fxdart의 fxEvents 체인 양쪽 모두 같은 switchMap 연산자.
  7. 41 각 요청에 최신 설정 도장 찍기 우열 없음 async 나가는 요청마다 그 순간의 config 버전을 싣습니다 — rxdart와 fxEvents 양쪽 모두 같은 withLatestFrom 연산자.
  8. 42 100 ms마다 호출 하나 우열 없음 async 최소 100 ms 간격의 핑 다섯 번, 단조 Stopwatch로 증명 — rx interval vs 순차 풀 체인의 매퍼 안에 넣은 평범한 delay.
  9. 43 폼이 유효해지면 제출 버튼 켜기 우열 없음 async 최신 이메일·비밀번호 값을 결합해 제출 버튼을 구동 — Rx.combineLatest2 vs fxdart 이벤트 레이어의 combineLatest.
  10. 44 새로고침 버튼 스로틀링하기 우열 없음 async 300 ms 윈도마다 탭 하나만 통과 — 탭 스트림 위의 throttleTime vs fxdart 이벤트 레이어의 동등한 throttle 체인.
  11. 45 폴링 틱마다 게이지 샘플링하기 우열 없음 async 각 폴링 틱 시점의 최신 게이지 값 읽기 — RxDart의 명시적 sample 트리거 스트림 vs fxdart 이벤트 레이어의 sampleOn.
  12. 46 두 미러 경주시키기 우열 없음 async 페이로드 하나를 두 미러가 경주합니다 — Rx.race와 FxEvents.race 모두 지는 fetch를 진행 중에 취소하고, 완료된 fetch가 하나뿐임을 똑같이 증명합니다.
  13. 47 파이프라인이 스트림 소비자로 이어질 때 우열 없음 async ⚡ 속도 동일 순서를 지키는 mapConcurrent fetch가 결과를 toStream으로 스트림 소비자에게 넘깁니다 — 반대 방향으로 건넌 다리.
  14. 48 소진될 때까지 페이지 크롤링하기 FxDart 승 async ⚡ 속도 동일 준비됐을 때만 다음 페이지를 요청 — 요구가 있을 때 당겨지는 끝없는 지연 커서 vs 첫 빈 페이지에서 취소되는 충분히 큰 Rx.range.
  15. 49 각 호출이 다음 호출로 이어질 때 우열 없음 async ⚡ 속도 동일 각 응답이 다음 요청의 재료가 되는 API 호출 네 번 — scan은 상태를 파이프라인 속으로 꿰어 넣고, asyncMap은 가변 토큰을 클로저로 붙듭니다.
  16. 50 소스 하나, 독립 리더 둘 우열 없음 ⚡ FxDart가 더 빠름 부수효과 있는 소스 하나에서 두 번 실행하지 않고 합계와 최댓값 얻기 — connectable 스트림 vs 같은 원소 위에서 함께 전진하는 두 개의 폴드.