Debounce와 Throttle, 이벤트 중복 방지를 위한 필터링 방법
안녕하세요!
버튼을 빠르게 두 번 눌렀는데, 의도치 않게 두 번 실행된 경험이 있지 않나요?🥲
예를 들어, 결제 버튼을 여러 번 눌러 중복 결제가 발생하거나, API 요청이 짧은 시간 내에 여러 번 호출되는 경우가 있을 수 있습니다.
위처럼 예기치 못한 오류를 방지하거나 혹은 성능 상의 문제를 해결하기 위해 중복된 이벤트를 필터링해야 할 때가 있습니다.
이러한 필터링 기법 중 하나인 함수 호출 빈도를 줄이는 기법인 debounce와 throttle에 대해 알아보겠습니다.
1️⃣ debounce
debounce란 연속해서 호출되는 함수 중에서 마지막 호출만 실행되도록 하는 기법입니다.
이게 구현을 어떻게 하느냐 하면, 이벤트가 발생한 후 특정 시간의 타이머가 실행되어 다음 이벤트를 기다립니다.
새로운 이벤트가 발생하면 타이머를 초기화하고 새로운 이벤트가 발생하지 않고 타이머가 종료되면 그제서야 이벤트를 실행합니다. 즉, 이 말을 뒤집어보면 어떤 이벤트가 발생한 후 특정 시간이 지나기 전까지 실행되지 않습니다.
📌 동작 방식
- 특정 이벤트가 발생하면 타이머를 설정
- 만약 일정 시간(delay) 내에 동일한 이벤트가 다시 발생하면 타이머를 리셋하고 다시 대기
- 마지막 이벤트가 발생한 후 delay 시간이 지나면 실행
📌 사용 사례
- 검색창 자동완성: 사용자가 입력할 때마다 API 요청을 하면 불필요한 요청이 많아짐 → 입력이 끝난 후 nms 뒤에 실행
- 자동 저장: 사용자가 타이핑할 때마다 저장하지 않고, 입력이 끝나고 일정 시간 후에만 저장
- 창 크기 조절 (resize 이벤트): 사용자가 창 크기를 조절하는 동안 계속 실행되지 않고, 조절이 끝난 후 한 번 실행
간단한 Debounce 구현 코드입니다. queue를 지정하도록 하거나, workItem.cancel()등을 통해 세부적으로 제어할 수 있지만 기본 동작만 구현해 봤습니다.
import Foundation
class Debouncer {
private var workItem: DispatchWorkItem?
private let delay: TimeInterval
init(delay: TimeInterval) {
self.delay = delay
}
func run(action: @escaping () -> Void) {
workItem?.cancel()
workItem = DispatchWorkItem { action() }
if let workItem = workItem {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
}
Playground에서 간단한 사용 예를 만들어봤습니다.
let debouncer = Debouncer(delay: 0.5)
func search(query: String) {
debouncer.run {
print("🔍 검색 API 호출: \(query)")
}
}
// 예제 실행
search(query: "A")
search(query: "Ap")
search(query: "App")
// 0.5초 후 "🔍 검색 API 호출: App"만 실행됨 (이전 호출들은 취소됨)
2️⃣ throttle
throttle은 일정한 간격마다 함수가 한 번만 실행되도록 제한하는 기법입니다. debounce랑 비슷하게 보이지만 동작 원리는 조금 달라요.
이벤트가 발생하면 현재 시간을 기준으로 특정 시간의 타이머가 실행됩니다. 이 상태에서 지정된 시간 간격 동안은 추가적인 이벤트가 발생하더라도 실행하지 않습니다.
일정 시간이 지나고 타이머가 해제되면 다시 이벤트를 실행할 수 있습니다. 이벤트가 발생하면 타이머를 초기화하는 debounce와는 다른 용도로 사용할 수 있을 것 같네요.
📌 동작 방식
- 이벤트가 발생하면 현재 시간을 기록
- 다음 이벤트가 발생해도 일정 시간이 지나기 전까지는 실행하지 않음
- 일정 시간이 지나면 다시 실행 가능
📌 사용 사례:
- 스크롤 이벤트: 사용자가 페이지를 스크롤할 때 너무 많은 scroll 이벤트가 발생하는 것을 방지
- 버튼 연타 방지: 사용자가 버튼을 너무 빠르게 클릭하는 경우, 일정 간격으로만 클릭 이벤트 실행
- 마우스 이동 이벤트: 사용자의 마우스 움직임을 감지하는 경우, 너무 자주 실행되지 않도록 제한
간단한 throttle 구현 코드
import Foundation
class Throttler {
private var lastExecution: Date?
private let delay: TimeInterval
init(delay: TimeInterval) {
self.delay = delay
}
func run(action: @escaping () -> Void) {
let now = Date()
if let lastExecution = lastExecution, now.timeIntervalSince(lastExecution) < delay {
return
}
lastExecution = now
action()
}
}
Playground에서 간단한 사용 예를 만들어봤습니다.
let throttler = Throttler(delay: 1.0)
func buttonClicked() {
throttler.run {
print("✅ 버튼 클릭 실행됨!")
}
}
// 예제 실행
buttonClicked()
buttonClicked()
buttonClicked()
// 결과 첫 번째 함수만 실행됨 (이후 호출은 1.0초가 경과하지 않아 무시됨)
🎁 Wrap up
사용자가 마지막 입력을 마친 후 실행해야 한다면? → debounce
일정한 간격으로 실행해야 한다면? → throttle
👉 debounce는 사용자 입력과 같은 이벤트에 적합 (ex. 검색, 자동 저장)
👉 throttle은 빠르게 발생하는 이벤트를 조절하는 데 적합 (ex. 스크롤, 마우스 이동)