[Combine] Basics (Subject)
Subject 란
이전에 Publisher를 설명하면서 Just, Future, Empty, Fail 등등은 모두 Publisher가 내부에서
자체적으로 만든 데이터만 내보내는 것이었는데요.
-> Subject는 이미 만들어져있는 Publisher에 값을 밀어 넣어주는 행동을 할 수 있음!
즉 publisher에게 외부에서 값을 주입해서, publisher를 구독하고 있는 subscriber에게 값을 전달할 수 있음.
왜 subject를 써야하나?
앞에서 봤듯이 Publisher와 Subscriber들은 Subcription을 매개로 연결되어 있음
-> Subject를 사용하면, 이 연결 관계를 이용해서 내가 지금 보내고 싶은 값을 Subscriber에게 한번에 전달할 수 있음 + 내가 원할 때 내가 원하는 값을 보낼 수 있음.
여기서 '나'는 개발자가 될수도, 아니면 어떤 이벤트가 될수도 있겠죠
Subject를 사용해서 외부에서 Publisher로 값을 주입하는 방법
우선 {subject}.send(_) 라는 함수를 통해서, 인자에 주입하고 싶은 값을 보내면,
이 subject라는 publisher는 받은 값을 구독자들에게 내보냄.
이 Subject에는 두가지 종류가 있는데요,
1. 보내진 값을 저장하지 않고 내보내기만 하는 Subject = PassthroughSubject (이름 한번 직관적이다 그쵸)
2. 가장 최근에 보내진 값은 저장하고 있고, 값이 변할 때 (새로운 값이 들어올 때) 값을 내보내는 Subject = CurrentValueSubject
PassthroughSubject
는 Subscriber에게 element(요소, 값)를 broadcast하는 Subject입니다
즉, 보내진 값을 방송하듯이 내보내준다는 거죠
예를 들어, subject -> sink 가 연결되어 있으면, 우리가 subject.send로 값을 보내면, subject에서 값이 나와서 sink가 받는 구조입니다.
이렇게 필요할 때 새로운 값을 게시(publish)할 수 있고, 게시된 값은 해당 subject를 구독하는 subscriber에게 전달됩니다. (같은말 여러번 반복하는 거 맞음)
당연하지만, 구독하기 전에 send된 값은 받지 못합니다.
CurrentValueSubject
는 passthorugh와 달리 값을 저장하고 있는 subject입니다.
CurrentValueSubject의 value에 가장 최근에 받은 값을 저장하고 있습니다.
subject.value로 접근도 가능하고 값을 바꾸는 것도 가능합니다. (값을 바꾸는 방법은 value에 다른 값 대입 혹은 send())
+생성시 value의 초기값 지정 가능
만약 currentValueSubject를 어떤 시점에 한 subscriber가 구독하면,
그 시점에 value에 저장되어 있는 값 부터 전달받습니다.
요약 정리:
Subject는 외부에서 값을 주입할 수 있는 Publisher,
그 중 PassthroughSubject는 주입된 값을 내보내는 일회성,
CurrentValueSubject는 최근 주입된 값 하나를 갖고 있는 subject
구독을 유지하는 방법
완전 기본적으로 Publisher와 Subscriber를 사용하는 방법은
Just("안녕").sink { print($0 } 일거에요.
근데 이전에 분명히 Publisher와 Subscriber가 있고 그들을 연결하는 Subscription이 있다고 했는데,
여기서 Publisher는 Just이고 sink를 통해 만들어진 Sink가 Subscriber라면,
Subscription은 어딨냐 이럴 수 있는데, sink의 return type이 Cancellable입니다.
이전 포스팅에서 subscriber가 subscription을 .cancel() 해서 구독 취소를 할 수 있다고 했는데,
여기서 cancel을 따와서 Cancel할 수 있는 것 = Cancellable 타입이 == Subscription 이 된겁니다. (Subscription이 Cancellable을 따르는 타입입니다)
그러면, sink가 Subscription을 반환하는 것인데, 이를 아무도 저장하고 있지 않으면?
스위프트는 이 subscription이 사용되지 않는 다고 판단하고 메모리에서 해제시켜버림
그럼 subscription이 사라지고, Just와 sink를 연결하는 구독권이 없어졌으니 둘의 연결은 끊기게 됩니다.
이때 뭐 만약에 Just라는 publisher에 값을 주입한다고 한들 sink가 받을 수가 없겠죠.
이런 경우는 Subject에서도 마찬가지 입니다. Subject는 애초에 외부에서 내가 원하는 시점에 데이터를 주입해서
구독자들에게 전달하는 것이 목적인데, 구독권이 사라져버리면 전달할수가 없죠.
그래서? 이 구독권을 메모리에서 해제되지 않도록 잡고있는 변수를 선언해서 이곳에 저장해두어야합니다.
그게 Combine 사용 예제에서 많이 보이는
var cancellables = Set<AnyCancellable>() 라는 코드가 되는거죠
그럼 어떻게 저장함?
그냥 sink까지 한 코드 블럭을 변수에 저장해도 되고, store라는 메서드를 사용하는 방법이 있음
아래 코드 처럼 sink 후에 .store 해주면 cancellables라는 Set에 저장됩니다. (중복 저장을 막아주겠네요)
var cancellables = Set<AnyCancellable>() // ✅ 구독 보관소
func start() {
let pub = Just("🍎")
pub
.sink { value in
print("받은 값:", value)
}
.store(in: &cancellables) // ✅ 구독을 저장함
}
아니면 어차피 sink의 반환 타입이 cancellable이라고 했으니까
아래 코드 처럼 대입해주기만 해도 됩니다.
var subscription: AnyCancellable?
func setup() {
subscription = Just("🍎")
.sink { value in
print("값:", value)
}
}
.eraseToAnyPublisher()
또 갑자기 등장한 한 함수..
subject에 쓸 수 있는 메서드 인데요, erase지운다. to any publisher 그리고, 어떤 퍼블리셔로
지워서 publisher로 된다는 것 같네요.
뭔가 하면, Subject는 외부에서 값을 주입할 수 있다고 했죠.
근데 그 외부가 내 영역안에서의 외부면 좋겠는거에요. 즉, 완전 외부 완전 다른 사람이 이 subject의 send메서드를 사용해서 값을 주입하게 하고 싶지 않을 수 있겠죠?
이때 실제로는 subject인데, 밖에서는 publisher로 보이게 해주는 함수가 eraseToAnyPublisher() 입니다.
사용 예시
class MyViewModel {
private let subject = PassthroughSubject<String, Never>()
var publisher: AnyPublisher<String, Never> {
subject.eraseToAnyPublisher()
}
func doSomething() {
subject.send("💥 내부에서만 발행됨")
}
}
이렇게 내부적으로는 private로 subject를 만들어두고, 외부에는 publisher만 내놓는데,
이 publisher가 반환하는 것은 사실 subject에서 subject타입을 지운 (밖에서 send할 수 없게) publisher가 되는거죠.
그래서 내부에서만 subject.send를 사용할 수 있게!
그럼 subject가 읽기 전용으로 만들어지게 됩니다. (외부에서는)
operator를 하려고 했는데 너무 길어져서 나눠야겠음