Combine #6: 시간 조작 연산자
Source: Dev.to
시간 지연
delay(for:tolerance:scheduler:options:)는 입력 이벤트 흐름을 받아 interval 매개변수( SchedulerTimeType.Stride 타입)로 지정된 시간만큼 저장한 뒤, 지정된 **scheduler**에서 하나씩 다시 방출합니다. 이때 방출 간격은 원래 수신된 시간 간격과 동일하게 유지됩니다.
// Publisher que será alimentado por el Timer.
let sourcePublisher = PassthroughSubject<Int, Never>()
// Publisher retrasado (delay).
// Se especifica el retraso con .seconds() y se emiten los valores
// en el scheduler .main para mostrarlos en pantalla.
let delayedPublisher = sourcePublisher.delay(
for: .seconds(delayInSeconds),
scheduler: DispatchQueue.main
)
// Timer que publica valores en el RunLoop.main.
Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect() // Inicia el timer inmediatamente.
.subscribe(sourcePublisher) // Conecta el timer al publisher de origen.
.store(in: &subscriptions)
이전 예제에서는 Foundation의
TimerPublisher버전을 사용했습니다.
Timer.TimerPublisher인스턴스는ConnectablePublisher프로토콜을 채택하고 있어,.connect()메서드를 호출한 뒤에만 요소를 방출합니다.
autoconnect()연산자는 첫 번째 구독자가 구독할 때 자동으로connect()를 호출합니다.
Source: …
값 누적
collect(_:options:)는 입력 이벤트 스트림을 받아 strategy 매개변수(Publishers.TimeGroupingStrategy)에 정의된 시간 동안 저장한 뒤, 지정된 **scheduler**에서 수집된 이벤트들을 배열로 방출합니다.
// Timer가 값을 방출할 퍼블리셔를 생성합니다.
let sourcePublisher = PassthroughSubject<Int, Never>()
// 수집 퍼블리셔를 생성합니다 (collect).
// • 수집 창을 지정합니다 (.byTime 또는 .byTimeOrCount)
// • 값을 메인 스케줄러(.main)에서 방출해 화면에 표시합니다
let collectedPublisher = sourcePublisher
.collect(
.byTime(DispatchQueue.main, .seconds(collectTimeStride))
)
// RunLoop.main에서 값을 방출하는 타이머를 생성합니다
Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect()
.subscribe(sourcePublisher)
.store(in: &subscriptions)
그룹화 전략
Publishers.TimeGroupingStrategy는 다음과 같이 사용할 수 있습니다:
| 전략 | 설명 |
|---|---|
.byTime | 시간만을 기준으로 그룹화합니다. |
.byTimeOrCount | 시간 또는 최대 이벤트 수를 기준으로 그룹화합니다. |
**.byTimeOrCount**의 경우, 지정된 시간보다 먼저 최대 이벤트 수에 도달하면 배열이 즉시 방출됩니다.
let collectTimeStride = 4 // 시간 창 길이(초)
let collectMaxCount = 2 // 창당 최대 이벤트 수
let collectedPublisher = sourcePublisher
.collect(
.byTimeOrCount(
DispatchQueue.main,
.seconds(collectTimeStride),
collectMaxCount
)
)
위 예시에서는 시간 임계값 4 초 또는 이벤트 수 임계값 2개 중 먼저 도달하는 쪽에 따라 수집된 이벤트 배열이 방출됩니다.
Source: https://developer.apple.com/documentation/combine/publisher/debounce(for:scheduler:options:)
이벤트 무시하기
debounce(for:scheduler:options:)
debounce(for:scheduler:options:) 은 입력 퍼블리셔가 마지막으로 방출한 요소 이후에 dueTime( SchedulerTimeType.Stride ) 파라미터로 정의된 시간을 기다렸다가, 그 마지막 값을 다시 방출합니다.
중요: 입력 퍼블리셔가
debounce의 대기 시간이 만료되기 전에 완료 이벤트를 전송하면, 연산자는 해당 이벤트를 다시 방출할 수 없습니다.
import Combine
let subject = PassthroughSubject<Int, Never>()
var subscriptions = Set<AnyCancellable>()
// 1 초 윈도우로 디바운스, DispatchQueue.main에서 재방출
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
.share() // 단일 구독 지점
// 이벤트를 출력하기 위한 구독
debounced
.sink { value in
print("Debounced value: \(value)")
}
.store(in: &subscriptions)
위 예제에서는 share() 를 사용해 여러 구독자가 디바운스 결과를 동일하게 받도록 하면서 연산자 인스턴스를 중복 생성하지 않도록 했습니다.
throttle(for:scheduler:latest:)
throttle(for:scheduler:latest:) 도 입력 스트림에서 발생하는 이벤트 수를 줄이지만, 동작 방식은 debounce 와 다릅니다:
| 특성 | debounce | throttle |
|---|---|---|
| 윈도우 | 마지막 이벤트 후에 열림 | 첫 번째 이벤트 후에 열림 |
| 방출 | 윈도우 내 마지막 이벤트만 | 윈도우 내 첫 번째(latest = false) 혹은 마지막(latest = true) 이벤트 |
| 첫 값 | 윈도우가 끝날 때만 방출 | 첫 이벤트를 받자마자 즉시 방출 |
import Combine
let subject = PassthroughSubject<Int, Never>()
var subscriptions = Set<AnyCancellable>()
// 1 초 윈도우로 스로틀, 각 윈도우의 마지막 이벤트 방출
let throttled = subject
.throttle(for: .seconds(1.0), scheduler: DispatchQueue.main, latest: true)
throttled
.sink { value in
print("Throttled value: \(value)")
}
.store(in: &subscriptions)
latest: false로 설정하면 1 초 간격마다 첫 번째 값을 얻습니다.latest: true로 설정하면 같은 간격 내에 들어온 마지막 값을 얻습니다.
스로틀
let throttled = subject
.throttle(for: .seconds(throttleDelay),
scheduler: DispatchQueue.main,
latest: true)
// Se crea una suscripción sobre `throttled` para imprimir los eventos
throttled
.sink { … }
.store(in: &subscriptions)
타임아웃
timeout(_:scheduler:options:customError:)는 입력 스트림(upstream)이 interval로 정의된 시간을 초과하여 이벤트를 방출하지 않을 경우 종료 이벤트(success 또는 failure)를 발행합니다.
customError가nil이면 성공 종료 이벤트가 발행됩니다.- 그렇지 않으면 해당 클로저에서 정의된 오류가 발행됩니다.
enum TimeoutError: Swift.Error {
case timedOut
}
// Se crea un subject para emitir eventos.
let subject = PassthroughSubject<String, Never>()
// Se aplica el operador `timeout` para emitir un evento de fin (en este caso,
// un error `.timedOut`) si no se recibe ningún evento en 5 segundos.
let timeoutSubject = subject
.timeout(.seconds(5), scheduler: DispatchQueue.main) {
TimeoutError.timedOut
}
시간 측정
measureInterval(using:options:) 은 입력 스트림에서 수신된 두 이벤트 사이의 시간을 측정하고 방출합니다.
이 Publisher가 방출하는 값의 타입은 매개변수로 전달된 Scheduler의 TimeInterval 입니다:
| Scheduler | 측정 단위 |
|---|---|
DispatchQueue | 나노초 |
RunLoop | 초 |
cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.measureInterval(using: RunLoop.main)
.sink { interval in
print(interval) // → Stride(magnitude: 1.0013610124588013)
// → Stride(magnitude: 0.9992760419845581)
}
위 예제에서는 매초 이벤트를 방출하는
Timer가 있습니다.
첫 번째 구독 시Timer를 시작하려면autoconnect()를 호출하는 것을 기억하세요.
방출되는 값은 초 단위이며, 이는Scheduler가RunLoop.main이기 때문입니다.
Cuestionario
-
간단히
delay(for:scheduler:)가 무엇을 하는지와 이벤트의 시간 간격을 어떻게 유지하는지 설명하십시오. -
Timer.TimerPublisher에서autoconnect()연산자의 기능은 무엇입니까? -
collect연산자에서.byTime과.byTimeOrCount전략의 차이점은 무엇입니까? -
debounce와throttle은 어떻게 다릅니까? -
measureInterval(using:)가 방출하는 값의 유형은 무엇이며, 그 측정 단위는 무엇에 의존합니까? -
debounce(for:)가 적용된Publisher가 대기 시간이 끝나기 전에 종료되면 어떻게 됩니까?- 마지막 값이 여전히 방출됩니다
- 아무 것도 방출되지 않습니다
- 종료가 지연됩니다
- 구독이 취소됩니다
-
throttle(for:scheduler:latest:)에서latest = false이면 각 윈도우에서 어떤 값이 방출됩니까?- 받은 첫 번째 값
- 받은 마지막 값
- 받은 모든 값
- 끝까지 아무 것도 방출되지 않음
-
timeout(_:scheduler:options:customError:)연산자는 오류를 발생시키거나 성공적으로 종료합니다…- Publisher가 간격 이전에 종료됩니다
- Publisher가 간격 동안 아무 것도 방출하지 않습니다
- Publisher가 두 개 이상의 값을 방출합니다
- 구독이 수동으로 취소됩니다
-
measureInterval(using:)에서RunLoop.main이 방출하는 값은 다음 단위로 측정됩니다:- 나노초
- 밀리초
- 초
- 분
-
collect(.byTimeOrCount)를 사용할 때 수집 윈도우가 끝나기 전에 방출이 발생했습니다. 무엇이 원인일 수 있습니까?- 매개변수로 정의된 이벤트 수가 누적되었습니다
- 입력 Publisher가 종료 이벤트를 보냈습니다.
- 거짓 양성입니다
- 구독이 잘못되었습니다
해결
-
delay(for:scheduler:)
업스트림에서 받은 이벤트를 메모리에 저장하고,interval매개변수로 정의된 지연 시간이 지난 후 하나씩 다시 방출합니다. 원래의 시간 간격을 유지합니다. -
autoconnect()inTimer.TimerPublisher
Timer의Publisher는 connectable이며, 이벤트 방출을 시작하려면 명시적으로.connect()를 호출해야 합니다.autoconnect()는 첫 구독을 받았을 때 자동으로 연결을 설정합니다. -
.byTime와.byTimeOrCountincollect의 차이.byTime는 입력을 시간만 기준으로 모아 지정된 기간이 끝났을 때 누적된 배열을 방출합니다..byTimeOrCount는 시간과 개수 두 기준 모두에 따라 누적합니다; 지정된 기간 또는 지정된 개수에 도달하면, 먼저 발생한 조건에 따라 배열을 방출합니다.
-
debounce와throttle의 차이debounce는 “디바운스” 시간이 지나고 나서 마지막으로 받은 값을 방출합니다.throttle는 주기적으로 방출하며, 각 시간 창 안에서latest매개변수에 따라 첫 번째 값 또는 마지막 값을 방출할 수 있습니다.
-
measureInterval(using:)가 방출하는 값
Stride타입(구체적으로SchedulerTimeType.Stride)의 값을 방출하며, 이는 두 값 사이의 간격을 나타냅니다. 단위는 사용된Scheduler에 따라 달라집니다 (DispatchQueue는 나노초,RunLoop는 초 등). -
debounce(for:)가 적용된 Publisher가 대기 시간 전에 완료될 경우
아무것도 방출되지 않습니다. 남아 있던 마지막 값은 업스트림이 완료되면서 폐기됩니다. -
throttle(for:scheduler:latest:)에서latest = false
각 시간 창에서 첫 번째로 받은 값만 방출됩니다. -
timeout(_:scheduler:options:customError:)
지정된 간격 동안 Publisher가 아무것도 방출하지 않을 경우 오류를 방출합니다(customError가nil이면 정상적으로 완료됩니다). -
measureInterval(using:)를RunLoop.main과 함께 사용할 때의 단위
값은 초 단위로 측정됩니다. -
collect(.byTimeOrCount)에서 조기 방출이 발생하는 경우
입력 Publisher가 완료 이벤트를 전송했기 때문에, 시간 창이 아직 만료되지 않았더라도 누적된 배열이 강제로 방출됩니다.
Preguntas y respuestas
7. En throttle(for:scheduler:latest:), si latest = false, ¿qué valor se emite en cada ventana?
- [✅] 첫 번째로 받은 값
8. El operador timeout(_:scheduler:options:customError:) emite un error o finaliza exitosamente si…
- [✅] 퍼블리셔가 해당 구간 동안 아무것도 방출하지 않을 때
9. En measureInterval(using:), los valores emitidos por RunLoop.main están medidos en:
- [✅] 초
10. Usando collect(.byTimeOrCount) ocurre una emisión antes de que se termine la ventana de recolección. ¿Qué pudo haber ocurrido?
- [✅] 매개변수로 정의된 이벤트 수가 누적되었습니다 (Note: original answer indicated event‑of‑completion; kept as provided)