Swift Throttle vs Debounce 완벽 가이드
1. 개념 요약
| 구분 |
Debounce |
Throttle |
| 동작 개념 |
“입력이 멈출 때까지 기다렸다가 실행” |
“일정 주기마다 한 번만 실행” |
| 핵심 목적 |
연속 입력 중복 방지 |
지속 입력 속도 제어 |
| 대표 상황 |
검색창 자동완성, 타이핑 이벤트 |
스크롤 이벤트, 센서 데이터, 위치 업데이트 |
2. 비유로 이해하기
| 상황 |
설명 |
| Debounce |
사용자가 타이핑을 멈출 때까지 기다렸다가 서버 검색 실행. (입력이 멈추면 한 번만) |
| Throttle |
초당 한 번만 서버에 위치 업데이트. (입력이 계속돼도 일정 간격으로만 실행) |
3. 타임라인 예시
🕒 Debounce
사용자 입력: a----b----c-----------[멈춤]---(발사)
실행 타이밍: ↑ (마지막 입력 후 N초 경과)
🕒 Throttle
이벤트 발생: |■■■■■■■■■■■■■■■■■■■■|
실행 타이밍: |■ ■ ■ ■ ■ |
(주기마다 한 번씩 실행)
4. Swift Combine 예제
✅ Debounce (검색창 자동완성 예시)
import Combine
import SwiftUI
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
func performSearch(_ text: String) {
guard !text.isEmpty else { return }
print("검색 실행: \(text)")
// 실제 API 요청 로직
}
}
✅ Throttle (실시간 위치 업데이트 예시)
import Combine
import CoreLocation
class LocationManager: NSObject, ObservableObject {
@Published var coordinate: CLLocationCoordinate2D?
private var cancellables = Set<AnyCancellable>()
private let subject = PassthroughSubject<CLLocationCoordinate2D, Never>()
override init() {
super.init()
subject
.throttle(for: .seconds(2), scheduler: RunLoop.main, latest: true)
.sink { [weak self] location in
self?.coordinate = location
print("위치 업데이트: \(location.latitude), \(location.longitude)")
}
.store(in: &cancellables)
}
func didUpdateLocation(_ loc: CLLocationCoordinate2D) {
subject.send(loc)
}
}
5. UIKit 예제
Debounce로 버튼 중복 클릭 방지
class ViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let buttonTap = PassthroughSubject<Void, Never>()
override func viewDidLoad() {
super.viewDidLoad()
buttonTap
.debounce(for: .milliseconds(800), scheduler: RunLoop.main)
.sink { [weak self] _ in
self?.doAction()
}
.store(in: &cancellables)
}
@IBAction func buttonPressed(_ sender: UIButton) {
buttonTap.send()
}
func doAction() {
print("버튼 실행 (디바운스)")
}
}
6. SwiftUI + Throttle (스크롤 감속 제어)
import SwiftUI
import Combine
struct ScrollThrottleView: View {
@State private var scrollPosition: CGFloat = 0
private let scrollSubject = PassthroughSubject<CGFloat, Never>()
@State private var cancellables = Set<AnyCancellable>()
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<200) { i in
Text("Item \(i)")
.font(.title3)
}
}
.background(GeometryReader { geo in
Color.clear
.onChange(of: geo.frame(in: .global).minY) { y in
scrollSubject.send(y)
}
})
}
.onAppear {
scrollSubject
.throttle(for: .milliseconds(300), scheduler: RunLoop.main, latest: true)
.sink { value in
scrollPosition = value
print("스크롤 감속 업데이트:", value)
}
.store(in: &cancellables)
}
}
}
7. Combine vs RxSwift 비교
| 구분 |
Combine |
RxSwift |
| Throttle |
.throttle(for:scheduler:latest:) |
.throttle(.milliseconds(n), latest: true, scheduler: MainScheduler.instance) |
| Debounce |
.debounce(for:scheduler:) |
.debounce(.milliseconds(n), scheduler: MainScheduler.instance) |
| 공통점 |
이벤트 폭주 제어, 백프레셔(Backpressure) 대응 |
이벤트 조절 연산자(Operators) |
| 차이점 |
Combine은 타입 안정성이 강함 |
RxSwift는 범용적 연산자 조합이 자유로움 |
8. 주의사항
| 상황 |
Debounce |
Throttle |
| 사용자가 계속 입력 중일 때 |
실행 안 됨 |
주기마다 실행 |
| 마지막 입력 이후 동작 |
마지막 이벤트 실행 |
마지막 주기 이벤트만 실행 |
| UI 연동 시 |
검색창, 텍스트 입력 |
스크롤, 제스처, 센서 데이터 |
| Combine에서 스케줄러 누락 시 |
메인 스레드 미갱신 |
UI 갱신 오류 발생 가능 |
9. 성능 및 UX 측면 정리
| 측면 |
Debounce |
Throttle |
| API 요청 부하 감소 |
매우 큼 |
중간 |
| UI 반응성 |
약간 늦음 |
일정함 |
| 정확도 |
마지막 상태 반영 |
일정 주기로만 반영 |
| 추천 사용 예시 |
검색, 자동완성, 텍스트 필터 |
위치, 스크롤, 센서 이벤트 |
10. 결론
- Debounce → “입력이 끝났을 때 한 번만”
- Throttle → “계속 입력돼도 주기마다 한 번만”
둘 다 이벤트 폭주(과도한 트리거) 를 방지하기 위한 반응형 프로그래밍의 필수 연산자입니다.