iOS/SwiftUI

[iOS/SwiftUI] Debounce 사용해보기

yanni13 2024. 8. 25. 18:57

 

 

오늘은 디바운스에 대해 알아보자!

 

💡Debounce vs throttle

Debounce : 일정시간 내에 이벤트를 실행했을 때 마지막(또는 첫 번째) 이벤트만 실행되도록 하여 일정시간이 지난 후에 이벤트를 트리거 시킨다.

 

Throttle : 지정된 시간 간격으로 구독한 최근 값(마지막 이벤트)을 publish한다. 입력 후 바로 입력된 다음 대기 상태로 넘어간다.

 

 

 

디바운스에 대해 자세히보면 combine에 속한 메서드이다. 그래서 꼭 combine을 import 해줘야 하고, for dueTime과 scheduler는 debounce를 사용할 때 필수로 지정해줘야 하는 값이기 때문에 잘 알아보고 넘어가자!

 

  • for dueTime: 실행시킬 이벤트 시간
  • scheduler: publisher가 요소를 어디에 전달할껀지 

 

 

나는 사용자가 특정 버튼을 여러번 눌렀을 때 그 이벤트를 다루기 위해서 디바운스를 사용했다!

 

구현코드

import Combine
import SwiftUI

class InquiryViewModel: ObservableObject {
...

    var cancellables = Set<AnyCancellable>()
    let debounceInterval = 0.3
    var debounceTimer = PassthroughSubject<Void, Never>()
    
    var dismissAction: (() -> Void)?
    
    init() {
        debounceTimer
            .debounce(for: .seconds(debounceInterval), scheduler: RunLoop.main)
            .sink { [weak self] in
                self?.sendInquiryMailApi { success in
                    if success {
                        self?.dismissAction?()
                        Log.debug("디바운싱 문의하기 마지막 이벤트 보냄")
                    } else {
                        Log.debug("문의하기 디바운싱 실패")
                    }
                }
            }
            .store(in: &cancellables)
    }

 

앞에서 말했듯이 debounce를 사용하려면 combine을 꼭 import 해줘야 한다. cancellables를 통해 publisher에 대한 구독을 저장하고 있기 때문에!!! 

 


나는 문의하기 api를 호출하는 버튼을 클릭했을때 디바운싱이 실행되게 구현하였고, viewModel내부에서 초기화를 시켜주었다.

 

.debounce(for: .seconds(debounceInterval), scheduler: RunLoop.main)

 

위 코드를 통해 0.3초동안의 이벤트 지연시간을 지정해 주었고, 이 지정시간이 끝나면 가장 최근의 이벤트 한 개만 발생시켜 준다.

그리고 메인스레드에서 publisher 요소를 전달하겠다는 의미로 RunLoop.main을 사용해 주었다.

(보통 UI관련은 메인스레드에서 작업한다고 한다!)

 

그래서 다른 블로그들을 참고해도 디바운싱이나 쓰트롤링을 처리하려면 텍스트필드나 버튼이랑 상호작용이 돼야 되기 때문에 다 메인으로 되어 있을 것이다!

 

sink

sink는 Combine에서 퍼블리셔의 값을 구독하는 방법 중 하나이다! (더 자세하게 들어가면 combine 포스팅이 될 거 같아서 생략)

 

sink { ... } 내부로직으로 인해 디바운싱된 publisher에 구독하도록 한다. 그래서 클로저 내부에서 문의하기 api를 호출시키고, 성공했다면 

지정한 액션을 수행하도록 설계하였다.

 

store

store는 sink를 통해 받은 구독을 cancellables 저장하여 참조를 유지한다.

store(in: &cancellables)을 통해서 viewModel이 살아있는 동안에 구독이 유지되도록 한다.

 

이쯤 되면 한 가지 의문이 들 수도 있다! cancellables에 굳이 저장 안 해도 되지 않나?? 하는 생각이 들 수도 있다

안타깝게도 combine을 사용하는데 cancellables에 구독을 보관/저장하지 않으면 즉시 취소되어 디바운싱이 작동하지 않을 수 있다!!

이외에도 combine을 사용하려면 거의 모든 메서드들이 store를 통해 받은 구독을 저장해줘야 한다.

 

이해가 안 간다면 combine의 기본 동작원리 정도 공부하고 다시 이 블로그를 본다면 무슨 말인지 이해할 것이다

https://yanni13.tistory.com/16

 

[swift] Combine이랑 ObservableObject 예제로 파헤치기

combine과 observableobject를 사용하여 간단하게 구현해 보고 비교해보려 한다. 2024.03.15 - [Swift] - [swift] Combine & ObservableObject [swift] Combine & ObservableObject 오늘은 swiftUI에서 꼭 알아야 하는 Combine랑 Observabl

yanni13.tistory.com

 

 

 

CustomBottomButton(action: {
    	continueButtonAction()
}, label: "문의하기", isFormValid: $viewModel.isFormValid)

...

    private func continueButtonAction() {
        if viewModel.isFormValid {
            viewModel.dismissAction = {
                self.presentationMode.wrappedValue.dismiss()
            }
            
            // 디바운스 타이머 트리거
            viewModel.debounceTimer.send(())
        }
    }

 

다시 코드로 돌아와서, 폼의 상태가 모두 유효하다면 디바운스 타이머 트리거를 실행시키는 viewModel.debounceTimer.send()

를 통해서 지정된 초로 디바운스 타이머를 트리거 시켜주고 , viewModel.dismissAction을 통해 문의하기 api요청에 성공했을 경우 현재 창을 닫도록 구현하였다!

 

viewModel.debounceTimer.send()

  • PassthroughSubject를 사용하여 외부에서 이벤트를 방출하는 메서드
  • send()는 사용자의 입력을 받아 디바운싱된 동작을 실행하기 위해 사용한다!
  • 그래서 지정된 초에 몇 번을 누르던지 간에 debounce(for:scheduler:)에 의해서 마지막 이벤트 하나를 보내게 된다!

 

 

마무리

내가 디바운스를 사용한 이유는 api를 호출할 때 돌아오는 응답이 늦기 때문에 사용자가 버튼을 여러 번 클릭했을 경우 일정시간이 지난 후에 트리거 되도록 하는 디바운스를 사용했다.

 

하지만 다른 자료들을 더 찾아보니 보통 텍스트 필드에 디바운스를 사용하고 버튼 중복 이벤트에 대해서는 쓰로틀링을 사용하더라!! 

 

어떻게 이벤트를 담아두고 있다가 어느 시점에서 트리거 해줄지에 대한 차이점만 다를 뿐 대기하고 있다가 일정 값을 전달하는 방법에는 일치하기 때문에 무엇을 사용하던지 정답은 없다고 생각한다