소개

먼저 Debounce, Throttle 이란 어떤 기능인지에 대해 알아보겠습니다.

debounce, throttle은 생소한 기능인데요 간단히 요약하면 이벤트의 반복 실행시 콜백 함수의 불필요한 실행을 줄이는 역할을 합니다. 이로 인해 클라이언트가 혜택을 볼 수도 있거나 혹은 서버 측에 불필요한 리퀘스트를 줄일 수도 있습니다.

 

Debounce: 동일 이벤트가 반복적으로 시행되는 경우 마지막 이벤트가 실행되고 나서 일정 시간(밀리세컨드)동안 해당 이벤트가 다시 실행되지 않으면 해당 이벤트의 콜백 함수를 실행합니다.

Debounce GIF - Find & Share on GIPHY

 

Throttle: 동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장합니다.

Lodash GIF - Find & Share on GIPHY

 

DispatchWorkItem 이라는 것을 이용해 Swift에서 외부 라이브러리 없이 이러한 기능을 적용하는 방법에 대해 알아보겠습니다.

 

DispatchWorkItem

DispatchWorkItem이란 DispatchQueue 등 비동기 작업을 할 때 사용하는 클로저 함수를 클래스 형태로 한 번 더 캡슐화 한 것을 말합니다. 메인 스레드에서 비동기 작업을 실행할 때 자주 사용하는 아래와 같은 예제 코드를 보면 트레일링 클로저를 사용하여 비동기 작업을 실행하는 것을 알 수 있습니다.

여기서 트레일링 클로저 부분을 DispatchWorkItem이라는 클래스로 한번 더 감싸면 동일한 작업을 할 수 있습니다.

위의 두 코드는 사실상 동일하다고 볼 수 있습니다.

 

cancel()

DispatchWorkItem의 형태의 인스턴스는 여러 작업을 할 수 있는데, 그 중 cancel()이라는 작업이 있습니다.

이 함수는 작업의 실행 여부에 따라 동작이 조금 달라집니다.

작업 실행 전: 즉 작업이 아직 큐에 있는 상황입니다. 이때 cancel() 을 호출하면 작업이 제거됩니다.

작업 실행 중: 실행 중인 작업에 cancel()을 호출하는 경우, 작업이 멈추지는 않고 DispatchWorkItem 의 속성인 inCancelledtrue 로 설정됩니다

[iOS] 차근차근 시작하는 GCD — 9

 

예제 코드를 보겠습니다.

// 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 초 간격으로 작업이 반복 실행됩니다.
  • run()
    • Debounce 또는 Throttle을 적용하여 함수를 실행하는 명령입니다.
    • 두 부분이 다르게 구현되기 때문에 빈 괄호로 남겨두었습니다.

DebounceThrottlerun() 함수를 빼면 똑같기 때문에 추상 클래스 비슷한 것을 만들어 상속시키겠습니다.

 

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은 다시 사용할 수 없으므로 재할당합니다.
  • 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.workItemnil이 아닌 경우는 데드라인이 지날 때까지 workItem을 실행하도록 대기하고 있는 경우이므로 if문이 실행되지 않습니디.
  • handler?(Date())
    • if문이 실행된 경우, handler를 바로 실행시킵니다.
  • self.workItem = workItem
    • cancelself.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()
    }
}

 

Animated GIF - Find & Share on GIPHY

전체 코드


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)
}
}

view raw

Debounce.swift

hosted with ❤ by GitHub


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() {}
}

view raw

DelayWork.swift

hosted with ❤ by GitHub


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)
}
}
}

view raw

Throttle.swift

hosted with ❤ by GitHub

 

참고

문의 | 코멘트 또는 yoonbumtae@gmail.com


카테고리: Swift


0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다