[Combine] Combine Operator 완전 정복하기 (4) - Timing and Control Operators

2025. 4. 6. 13:59Framework, Library/Combine

마지막 Combine Operator를 정리해볼 시간입니다!

이번 글에서 살펴보게 될 Timing and Control Operators는 delay, debounce, throttle, timeout, retry, catch 등이고요.
해당 연산자들을 활용해서 데이터 스트림의 타이밍을 조작하거나, 제어 (잠시 멈추거나 기다리는 등의)할 수 있는 방법을 배우게 될 겁니다.

오늘 글을 마무리로, Combine에서 사용할 수 있는 대부분의 Operator는 모두 배웠다고 할 수 있습니다.
어여 마지막 고지를 정복하기 위해. 바로 글을 시작해 보죠!

그동안 살펴본 Combine Operator가 궁금하다면, 👇🏻아래 링크👇🏻를 참조해주세욧!

 

[Combine] Combine Operator 완전 정복하기 (1) - Combining Operators

예전 아래 제 글에서 Operator의 개념과 종류들을 소개한 적이 있습니다.그런데 단순히 글과 표로만 정리해서 읽고 넘어가기에는, Combine을 사용하면서 충분히 Operator를 적재적소에 사용하기가 어

mini-min-dev.tistory.com

 

[Combine] Combine Operator 완전 정복하기 (2) - Transforming Operators

[Combine] Combine Operator 완전 정복하기 - Combining Operators예전 아래 제 글에서 Operator의 개념과 종류들을 소개한 적이 있습니다.그런데 단순히 글과 표로만 정리해서 읽고 넘어가기에는, Combine을 사용

mini-min-dev.tistory.com

 

[Combine] Combine Operator 완전 정복하기 (3) - Filtering Operators

오늘 글은 세 번째 Combine Operator인 Filtering Operators에 대해 알아보고자 합니다!Filtering Operator는 Upstream Publisher가 방출하는 값을 필터링해, 필요한 데이터만 처리할 수 있도록 돕는 연산자입니다.한

mini-min-dev.tistory.com

 

Delay

✔️ delay : Upstream Publisher가 값을 방출하면, 잠시 보류했다가 일정 시간 이후에 값을 Publish함.


delay는 데이터 스트림의 시간을 제어하는 Operator입니다.

delay라는 이름처럼 Upsteam Publisher에서 방출한 값을 잠시 지연했다가 일정 시간이 지난 이후에 값을 방출하는 역할이죠.
여기서의 일정 시간은 delay의 for 파라미터로 지정할 수 있습니다.

예를 들어 아래와 같은 코드가 있다고 한다면, 
upstreamPublisher에서 방출된 값을 받아 잠시 1초의 시간동안 보류했다가 이후 메인 스레드에서 방출하도록 만들 것입니다.
*scheduler 파라미터를 통해서는 Event를 지연시킬 때 사용하는 스케줄러를 지정할 수 있습니다. 해당 코드에서는 main으로 지정했죠.

let delayedPublisher = upstreamPublisher
    .delay(for: .seconds(1.0), scheduler: DispatchQueue.main)

 

 

Debounce

✔️ debounce : Upstream Publisher에서 일정 시간동안 값이 방출되지 않을 때, 마지막 값을 Publish함.


debounce는 특이하게 Publisher로부터 전달되는 값이 멈춘 뒤 -> 일정 시간이 지난 이후에만 값을 방출시키는 연산자입니다.

delay와 동일하게 for 파라미터로 일정시간을 지정할 수 있고,
scheduler 파라미터로 Event를 지연시킬 때 사용하는 스케줄러를 지정할 수 있습니다.

let debouncedPublisher = upstreamPublisher
    .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)

저 같은 경우 debounce는 텍스트 필드에서 입력되는 값을 실시간으로 네트워크 통신을 통해 중복체크하고자 할 때 사용합니다.

사용자가 텍스트 필드에 입력하는 값은 한 글자 한 글자 입력마다 실시간으로 변하게 되는데요.
만약, 그 빠른 입력 한번 한 번마다 실시간으로 네트워크 요청을 보낸다고 하면 매우 비효율적일 것입니다.
그럴 때 아래 코드와 같이 0.2초의 시간을 debounce로 지정해 지나치게 빠른 텀으로 바뀐 값에 대해서는 불필요한 네트워크 통신을 하지 않도록 제한할 수 있는 것이죠.

텍스트 필드는 UI 작업이기 때문에 스케줄러도 메인으로 지정해서 사용할 수 있을 것입니다!

input.clipNameChanged
    .debounce(for: 0.2, scheduler: RunLoop.main)
    .removeDuplicates()
    .networkFlatMap(self) { context, clipTitle in
        context.getCheckCategoryAPI(categoryTitle: clipTitle)
    }
    .sink { isDuplicate in
        output.duplicateClipName.send(isDuplicate)
    }.store(in: cancelBag)

 

 

