티스토리 뷰
iOS Task 비동기 활용, custom throttle event 구현방법 (Combine 미사용)
applebuddy 2025. 5. 20. 02:29
안녕하세요, 개발자 멍구입니다! 🍀
오늘은 iOS 앱에서 자주 필요한 기능 중 하나인 "버튼 중복 클릭 방지", 바로 throttle 기능을 Swift 코드로 직접 구현해본 경험을 공유해보려 해요.
보통 throttle은 Combine에서 제공되지만, Combine 없이도 구현 가능하다면?
코드를 직접 다루며 개념을 이해하면, 좋을 것 같아서 공부 겸 Task 비동기 API를 이용해서 구현해봤어요.
오늘은 Task와 @MainActor, 그리고 async/await 기반으로 Throttler 클래스를 직접 만들어보며, 어떤 원리로 동작하는지 친절하게 풀어드릴게요.
throttle이란? 간단 개념 정리
throttle은 지정된 시간 간격 안에서는 한 번만 이벤트를 허용하고, 그 외에는 무시하는 제어 방식이에요. 대표적인 사용 예시는 이런 것들이 있어요:
- 버튼 연타 방지
- 빠르게 반복 호출되는 API 요청 차단
- 입력 폼에서 debounce와 함께 입맛따라 활용
오늘 구현할 throttle은 딱 한 가지 목적!
“버튼을 여러 번 눌러도, 일정 시간(예: 3초) 안에서는 한 번만 동작하게 만들기” 입니다.
직접 만든 Throttler 클래스의 구조
이번 예제의 핵심은 이 Throttler 클래스입니다:
final class Throttler {
enum ThrottleError: Error {
case timeout
}
private var timeCheckTask: Task<Void, Error>?
private let delayInterval: TimeInterval
}
- delayInterval은 throttle 간격 (ex. 3초)을 의미합니다.
- timeCheckTask는 이벤트가 발생한 뒤 일정 시간 동안 추가 이벤트가 발생하지 않도록 막는 역할을 해요.
- 시간이 지나면 ThrottleError.timeout이 발생하게 설계되어 있어요.
왜 Task를 쓰나요?
Task를 사용하면 async/await 문법 안에서 delay 처리를 유연하게 할 수 있어요. 기존의 DispatchQueue보다도 더 Swift스럽게 처리할 수 있어요. async await 방식으로 더욱 깔끔하고 동기코드처럼 작성 가능하게 해주는 놈입니다. 뿐만 아니라 에러처리도 되고, 도중에 취소하기에도 간편해요.
Throttler runEvent 함수 설명
이제 본격적인 throttle 실행 로직입니다:
@MainActor
func runEvent(_ handler: @escaping () -> Void) async
여기서 @MainActor는 매우 중요한데요, UI 이벤트 처리나 스레드 안전성을 고려할 때, 메인 스레드에서만 실행되도록 보장해줍니다.
그리고 struct, enum, actor와 다르게 class는 기본적으로 값을 안전하게 전달할 수 있게 해주는 `Sendable`을 따르지 않기 때문에 Task에서 해당 인스턴스를 캡처하려 할 때 컴파일 타임 에러를 발생시킬 수 있습니다.
하지만 해당 클래스나 내부 메서드에 `@MainActor`를 명시하면, 항상 메인 스레드에서 실행된다는 제약 덕분에 컴파일러는 이를 안전하다고 판단하게 되어 `Sendable` 문제를 회피할 수 있습니다.
아래는 핵심 동작 순서입니다:
- timeCheckTask가 아직 살아 있고 취소되지 않았다면 → 현재 이벤트는 무시 (18~21행)
- timeCheckTask가 없거나 이미 끝났다면 → 새로운 Task를 만들고, 이벤트를 실행할 준비 (22~26행)
- Task는 delayInterval만큼 대기 후 timeout 에러를 던짐 (23~25행)
- 이 에러를 catch했을때 task를 cancel하고 다시 이벤트를 받을 준비를 함 (33~36행)
이 구조를 통해 이벤트 → 기다림 → 이벤트 차단 해제라는 흐름이 깔끔하게 구현돼요.
throttler 실행으로 전체 실행 흐름 보기
실제 사용할 때는 아래처럼 간단하게 호출할 수 있습니다:
let throttler = Throttler(delayInterval: 3.0)
Task {
await throttler.runEvent {
print("🔥 첫 번째 이벤트 실행!")
}
}
이후 시간 간격을 달리해 이벤트를 실행해볼게요:
Task {
try await Task.sleep(for: .seconds(2.5))
await throttler.runEvent {
print("🔥 두 번째 이벤트 실행!")
}
}
Task {
try await Task.sleep(for: .seconds(3.5))
await throttler.runEvent {
print("🔥 세 번째 이벤트 실행!")
}
}
Throttle event 예상 결과는?
🤔 throttle event ready at (현재 시각)
🔥 첫 번째 이벤트 실행!
🧱 event blocked : (2.5초 시점)
🫠 cancelled task at error : timeout
🔥 세 번째 이벤트 실행!
- 첫 번째 이벤트는 바로 실행
- 2.5초 후의 두번째 이벤트는 무시됨 (3초가 안 지남)
- 3.5초가 지난 후 실행된 세번째 이벤트는 성공
이렇게 중복 호출, 중복 이벤트를 방지해주는 throttle 이벤트를 간이로 만들어봤습니다.
마무리하며
Swift의 Task, @MainActor, async/await만으로도 throttle 기능을 꽤 심플하게 구현할 수 있답니다. Combine 없이도 가능하다는 점에서, 더 유연한 커스터마이징이 필요할 때 매우 유용하겠죠?
👉🏻 UI에 민감한 동작을 다루고 있다면, throttle은 필수입니다!
👉🏻 직접 구현해보며 개념까지 익혀두면 실무에서 훨씬 쉽게 활용할 수 있어요.
다음에는 비슷한 개념의 debounce도 구현해보는 글로 찾아뵐게요 🙂 궁금한 점이나 피드백은 언제든지 환영입니다!
아래 Throttler 전체 코드도 남길게요. 많은 고려없이 후다닥 만들어서 부족한게 많을테니, 피드백 부탁드려요.
import Foundation
final class Throttler {
/// 지정된 time interval이 지난 경우, 발생할 에러
enum ThrottleError: Error {
case timeout
}
private var timeCheckTask: Task<Void, Error>?
private let delayInterval: TimeInterval
init(delayInterval: TimeInterval) {
self.delayInterval = delayInterval
}
@MainActor
func runEvent(_ handler: @escaping () -> Void) async {
if let timeCheckTask,
!timeCheckTask.isCancelled {
debugPrint("🧱 event blocked : at \(Date())")
return
} else {
self.timeCheckTask = Task<Void, Error> {
try await Task.sleep(for: .seconds(delayInterval))
throw ThrottleError.timeout
}
}
do {
handler()
try await timeCheckTask?.value
} catch {
timeCheckTask?.cancel()
debugPrint("🫠 cancelled task at error : \(error) at \(Date())")
}
}
}
let throttler = Throttler(delayInterval: 3.0)
print("🤔 throttle event ready at \(Date())")
Task {
await throttler.runEvent {
print("🔥 fisrt event fired at \(Date())")
}
}
Task {
try await Task.sleep(for: .seconds(2.5))
await throttler.runEvent {
print("🔥 second event fired at \(Date())")
}
}
Task {
try await Task.sleep(for: .seconds(3.5))
await throttler.runEvent {
print("🔥 third event fired at \(Date())")
}
}
'iOS 개발 > iOS 개발 팁' 카테고리의 다른 글
iOS 중복 이벤트 방지 방법, Timer와 closure로 throttle event 구현방법 (0) | 2025.05.23 |
---|---|
iOS 개발 팁, Timer로 간단하게 Debounce 구현하는 방법 (0) | 2025.05.22 |
iOS Swift Concurrency, Task Closure 활용 Debouncer 구현방법 (0) | 2025.05.17 |
개발한 사이드프로젝트 서비스를 무료로 홍보하는 방법들 (0) | 2025.01.23 |
개발자로서 공부한 내용을 기록하고 공유, 증명하는 방법 (2) | 2025.01.05 |
- Total
- Today
- Yesterday
- Protocol
- Collection
- 개발자문서
- swift
- swift언어
- 프로그래머스
- 프로그래머스swift
- uikit
- 백준알고리즘
- SwiftUI
- 알고리즘
- swift 문자열
- createML
- swift알고리즘
- swift문제
- swift 기초
- 프로토콜
- swift concurrency
- Swift 알고리즘
- 김프매매
- 자연어처리
- 백준swift
- swift string
- ios
- 부스트코스
- swift reduce
- 스위프트
- CoreML
- 알고리즘문제
- 컬렉션
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |