-
TCA - ThrottlingTCA 2022. 10. 20. 10:18
안녕하세요. 그린입니다🍏
이번 포스팅에서는 지난 Debouncing에 이어 TCA에서의 Throttling에 대해 학습하겠습니다🙌
혹 Debouncing 포스팅을 보지 않으셨으면 이번 Throttling과 비교가 될거라 먼저 보고 오시면 도움이 됩니다ㅎㅎ
https://green1229.tistory.com/290
우선 Throttling을 알아보기전에 Rx나 Combine에서 쓰이는 Debounc와 Throttle의 차이가 무엇인지 궁금하시면 아래 포스팅을..!
https://green1229.tistory.com/178
자 그럼 본격적으로 해당 포스팅 시작합니다🕺🏻
우선 Debounce와 Throttle이 어떤 차이인지 개념적으로 짚고 넘어가야 합니다.
사실 저 차이만 알아도 오늘 학습 끝!Debounce와 Throttle의 차이
Debounce는 이벤트가 들어온 후 일정 시간 만큼 아무 이벤트가 들어오지 않으면 들어온 마지막 이벤트를 방출합니다.
만약 일정 시간이 지나기전 다른 추가 이벤트가 발생되면 이전 이벤트는 취소됩니다.Throttle은 일정 시간 동안에 이벤트가 한번만 방출되도록 보장해줍니다.
개념적으로 설명해도 조금 이해가 난해하죠?
그렇기에 예시를 들어서 풀어서 딱 쉽게 설명해볼께요!우리가 검색창에 타이핑을 하면 하단에 연관검색어가 나타나는걸 볼 수 있죠.
이건 해당 타이핑된 텍스트를 가지고 연관검색어 API를 호출하는 이벤트라고 볼 수 있습니다.
이 기능을 사용한다고 할때 Debounce와 Throttle의 차이를 보겠습니다.
우선 Debounce!
타이머 간격은 3초로 설정해주고 타이핑 이벤트에 Debounce를 걸어주었다고 가정해봅시다.
GREEN을 검색한다고 가정할때 우리는 G, R, E, E, N을 하나씩 칠때 G를 치고 3초안에 R을 치는 경우라면 가장 최근 이벤트는 R로 교체되고 아직 연관검색어 API를 쏘는 이벤트는 3초가 지나지 않아 발생하지 않아요.
그러고 만약 다음 E라는 워드 타이핑이 있기까지 3초의 시간이 흘렀다면 GR을 가지고 이벤트를 방출합니다.
즉 GR에 해당하는 연관 검색어를 찾아주죠!
그 다음 E를 입력하면 또 3초 지연이 있나 다시 체크하게 되는 방식입니다.
GREEN를 각 단어별 2초씩 시간을 잡으면서 타이핑해도 도중 3초 지연이 없기에 결국 N을 입력하고 3초 후 GREEEN을 가지고 한번만 연관 검색어 API를 찔러주는 이벤트를 발생하는것입니다.
그다음 Throttle!
타이머 간격을 동일하게 3초로 설정해주고 타이핑 이벤트에 Throttle을 걸어주었다고 가정할께요.
또 똑같이 GREEN을 쳐봅시다!
각 단어를 치는데 2초가 걸렸어요.
그럼 어떤 결과가 나올까요?
처음에서 3초가 지나는 시점인 GR에서 연관 검색어 API 이벤트를 한번 방출해줍니다.
그리고 또 3초가 지나는 시점인 GRE에서 이벤트를 방출해줍니다.
마지막으로 3초가 또 지나는 시점인 GREEN에서 이벤트를 방출해줍니다.
어떤 느낌인지 아시겠죠..!?
Throttle이 갖는 Debounce와의 차이는 일정 시간 간격으로 마지막 이벤트를 방출해주는 차이입니다.
자 그럼 이제 두 차이를 알아봤으니 오늘 주제인 진짜.. Throttling에 대해 알아보죠..🥲
TCA에서의 Throttling?
TCA라고 다를건 없습니다.
위의 설명된 Throttle 개념 그대로입니다!
(그렇기에 대충 위의 설명으로 갈무리 하는짤)
TCA에서의 throttle 메서드 정의
import Combine import Dispatch import Foundation extension EffectPublisher { public func throttle<S: Scheduler>( id: AnyHashable, for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool ) -> Self { switch self.operation { case .none: return .none case .publisher, .run: return self.receive(on: scheduler) .flatMap { value -> AnyPublisher<Action, Failure> in throttleLock.lock() defer { throttleLock.unlock() } guard let throttleTime = throttleTimes[id] as! S.SchedulerTimeType? else { throttleTimes[id] = scheduler.now throttleValues[id] = nil return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() } let value = latest ? value : (throttleValues[id] as! Action? ?? value) throttleValues[id] = value guard throttleTime.distance(to: scheduler.now) < interval else { throttleTimes[id] = scheduler.now throttleValues[id] = nil return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() } return Just(value) .delay( for: scheduler.now.distance(to: throttleTime.advanced(by: interval)), scheduler: scheduler ) .handleEvents( receiveOutput: { _ in throttleLock.sync { throttleTimes[id] = scheduler.now throttleValues[id] = nil } } ) .setFailureType(to: Failure.self) .eraseToAnyPublisher() } .eraseToEffect() .cancellable(id: id, cancelInFlight: true) } } public func throttle<S: Scheduler>( id: Any.Type, for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool ) -> Self { self.throttle(id: ObjectIdentifier(id), for: interval, scheduler: scheduler, latest: latest) } } var throttleTimes: [AnyHashable: Any] = [:] var throttleValues: [AnyHashable: Any] = [:] let throttleLock = NSRecursiveLock()
자 길어보이지만 별거 없습니다.
debounce와 마찬가지로 Effect에서의 메서드이기에 EffectPublisher를 extension해 기능을 구현해뒀습니다.
우선 Throttle은 일정 시간 마다 해당 마지막 들어온 이벤트를 하나씩 처리하는거라고 학습했어요.
그렇기에 스케쥴링이 필요하고 비동기 처리가 되어야 하니 Dispatch 라이브러리를 임포트 합니다.
throttle 메서드 구현을 보시면 총 4개의 파라미터를 받아요.
1. id: Hashable한 id 파라미터를 받습니다.
2. interval: 제일 중요한 어느 정도의 일정 시간 간격을 설정해줄지에 대한 값을 받습니다.
3. scheduler: 어느 스케쥴러에서 스케쥴링될지 스케쥴러를 받습니다.
4. latest: 일정 시간 간격이 끝났을때 제일 마지막에 들어온 이벤트를 방출할지 말지 T/F로 결정해주는 Bool 값을 받습니다.
그러고 반환 타입은 역시 해당 Effect 셀프 타입이죠.
이제 run case일 시 내부 구현을 보죠.
총 3개의 외부 변수를 사용하는데 throttleTimes, throttleValues, throttleLock이 있습니다.
요것들을 활용하는걸 볼 수 있어요.
우선 맨 처음 만나는건 defer입니다.
함수 안에서 해당 실행을 제일 마지막으로 연기시켜 실행하는 클로저에요.
결국 throttleLock을 풀어주는 unlock을 마지막에 실행 시킨다는걸 추측할 수 있습니다.
그 다음 throttleTimes를 스케쥴러타임타입으로 형 변환해 바인딩 해줍니다.
조건에 맞지 않으면 throttle 중이 아닌것이기에 바로 방출해버립니다.
그리고 unlock으로 풀어주죠.
이러한 조건 체크와 설정을 거친 후 delay를 걸어주게 됩니다.
정해진 delay 시간 후에는 handleEvents로 아웃풋을 받게됩니다.
요 내부에서 throttleLock의 동기 처리를 해주게 되요.
즉 throttle 시간을 다시 걸어주게 되고 이전 throttleValue는 제거해주죠.
이게 구현 다에요.
핵심적으로 제가 중요하다고 느꼈던건 헷갈릴 수 있고 너무 비슷해 보이는
"Debounce와 Throttle의 차이를 인지하고 잘쓰자!" 입니다🙌마무리
Throttle의 어원을 찾아보니 목을 조르다 이런 뉘앙스의 단어더라구요..?
그래서 목을 졸라서 숨 쉬는걸 통제하는것처럼 이벤트도 그렇게 강제적으로 혈관 막듯이 3초에 하나씩만 방출되게끔 통제하는게 아닌가...
하는 생각이 들었습니다😱
[참고자료]
'TCA' 카테고리의 다른 글
TCA - fireAndForget (0) 2022.10.27 TCA - concatenate & merge (여러 Effect를 단일 Effect로 만들기) (2) 2022.10.25 TCA - Debouncing (0) 2022.10.17 TCA - Timer (2) 2022.10.13 TCA - IfLetStore (0) 2022.10.11