felix-iOS

Combine) debounce와 throttle의 차이 본문

Combine

Combine) debounce와 throttle의 차이

felix-mr 2021. 7. 18. 17:47

안녕하세요 🙇‍♂️

 

debounce와 throttle는 자주 사용해보셨을겁니다.

하지만 항상 헷갈리게 하는 분들이기 때문에 정리를 확실하게 해놓기 위해서 이번 게시글을 작성합니다.

 

 

Debounce

func debounce<S>(for dueTime: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.Debounce<Self, S> where S : Scheduler

이벤트 간에 지정된 시간이 경과된 후에만 요소를 게시합니다.

 

공식문서에 따르면

 

debounce operator를 사용하여 업스트림 publisher의 전송 간격과 개수를 제어합니다.

이 operator는 다운스트림에 전달되는 값의 수를 지정한 속도로 줄여야 하는 버스트 또는 대량 이벤트 스트림을 처리하는 데 유용합니다.

 

라고 되어있습니다.

 

주로 사용되는 출처는 API 검색을 할 때, TextField나 SearchBar에 있는 텍스트를 입력할 때마다 API 요청을 하는 것이 아니라 사용자가 검색을 잠시 멈추는 경우에 API 요청을 하게 하기 위해 사용합니다. (개인적으로 이렇게 많이 사용했습니다😅)

 

공식문서의 예제 말고 직접 예제를 만들어 봤습니다.

 

 

UI는 UITextField(위)와 UILabel(아래)로 구성을 했습니다.

텍스트 필드에 텍스트를 입력하면 debounce operator를 통해 일정시간이 흐른뒤 라벨이 업데이트 되도록 했습니다.

왜 검색 API에 많이 사용되는지 감이 오시나요?

 

이번에 코드를 확인해보도록 하겠습니다.

다른 잡다한 코드는 제거하고 흐름에 따라서 바인딩과 관련된 코드만 확인해보겠습니다.

// DebounceViewController

private extension DebounceViewController {
  
  func setupView() {
  
    ... 생략 ...
  
    textField.addTarget(
      self, 
      action: #selector(textFieldEditingChanged(_:)), 
      for: .editingChanged
    )
  }
  
  @objc func textFieldEditingChanged(_ sender: UITextField) {
    viewModel.text
      .send(sender.text)
  }
}

먼저 텍스트필드에 입력이 될 때마다 viewModel의 text 서브젝트로 해당 텍스트를 send 해줍니다.

입력이 될 때마다 모든 텍스트가 send 되겠죠?

 

이제 DebounceViewModel을 살펴봅시다.

// DebounceViewModel

final class DebounceViewModel {
  
  private(set) var text = PassthroughSubject<String?, Never>()
  private(set) var result = PassthroughSubject<String, Never>()
  
  ... 생략 ...
}

private extension DebounceViewModel {
  
  private func bind() {
    text
      .debounce(for: 0.5, scheduler: RunLoop.main)
      .compactMap { $0 }
      .sink { self.result.send($0) }
      .store(in: &cancellables)
  }
}

DebounceViewModel에는 text와 result 프로퍼티가 PassthroughSubject로 정의되어 있습니다.

text의 경우에는 위에서 확인했듯이 텍스트 필드의 입력되는 텍스트를 모두 send 받습니다.

result는 말 그대로 라벨에 표시될 결과값을 정의한 프로퍼티겠죠?

 

자! bind()를 보면 정의된 text를 구독하고 있습니다. 여기서 debounce operator가 사용되고 있는 것을 확인할 수 있습니다.

또 값이 방출되면 sink를 통해 그 값을 다시 result로 send 해줍니다.

 

여기서는 text 서브젝트에 값이 send 된 후 0.5초 후에 해당 값을 방출하게 됩니다. 하지만 text로 또 다른 값이 send되는 경우에는 타이머가 리셋되어 새로 send된 값을 0.5초 후에 방출하게 됩니다.

 

정리해보자면 마지막 값이 send된 후 dueTime에 설정된 시간이 지날 때 까지 다른 값이 send 되지 않았다면 그 값을 방출한다!

 

라고 설명할 수 있겠습니다. 

 

이해가 되셨나요..?

 

다시 실행화면을 보시면 좀 더 이해가 빠르게 되시지 않을까 생각됩니다..😂

 

 

 

Throttle

func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler

지정된 시간 간격으로 업스트림 publisher가 publish한 가장 최근 값 또는 첫 번째 요소를 publish 합니다.

 

예제를 확인하기 전에 공식문서의 추가적인 설명을 확인해보면

 

지정한 간격 동안 Throttle operator를 사용하여 업스트림 publisher에서 값을 선택적으로 다시 publish합니다.

조절 간격 동안 업스트림에서 수신된 값은 다시 publish되지 않습니다.

 

라고 합니다.

 

Throttle은 가장 최근 값 또는 첫 번째 값만을 publish하기 때문에

 

첫 번째 값일 경우 -> 시간이 경과되기 전까지 첫 번째 이후에 업스트림으로 부터 publish 된 값들은 무시

가장 최근 값일 경우  -> 시간이 경과되기 전 시점에서 마지막 요소 이전에 업스트림으로부터 publish 된 값들은 무시

 

의 경우를 말해줍니다. 이 부분의 이해가 가장 중요한 것 같습니다.

 

또한 publish 할 값을 가장 최근 값 또는 첫 번째 요소로 선택할 때 latest 인자를 사용합니다.

 

이번에도 공식문서 예제가 아닌 직접 만들어 본 예제를 통해 확인해 봅시다!

 

먼저 latest == false일 때 입니다.

 

UI의 경우 + 버튼을 탭하면 count 값이 증가하게되고, - 버튼을 탭하면 count 값이 감소하게 되도록 했습니다.

latest가 false로 설정되어 있기 때문에 최근 값이 아닌 첫 번째 값을 타이머에 맞춰 다시 publish 하겠죠?

확인해보면 + 버튼을 계속 탭하더라도 탭한 횟수만큼 count값이 증가하는 것이 아니라 약간의 텀을 두고 증가하는 것을 확인할 수 있습니다.

 

또한, + 버튼을 탭하다가 값이 증가하기전에 - 버튼을 탭하더라도 값이 감소하지 않고 역시 증가하는 것을 볼 수 있습니다. 이것은 latest가 false로 설정되어있기 때문입니다!

 

코드를 확인해 봅시다!

// ThrottleViewController

private extension ThrottleViewController {
  
  func setupView() {
  
    ... 생략 ...
    
    plusButton.addTarget(self, action: #selector(plusButtonTapped(_:)), for: .touchUpInside)
    minusButton.addTarget(self, action: #selector(minusButtonTapped(_:)), for: .touchUpInside)
  }
  
   @objc func plusButtonTapped(_ sender: UIButton) {
    viewModel.touchEvent.send(1)
  }
  
  @objc func minusButtonTapped(_ sender: UIButton) {
    viewModel.touchEvent.send(-1)
  }
}

plusButton과 minusButton을 탭했을 때 viewModel의 touchEvent로 1 또는 -1의 값을 send 해줍니다.

 

ThrottleViewModel을 확인해보겠습니다.

 

// ThrottleViewModel

final class ThrottleViewModel {

  private(set) var touchEvent = PassthroughSubject<Int, Never>()
  @Published private(set) var count = 0
  
  ... 생략 ...
}

private extension ThrottleViewModel {
  
  func bind() {
    touchEvent
      .throttle(for: 1, scheduler: RunLoop.main, latest: false)
      .sink { self.count += $0 }
      .store(in: &cancellables)
  }
}

ThrottleViewModel에는 touchEvent와 count가 프로퍼티로 선언되어 있습니다.

당연히 touchEvent를 구독하는 코드가 있겠죠? bind()를 확인해보면 touchEvent를 구독하고 있습니다. throttle의 타이머를 1초로 설정했고 latest는 false로 설정했습니다.

 

throttle oeprator를 통해 touchEvent(업스트림)에서 값이 publish 될 때 첫 번째로 publish 된 값을 1초가 지난 후에 다시 publish 해줍니다.

 

이번에는 latest == true 를 볼까요?. 코드는 false -> true 만 변경되기 때문에 실행화면만 확인해보겠습니다.

 

 

마찬가지로 + 버튼을 탭하면 count 값이 증가하고 - 버튼을 탭하면 count 값이 감소하는 것을 확인할 수 있습니다.

하지만 + 버튼을 계속 탭하다가 1초가 지나기전에 - 버튼을 탭하게 되면 count 값이 감소하게 됩니다. 이것은 latest를 true로 설정했기 때문에 업스트림에서 publish 된 첫 번째 값이 아닌! throttle을 통해 다시 publish 될 시점의 가장 최근값을 방출하게 되는 것입니다.

 

 

Debounce와 Throttle의 차이

자! 결론을 정리해 보겠습니다.

 

일단 Debounce와 Throttle의 공통점? 유사점? 을 확인해봅시다.

 

1. 타이머를 사용합니다. (실제 타이머 객체를 사용한다는 뜻이아니고 시간 간격에 따라 publish가 된다는 의미입니다 😅)

2. 가장 최근값을 publish 합니다. (Throttle의 경우에는 첫번째 값을 publish 하기도 합니다.)

3. 타이머에 맞춰 publish 되는 값 이외에 다른 값은 무시됩니다.

 

이런 점 때문에 그 동안 Debounce와 Throttle이 헷갈렸던게 아닐까요? ㅎㅎ..

 

그럼 확실한 차이점을 확인해봅시다.

 

1. Debounce는 타이머가 리셋됩니다. 업스트림에서 새로운 값이 publish 되는 경우 타이머가 리셋되는 것을 확인할 수 있었죠?

하지만 Throttle의 경우 타이머가 리셋되지 않고 정해진 시간 간격에 따라 주기적으로 값을 다시 publish 합니다.

2. Debounce는 항상 최근 값을 다시 publish 합니다. 하지만 Throttle의 경우에는 latest 인자를 통해서 최근 값 또는 첫번째 값 중 publish 할 값을 선택할 수 있습니다.

 

 

위의 예제는 Github에서 확인해 보실 수 있습니다!

 

 

이번 게시글에서 Debounce와 Throttle에 대해 정리를 해봤는데

블로그를 쓰면서 항상 느끼지만 다른 사람에게 설명하는 것이 증말 어려운것 같아요.🤯

 

혹시 글을 읽으시다가 이해가 안가시거나 왜 이렇게 얘기했는지 의문이 드는 점에 대해서는 댓글 남겨주시면 감사하겠습니다 :)

물론 틀린 부분도 댓글 부탁드립니다!

 

그럼 20000!

 

 

Reference

https://developer.apple.com/documentation/combine/fail/debounce(for:scheduler:options:) 

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/combine/fail/throttle(for:scheduler:latest:) 

 

Apple Developer Documentation

 

developer.apple.com

 

Comments