티스토리 뷰

반응형

 

안녕하세요~ 개발자 멍구입니다. ✨

저는 현업에서 개발을 하면서 비동기적으로 중복 호출을 방지하고, 예외처리 하는등의 다소 복잡한 로직을 구현해본적이 있는데요. 그렇게 자주 사용하지는 않다보니, 한번씩 리마인딩을 하지 않으면 얼마 안가서 까먹게 되더라고요... ㅠㅠ

근데 사용할일이 생기면 굉장히 중요하고, 많은 테스트가 필요한 작업이다보니, 리마인딩의 필요성을 느꼈습니다.. 

그래서 이 리마인딩 공부 겸... Swift Concurrency의 Task를 활용해서 Debouncer를 구현해봤습니다. 초기구현이라 부족한 점 많을 수 있으니, 감안해서 보고 많은 피드백 부탁드립니다! 

 


근데 그전에... Debounce가 머임?

debounce는 일정 시간 동안 특정 이벤트가 다수 발생했을때, 마지막 하나의 이벤트만 처리할 수 있는데요.

1초의 지정시간이 되어있는 debounce 이벤트 예시를 들자면, 유저가 검색을 위해서 글자를 입력하고, 입력이 끝난 뒤 일정 시간(1초)이 지나면 그때서야 1회 입력 텍스트에 대한 검색 API 호출을 할 수 있습니다.

불필요한 API 호출을 방지할 수 있어서 활용하면 상황에 따라 매우 유용합니다.

그럼 바로 Debouncer 구현한거 보겠습니다.

 


클래스로 Debouncer 구현하기 가보자구

Debouncer를 class로 구현했습니다. 사실 Swift Concurrency에서 안전하게 값을 전달하는 Sendable 조건을 충족하려면 구조체나, 열거형을 사용해야할 수 있지만, 이번에는 MainActor 기반의 비동기 동작만 있기에 이부분은 깊게 고려하지는 않았어요.

DebounceError도 정의했는데요. 특정 interval이 지났을때 발생하는 error로 특정 time interval 이 지나면 trigger할 시점을 체크하기 위해 사용합니다.

timeCheckTask 는 Void 타입의 결과 혹은 Error를 던질 수 있습니다.

delayInterval은 debouncer의 trigger 시간을 지정합니다. 그리고 초기화해줘요.

 


Debouncer 핵심로직, debounce event trigger method 구현하기

runDebounceEvent 메서드에 일정 시간동안 이벤트가 발생하지 않을 경우, 실행된 동작을 정의한 클로져를 넘겨줍니다. 그렇게 되면 기존의 timeCheckTask는 취소시키고, 다시 time interval을 체크할 Task를 정의 및 실행해줍니다.

그렇게 하고, timeCheckTask?.value 를 do 블럭에서 실행해주는데요. 여기가 핵심 로직입니다.

1) timeout error가 발생하면, 마지막 이벤트 이후 지정한 time interval동안 이벤트가 발생하지 않았기에 지정한 handler closure 동작을 실행합니다.
2) 그 외 CancellationError 등, task가 취소된 경우에는 그에 맞는 처리를 해줍니다. (현재 이부분을 고려해서 별도로 처리하는 동작은 없네요.)

@MainActor로 명시되어있는 메인스레드 내 동작 메서드이기 때문에 클래스 내에서 별도로 고려해야하는 Sendable 조건은 고려할 필요가 없었네요. 이부분은 좀 더 공부해봐야할 부분입니다.

 


내가 만든 간이 Debouncer 실행 확인하는 방법

그렇게 후다닥 만든 간이? Debouncer.... 인스턴스를 생성해주고요. debounce event를 하나하나 실행해봅니다. 3초의 지정시간을 가진 Debouncer로 첫번째 이벤트, 2.5초 뒤 두번째 이벤트 발생! 실제 출력은 어떻게 될까요?

 


간이 Debouncer 실행 debug 결과

2.5초 뒤에 이벤트가 한번 더 발생해버렸기 때문에 3초를 기다리던 첫번째 동작은 무시되고, 두번째 동작만 실행된 것을 볼 수 있어요. 대략 5.5초 이후에 출력이 발생하게 됩니다.

 

 


오늘은 중복호출 방지에 사용될 수 있는 Debouncer를 iOS Swift Concurrency, Task를 활용해서 만들고, 사용해봤는데요. Throttle, Debounce의 경우 UI동작과 함께 사용되는 경우가 많아서 메인스레드에서 안전하게 동작할 수 있는지, 동작 간에 프리징이 발생 할 수 있는지 등등... 고려해가면서 구현하고 사용해봐야겠습니다.

사실 Task를 사용하는 대신 타이머나 Combine publisher 등등을 사용할 수도 있겠는데요. 평소 사용하지 않는 Task 개념을 활용해봤습니다. 좀 더 공부해보고 개선할 부분이나 발생이 떠오르면 남겨볼게요. 많은 피드백 부탁드립니다. 아래 코드도 남길게요. 감사합니다~ ☺️

 

Debouncer swift 코드 (playground 기준 작성)

import Foundation

final class Debouncer {
    /// 지정된 time interval이 지난 경우, 발생할 에러
    enum DebounceError: Error {
        case timeout
    }

    private var timeCheckTask: Task<Void, Error>?
    private let delayInterval: TimeInterval

    init(delayInterval: TimeInterval) {
        self.delayInterval = delayInterval
    }

    @MainActor
    func runDebounceEvent(_ handler: @escaping () -> Void) async {
        timeCheckTask?.cancel()
        timeCheckTask = Task<Void, Error> {
            try await Task.sleep(for: .seconds(delayInterval))

            try Task.checkCancellation()
            throw DebounceError.timeout
        }

        do {
            try await timeCheckTask?.value
        } catch {
            debugPrint("🫠 error : \(error) at \(Date())")
            switch error as? DebounceError {
            case .timeout:
                // - 지정한 interval 내에 새로운 이벤트가 없으면 event trigger
                handler()

            default:
                // CancellationError throw 시
                // - 지정한 interval 내에 새로운 이벤트가 발생 시
                return
            }
        }
    }
}

let debouncer = Debouncer(delayInterval: 3.0)

print("🤔 Debounced event ready at \(Date())")

Task {
    await debouncer.runDebounceEvent {
        print("🔥 fisrt Debounced event fired at \(Date())")
    }
}

Task {
    try await Task.sleep(for: .seconds(2.5))

    await debouncer.runDebounceEvent {
        print("🔥 second Debounced event fired at \(Date())")
    }
}
반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함