소개
먼저 Debounce, Throttle 이란 어떤 기능인지에 대해 알아보겠습니다.
debounce, throttle은 생소한 기능인데요 간단히 요약하면 이벤트의 반복 실행시 콜백 함수의 불필요한 실행을 줄이는 역할을 합니다. 이로 인해 클라이언트가 혜택을 볼 수도 있거나 혹은 서버 측에 불필요한 리퀘스트를 줄일 수도 있습니다.
Debounce
: 동일 이벤트가 반복적으로 시행되는 경우 마지막 이벤트가 실행되고 나서 일정 시간(밀리세컨드)동안 해당 이벤트가 다시 실행되지 않으면 해당 이벤트의 콜백 함수를 실행합니다.
Throttle
: 동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장합니다.
DispatchWorkItem
이라는 것을 이용해 Swift에서 외부 라이브러리 없이 이러한 기능을 적용하는 방법에 대해 알아보겠습니다.
DispatchWorkItem
DispatchWorkItem
이란 DispatchQueue
등 비동기 작업을 할 때 사용하는 클로저 함수를 클래스 형태로 한 번 더 캡슐화 한 것을 말합니다. 메인 스레드에서 비동기 작업을 실행할 때 자주 사용하는 아래와 같은 예제 코드를 보면 트레일링 클로저를 사용하여 비동기 작업을 실행하는 것을 알 수 있습니다.
여기서 트레일링 클로저 부분을 DispatchWorkItem
이라는 클래스로 한번 더 감싸면 동일한 작업을 할 수 있습니다.
위의 두 코드는 사실상 동일하다고 볼 수 있습니다.
cancel()
DispatchWorkItem
의 형태의 인스턴스는 여러 작업을 할 수 있는데, 그 중 cancel()
이라는 작업이 있습니다.
이 함수는 작업의 실행 여부에 따라 동작이 조금 달라집니다.
작업 실행 전: 즉 작업이 아직 큐에 있는 상황입니다. 이때
cancel()
을 호출하면 작업이 제거됩니다.작업 실행 중: 실행 중인 작업에
cancel()
을 호출하는 경우, 작업이 멈추지는 않고DispatchWorkItem
의 속성인inCancelled
가true
로 설정됩니다
예제 코드를 보겠습니다.
// 1 - 비동기 방식으로 실행 DispatchQueue.global(qos: .userInteractive).async(execute: workItem) // 2, 3 - 동기 방식으로 실행 workItem.perform() workItem.perform() // 여기까지 3번 실행됨 // 취소 workItem.cancel() // 4 workItem.perform() // 5 DispatchQueue.global(qos: .background).async(execute: workItem)
.perform()
은 워크 아이템을 동기 방식으로 실행하고자 할 때 사용합니다.
workItem
을 5회 실행하고자 의도했지만 cancel()
로 인해 실제로 실행된 횟수는 3회만 이루어집니다. cancel()
부분을 지우면 5회 전부 실행됩니다.
Debounce와 Throttle은 DispatchWorkItem
의 cancel()
작업을 이용해 구현합니다.
구현
Abstract Class 만들기
class DelayWork { typealias Handler = ((Date) -> Void) var workItem: DispatchWorkItem? let delay: Int! let handler: Handler? init(milliseconds delay: Int, handler: Handler?) { self.delay = delay self.handler = handler } func run() {} }
- handler
- Debounce 또는 Throttle을 시키고자 하는 클로저 함수입니다.
- delay: 시간 간격입니다.
- Debounce의 경우 마지막으로 실행되고 나서
delay
밀리초 후에 작업이 실행됩니다. - Throttle의 경우 몇 번을 반복해서 실행시키라도 실제 작업은
delay
초 간격으로 작업이 반복 실행됩니다.
- Debounce의 경우 마지막으로 실행되고 나서
- run()
- Debounce 또는 Throttle을 적용하여 함수를 실행하는 명령입니다.
- 두 부분이 다르게 구현되기 때문에 빈 괄호로 남겨두었습니다.
Debounce
와 Throttle
은 run()
함수를 빼면 똑같기 때문에 추상 클래스 비슷한 것을 만들어 상속시키겠습니다.
Debounce 구현
class Debounce: DelayWork { override func run() { self.workItem?.cancel() let workItem = DispatchWorkItem { [weak self] in self?.handler?(Date()) } self.workItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: workItem) } }
- self.workItem?.cancel()
Debounce
는 일단workItem
이 있다면 무조건 취소부터 시킵니다.- 코드 하단을 보면
asyncAfter
를 통해deadline
이 지나면 (delay
밀리초 후에) 작업을 실행하라고 했는데 데드라인이 지나기 이전에debounce
명령이 실행이 되면 안되기 때문에 바로 취소하는 것입니다.
- let workItem
- 단순히
handler
를 실행시키는 워크 아이템입니다.
- 단순히
- self.workItem = workItem
- cancel된
self.workItem
은 다시 사용할 수 없으므로 재할당합니다.
- cancel된
- DispatchQueue…asyncAfter…
- 데드라인이 지나면
workItem
의 작업을 실행하라는 명령입니다. - 즉,
handler
를 실행하라는 명령과 동일한 의미입니다.
- 데드라인이 지나면
Throttle 구현
class Throttle: DelayWork { override func run() { if self.workItem == nil { handler?(Date()) let workItem = DispatchWorkItem { [weak self] in self?.workItem?.cancel() self?.workItem = nil } self.workItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: workItem) } } }
- let workItem
- 멤버 변수의
self.workItem
을 취소시키고nil
로 만듭니다.
- 멤버 변수의
- DispatchQueue…asyncAfter…
- 데드라인이 지나면
workItem
의 작업을 실행하라는 명령입니다. - 다시 말하면 데드라인이 지나면
self.workItem
을 취소시키고nil
로 만들라는 의미입니다.
- 데드라인이 지나면
- self.workItem == nil인 경우에만 실행
- 위에서
DispatchQueue...asyncAfter...
부분이 실행된 경우에만if
문이true
가 됩니다. self.workItem
이nil
이 아닌 경우는 데드라인이 지날 때까지workItem
을 실행하도록 대기하고 있는 경우이므로if
문이 실행되지 않습니디.
- 위에서
- handler?(Date())
if
문이 실행된 경우,handler
를 바로 실행시킵니다.
- self.workItem = workItem
cancel
된self.workItem
은 다시 사용할 수 없으므로 재할당합니다.
예제: 뷰 컨트롤러에 적용
버튼을 빠르게 반복 클릭하면 Debounce 및 Throttle 이 적용되는 예제입니다.
import UIKit class DelayWorkViewController: UIViewController { @IBOutlet weak var txvDebounce: UITextView! @IBOutlet weak var txvThrottle: UITextView! private var debounce: Debounce! private var throttle: Throttle! override func viewDidLoad() { super.viewDidLoad() txvDebounce.text = "" txvThrottle.text = "" debounce = Debounce(milliseconds: 1000) { [unowned self] date in txvDebounce.text += "DEBOUNCE: \(date)\n" } throttle = Throttle(milliseconds: 2000) { [unowned self] date in txvThrottle.text += "THROTTLE: \(date)\n" } } @IBAction func btnActClick(_ sender: UIButton) { debounce.run() throttle.run() } }
전체 코드
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class Debounce: DelayWork { | |
override func run() { | |
self.workItem?.cancel() | |
let workItem = DispatchWorkItem { [weak self] in | |
self?.handler?(Date()) | |
} | |
self.workItem = workItem | |
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: workItem) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class DelayWork { | |
typealias Handler = ((Date) -> Void) | |
var workItem: DispatchWorkItem? | |
let delay: Int! | |
let handler: Handler? | |
init(milliseconds delay: Int, handler: Handler?) { | |
self.delay = delay | |
self.handler = handler | |
} | |
func run() {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class Throttle: DelayWork { | |
override func run() { | |
if self.workItem == nil { | |
handler?(Date()) | |
let workItem = DispatchWorkItem { [weak self] in | |
self?.workItem?.cancel() | |
self?.workItem = nil | |
} | |
self.workItem = workItem | |
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: workItem) | |
} | |
} | |
} |
0개의 댓글