Combine #6: 시간 조작 연산자

발행: (2025년 12월 28일 오전 03:48 GMT+9)
13 min read
원문: Dev.to

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의 Timer Publisher 버전을 사용했습니다.
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 와 다릅니다:

특성debouncethrottle
윈도우마지막 이벤트 에 열림첫 번째 이벤트 에 열림
방출윈도우 내 마지막 이벤트만윈도우 내 첫 번째(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)를 발행합니다.

  • customErrornil이면 성공 종료 이벤트가 발행됩니다.
  • 그렇지 않으면 해당 클로저에서 정의된 오류가 발행됩니다.
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가 방출하는 값의 타입은 매개변수로 전달된 SchedulerTimeInterval 입니다:

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()를 호출하는 것을 기억하세요.
방출되는 값은 단위이며, 이는 SchedulerRunLoop.main이기 때문입니다.

Cuestionario

  1. 간단히 delay(for:scheduler:)가 무엇을 하는지와 이벤트의 시간 간격을 어떻게 유지하는지 설명하십시오.

  2. Timer.TimerPublisher에서 autoconnect() 연산자의 기능은 무엇입니까?

  3. collect 연산자에서 .byTime.byTimeOrCount 전략의 차이점은 무엇입니까?

  4. debouncethrottle은 어떻게 다릅니까?

  5. measureInterval(using:)가 방출하는 값의 유형은 무엇이며, 그 측정 단위는 무엇에 의존합니까?

  6. debounce(for:)가 적용된 Publisher가 대기 시간이 끝나기 전에 종료되면 어떻게 됩니까?

    • 마지막 값이 여전히 방출됩니다
    • 아무 것도 방출되지 않습니다
    • 종료가 지연됩니다
    • 구독이 취소됩니다
  7. throttle(for:scheduler:latest:)에서 latest = false이면 각 윈도우에서 어떤 값이 방출됩니까?

    • 받은 첫 번째 값
    • 받은 마지막 값
    • 받은 모든 값
    • 끝까지 아무 것도 방출되지 않음
  8. timeout(_:scheduler:options:customError:) 연산자는 오류를 발생시키거나 성공적으로 종료합니다…

    • Publisher가 간격 이전에 종료됩니다
    • Publisher가 간격 동안 아무 것도 방출하지 않습니다
    • Publisher가 두 개 이상의 값을 방출합니다
    • 구독이 수동으로 취소됩니다
  9. measureInterval(using:)에서 RunLoop.main이 방출하는 값은 다음 단위로 측정됩니다:

    • 나노초
    • 밀리초
  10. collect(.byTimeOrCount)를 사용할 때 수집 윈도우가 끝나기 전에 방출이 발생했습니다. 무엇이 원인일 수 있습니까?

    • 매개변수로 정의된 이벤트 수가 누적되었습니다
    • 입력 Publisher가 종료 이벤트를 보냈습니다.
    • 거짓 양성입니다
    • 구독이 잘못되었습니다

해결

  1. delay(for:scheduler:)
    업스트림에서 받은 이벤트를 메모리에 저장하고, interval 매개변수로 정의된 지연 시간이 지난 후 하나씩 다시 방출합니다. 원래의 시간 간격을 유지합니다.

  2. autoconnect() in Timer.TimerPublisher
    TimerPublisherconnectable이며, 이벤트 방출을 시작하려면 명시적으로 .connect()를 호출해야 합니다. autoconnect()는 첫 구독을 받았을 때 자동으로 연결을 설정합니다.

  3. .byTime.byTimeOrCount in collect의 차이

    • .byTime는 입력을 시간만 기준으로 모아 지정된 기간이 끝났을 때 누적된 배열을 방출합니다.
    • .byTimeOrCount는 시간과 개수 두 기준 모두에 따라 누적합니다; 지정된 기간 또는 지정된 개수에 도달하면, 먼저 발생한 조건에 따라 배열을 방출합니다.
  4. debouncethrottle의 차이

    • debounce는 “디바운스” 시간이 지나고 나서 마지막으로 받은 값을 방출합니다.
    • throttle주기적으로 방출하며, 각 시간 창 안에서 latest 매개변수에 따라 첫 번째 값 또는 마지막 값을 방출할 수 있습니다.
  5. measureInterval(using:)가 방출하는 값
    Stride 타입(구체적으로 SchedulerTimeType.Stride)의 값을 방출하며, 이는 두 값 사이의 간격을 나타냅니다. 단위는 사용된 Scheduler에 따라 달라집니다 (DispatchQueue는 나노초, RunLoop는 초 등).

  6. debounce(for:)가 적용된 Publisher가 대기 시간 전에 완료될 경우
    아무것도 방출되지 않습니다. 남아 있던 마지막 값은 업스트림이 완료되면서 폐기됩니다.

  7. throttle(for:scheduler:latest:)에서 latest = false
    각 시간 창에서 첫 번째로 받은 값만 방출됩니다.

  8. timeout(_:scheduler:options:customError:)
    지정된 간격 동안 Publisher가 아무것도 방출하지 않을 경우 오류를 방출합니다( customErrornil이면 정상적으로 완료됩니다).

  9. measureInterval(using:)RunLoop.main과 함께 사용할 때의 단위
    값은 단위로 측정됩니다.

  10. 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)
Back to Blog

관련 글

더 보기 »

Combine #13: 자원 관리

share 및 multicast_: share 대부분의 Combine Publishers는 struct이며 파이프라인만을 기술하고, 공유 상태를 저장하지 않습니다. 공유 상태가 생성되지 않습니다.

Swift Combine의 Hot 및 Cold Publishers

핫 퍼블리셔와 콜드 퍼블리셔란 무엇인가? 콜드 퍼블리셔 콜드 퍼블리셔는 구독자마다 새로운 실행을 생성합니다. 구독할 때 작업이 새롭게 시작됩니다. swift...

Swift #28: Foundation

!David Goyeshttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fu...