Throttle

✔️ throttle : 지정된 시간동안 Upstream Publisher에서 값이 연속 방출되더라도, 처음 또는 마지막으로 방출된 값만을 Publish함.


연속으로 방출되는 값을 제어하는 또 다른 방법으로는 throttle Operator가 사용될 수도 있습니다.

지정된 시간 간격 (for 파라미터로 지정) 내에서 처음 혹은 마지막 값만 방출하고, 이후 값을 무시하도록 하는 방법인데요.
처음 값을 방출할 때는 latest를 false로 / 마지막 값을 방출할 때는 latest를 true로 지정하면 됩니다.

let throttlePublisher = upstreamPublisher
    .throttle(for: .seconds(1.0), scheduler: DispatchQueue.main, latest: false)


저 같은 경우 throttle은 버튼의 중복 클릭을 방지할 때 주로 사용합니다.

사용자가 버튼 클릭 이후,
화면이 로딩되는 동안 (= 백그라운드에서 내트워크 작업을 수행하는 동안) 사용자가 중복으로 버튼 클릭 이벤트를 수행하는 경우.
예상치 못한 결과가 표출될 수 있기 때문에 Upstream Publisher의 입력을 막고자 할 때 아래와 같은 코드로 throttle을 사용할 수 있죠.

input.deleteClipButtonTapped
    .throttle(for: .seconds(1.0), scheduler: DispatchQueue.main, latest: false)
    .networkFlatMap(self) { context, clipID in
        context.deleteCategoryAPI(deleteCategoryDto: clipID)
    }
    .sink { _ in
        output.deleteClipResult.send()
        output.needToReload.send()
    }.store(in: cancelBag)

 

 

잠깐. 여기서 Debounce와 Throttle 정확하게 비교하고 넘어가기!

debounce throttle
지정된 시간동안 추가 Event가 발생하지 않을 때만 Publish함. 지정된 시간 간격마다 한 번의 Event를 Publish함.
연속으로 들어오는 Event에 대해 마지막 Event만 처리함. 연속으로 들어오는 Event에 대해 일정 주기마다 (처음 또는 마지막 값을) 처리함
사용자가 입력을 멈춘 후, 값을 처리해 불필요한 요청을 줄이는 데 활용 주기적 값의 방출이 필요하거나, 중복 요청을 무시할 때 사용

 

 

Timeout

✔️ timeout : 지정된 시간동안 Upstream Publisher에서 값을 방출하지 않으면, 방출을 완료하거나 에러를 Publish함.

일정 시간동안 값이 방출되지 않기를 기다렸던 debounce 연산자와는 반대로,
일정 시간동안 값이 방출되지 않으면 Publisher 자체를 완료하거나 / 에러를 방출할 수 있는 Operator는 timeout입니다.

예를 들어, 네트워크 요청 이후 일정 시간동안 응답이 없을 때
혹은 사용자가 해당 화면에서의 동작이 일정 시간 이상 발생하지 않을 때, 별도의 다른 Action 로직을 취하고 싶은 경우.
timeout Operator로 화면 전체의 흐름을 바꿔줄 수 있을 거에요!

let timeoutPublisher = upstreamPublisher
    .timeout(for: .seconds(5.0), scheduler: DispatchQueue.main)

 

 

Retry, Catch

✔️ retry : 에러가 발생했을 때, 지정된 횟수만큼 작업을 다시 시도함.
✔️ catch : 에러가 발생했을 때, 해당 에러를 처리하고 새로운 Publisher로 대체하여 처리함.


Upstream Publisher로부터 에러가 발생했을 때, 이를 처리할 수 있는 Operator는 retry와 catch가 있습니다.

바로 예시를 들어보죠.
네트워크 통신으로 랜덤한 Number를 가져오는 메서드, fetchRandomNumber()를 호출한다고 해봅시다.
만약, 어떤 특정 문제로 인해 해당 메서드로부터 특정 Number를 가져오지 못하는 상황이 발생하면, 우리가 수행할 수 있는 액션은 두 개가 있습니다.

  1. 다시 시도해야지! 값을 제대로 불러올 때까지 재도전 하는거야. -> retry가 해당
  2. 그냥 포기하쟈. 우리가 정한 값으로 대체해서 내보낼게. -> catch가 해당

두 Operator를 적절히 조화롭게 사용한다면,
에러가 발생하더라도 크래시가 발생하지 않는 부드러운 흐름의 앱을 만들 수 있을 겁니다. 우리 조화롭게 사용해보도록 해요😊

fetchRandomNumber()
    .retry(3)
    .catch { error -> AnyPublisher<Int, Never> in
        return Just(1).eraseToAnyPublisher()
    }
    .store(in: &cancellables)