[Combine]CurrentValueSubject vs Published

정유진·2023년 5월 25일
0

swift

목록 보기
20/24
post-thumbnail

의문의 시작

@StateObject 클래스의 property를 view가 subscribe 하여 view의 상태를 변화시키려 한다. publisher의 유형 중 내가 선택할 수 있는 것은 다음의 3가지 정도이다.

  • @Published
  • @PassthroughSubject
  • @CurrentValueSubject

이 중, Subject는 Publisher를 implement한 프로토콜이고 send(_:) 메서드를 통해 값을 주입할 수 있어 유용하게 사용된다.

https://developer.apple.com/documentation/combine/subject

protocol Subject<Output, Failure> : AnyObject, Publisher

위 프로토콜의 구현체인 PassthroughSubject와 CurrentValueSubject의 차이는 다소 분명한데 PassthroughSubject는 이벤트가 발생하면 자신을 구독한 모든 subscriber에게 이를 알린다는 점에서 NotificationCenter를 통해 post하던 것이랑 다를게 없지만 CurrentValueSubject는 초기값과 버퍼를 가진다는 점에서 좀 특이하다.

CurrentValueSubject가 초기값과 버퍼를 가지기 때문에 subscribe 해보면 이 initial value 부터 Stream에 포함된다는 것을 알 수 있고 (그래서 초기값을 별 의미없는 값으로 설정해두면 이를 걸러내야해서 귀찮다.) buffer로 값을 간직하기 때문에 이미 지나간 값은 알 수 없는 passthroughSubject와 달리 직전에 어떤 값이 publish 되었는지 버퍼에 저장된 value를 통해 확인할 수 있다.

따라서 용도에 따라 어떤 subject를 선택할 지를 결정할 수 있다. 그러나 굳이 Subject를 쓰느니 그냥 @Published를 사용하면 되지 않나 하는 생각이 드는 것이다. Published 또한 초기 값을 지정할 수 있고 값을 저장해 언제든 꺼내 쓸 수 있는데다가 값이 변화할 때마다 publish하기 때문에 view의 state를 subscribe 하기에 손색이 없기 때문이다.

subject이냐? published이냐?

나와 비슷한 의문을 가지고 있었던 StackOverFlow의 질문을 발견,
https://stackoverflow.com/questions/58676249/difference-between-currentvaluesubject-and-published

시간차에 따른 구분

위의 글에 따르면 Published는 SwiftUI에서 사용되도록 고안됐고 property의 willset block 안에서 emit이 이루어진다. subscriber가 값을 전달 받는 시점은 property가 set되기 직전이라는 뜻이다. 그래서 Published는 시간 차때문에 publish 받은 값과 실제 값과는 다른 시점이 존재할 수 있다.

class PublishedModel {
    @Published var number: Int = 0 // 초기값, 처음으로 publish될 것
}

let pModel = PublishedModel()

// 구독하기
pModel.$number.sink { number in
    print("구독 값: \(number)")
    print("number property의 값:  \(pModel.number) [구독 클로저 안에서 property 값을 확인하면?]")
}

pModel.number = 1
print("number property의 값:  \(pModel.number) [할당 후]")


class CurrentValueSubjectModel {
    var number = CurrentValueSubject<Int, Never>(0) // 초기값, 처음 publish 되면서 value
}

let cModel = CurrentValueSubjectModel()

// 구독하기
cModel.number.sink { number in
    print("구독 값: \(number)")
    print("number property의 값:  \(cvsModel.number.value) [구독 클로저 안에서 property 값을 확인하면?]")
}

cvsModel.number.send(1) // 1을 publishing 하기 

print("number property 값:  \(cvsModel.number.value) [할당 후]")

  1. @Published 결과 출력
구독 값: 0
number property의 값:  0 [구독 클로저 안에서 property 값을 확인하면?]
구독 값: 1
number property의 값:  0 [구독 클로저 안에서 property 값을 확인하면?]
number property의 값:  1 [할당 후]
  1. @CurrentValueSubject 결과 출력
구독 값: 0
number property의 값:  0 [구독 클로저 안에서 property 값을 확인하면?]
구독 값: 1
number property의 값:  1 [구독 클로저 안에서 property 값을 확인하면?] // 차이점
number property의 값:  1 [할당 후]

요약하자면,

  • property가 set되기 전에 publish 받는 것이 필요하다면 @Published
  • property가 set된 후에 publish 받는 것이 필요하다면 @CurrentValueSubject

Published의 특징

이 한계는 Published가 PropertyWrapper이기 때문에 갖는 특성이기도 하다.

  • top-level(class, function에 감싸지지 않은 코드) 에서 사용 불가
  • 프로토콜 정의에서 사용 불가
  • struct에서는 사용할 수 없고 class에서만 사용 가능하다.
  • main thread 안에서 publish 할 수 있다.
  • subscriber가 될 수 없다.

Published 주석 읽기

Combine 프레임워크에서 struct Published<Value>에 대해 읽다가 흥미로운 내용을 발견하여 정리해본다.

1. AsyncSequence와의 비교

doc://com.apple.documentation/documentation/Swift/AsyncSequence

  • 공통점
  1. both produce elements over time 둘다 단위 시간 동안 지속적으로 특정 타입의 값을 송출한다.
  2. Both APIs offer methods to modify the sequence by mapping or filtering elements. 반환 받는 data stearm 속에서 값을 필터링 할 수 있는 메서드를 제공한다.
  • 차이점
  1. the pull model in Combine uses a Combine/Subscriber to request elements from a publisher, while Swift concurrency uses the for-await-in syntax to iterate over elements published by an AsyncSequence. Combine의 경우 publisher에게 요청하는 식으로 값을 받지만 AsyncSequence의 경우 순회를 돌면서 값이 반환될 때 까지 기다리는 방식이다.
  2. only Combine provides time-based operations like debounce(for:scheduler:options:) and throttle(for:scheduler:latest:), and combining operations likemerge(with:) and combineLatest(_:_:). Combine에서만 제공하는 유용한 메서드들이 있다.
  • Combine에서 async하게 data를 받고 싶다면?

https://developer.apple.com/documentation/combine/publisher/values-1dm9r

To bridge the two approaches, the property Publisher/values-1dm9r exposes a publisher's elements as an AsyncSequence, allowing you to iterate over them with for-await-in rather than attaching a Subscriber.

var values: AsyncPublisher<Self> { get } // conforms to AsyncSequence

.values를 사용하면 sink(:)를 사용하지 않아도 async- await 를 사용하여 data stream을 아래와 같이 control 할 수 있다.

let numbers: [Int] = [1, 2, 3, 4, 5]
let filtered = numbers.publisher
    .filter { $0 % 2 == 0 }

for await number in filtered.values
{
    print("\(number)", terminator: " ")
}

2. Creating Own Publishers

Publisher protocol을 구현하여 사용하기 보다 용도에 따라 Combine framework에서 제공하는 type을 사용하라는 주석이 있어 어떤 상황에서 무엇을 사용할지의 기준이 될 수 있을 듯 하다.

  1. Use a concrete subclass of Subject, such as PassthroughSubject, to publish values on-demand by calling its Subject/send(_:) method. 내가 시점을 통제하여trigger 하고 싶을 때 사용한다.

  2. Use a CurrentValueSubject to publish whenever you update the subject’s underlying value. 갖고 있는 값이 바뀔 때마다 publish 되기 원할 때 사용한다.

  3. Add the @Published annotation to a property of one of your own types. In doing so, the property gains a publisher that emits an event whenever the property’s value changes. 내가 이미 가지고 있는 프로퍼티를 publisher로 만들기 원할 때 사용한다.

잠정 결론

나는 @Published를 쓸래.

profile
느려도 한 걸음 씩 끝까지

0개의 댓글