[iOS]@Published와 Subject중 뭘 선택해야할까?

Youth·2024년 8월 12일
6

고찰 및 분석

목록 보기
8/21

안녕하세요~ 요즘 좀 블로그 텀이 길어지는것다고 느끼는 킴스캐슬입니다...ㅎㅎ

개발공부를 소홀이 한다거나 그런건 아닌데 가족여행도 다녀오고 경험삼아 이력서를 써서 제출해보고있다보니 면접제의도 몇번 받게되고 그러다보니 정신없이 시간을 보냈던것같네요

그러던 와중에 정말 좋은 기회가 생겼었는데 제가 졸예자가 아니라서 놓쳐버린 기회도 생겨서 아쉽지만 지금 제 위치가 어느정도인지 잘 알수있는 그런 시간을 보내고 돌아왔습니다

그동안 잘해왔던 부분도 충분히 많다는 것과 아직 부족한 부분도 많다는걸 동시에 느꼈던 한주였던것같습니다. 물론 그동안 잘 공부해왔구나를 조금더 느끼긴 했습니다 ㅎㅎ

서론은 이정도로 하고 본격적으로 오늘의 주제인 @Published와 Subject에 대한 이야기를 해보도록 하겠습니다


기존 MVC의 프로젝트를 MVVM으로 리팩터링을 준비하는 과정에서 유저의 input에 의해 변화하는 변수를 @Published로 만들어서쓸지 아니면 subject로 쓸지를 고민하게 되었습니다

처음에 제 머릿속에 들었던 방식은 @Published를 만들어서 쓰는 방식이었는데 viewmodel의 input output을 만들어서 쓰는 방식에서는 대부분의 개발자분들이 subject를 사용하시더라고요...

그냥 문득 이런생각이 들었습니다

uikit에서 @Published와 Subject(CurrentValueSubject같은거)는 하는 역할이 거의 똑같지 않나…?

아래 코드를 보면 값을 넣어주는 방식만 다를뿐 동작자체는 거의 동일하지않나는 생각이 들었습니다

@Published var count: Int = 0
$count.sink { print($0) }
self.count = 3
0출력, 3출력
var count = CurrentValueSubject<Int, Never>(0)
count.sink { print($0) }.store(in: &cancellables)
count.send(3)
0출력, 3출력

결국 두개의 publisher를 sink해서 값을 출력한다고 했을때 값을 넘겨주는 방식만 다를뿐 우리가 예상한 방식과크게 다르지 않게 동작한다는걸 직관적으로 알 수 있죠

이랬을때 애매해지는게 하나 있습니다

그럼 우리는 어떤 기준으로 어떤 방식을 선택해야하는가?

그래서 해당 내용을 공부해보고 내린 나름의 결과를 정리해보고자 합니다

@Published와 Subject의 차이

제가 방금말씀드린 부분에서 틀린부분을 먼저 말씀드리면 @Published와 Subject는 완전히 동일하게 동작하지는 않는다는 겁니다

한가지 예시를 들어보겠습니다

class PublishedModel {
    @Published var number: Int = 0
}

let pModel = PublishedModel()

pModel.$number.sink { number in
    print("Closure: \(number)")
    print("Object:  \(pModel.number) [read via closure]")
}

pModel.number = 1
print("Object:  \(pModel.number) [read after assignment]")

이렇게 동작했을때 우리의 예상은 어떨까요

Closure: 0
Object:  0 [read via closure]
Closure: 1
Object:  1 [read via closure]
Object:  1 [read after assignment]

아마도 위와같은 출력값이 나올거라고 예상할 수 있을겁니다 우선 0이라는 값이 published될거고 추후에 1이라는 값으로 바뀌었으니까 1이라는 값이 published되니까 다시 1과 1이 출력될거고 그리고 최종적으로 1이 출력되는 결과를 예상하실겁니다. 하지만 실제로 실행해보면 조금 다른 결과가 출력되게 됩니다

Closure: 0
Object:  0 [read via closure]
Closure: 1
Object:  0 [read via closure]     <- ...?이게뭐지???
Object:  1 [read after assignment]

분명히 sink를 통해 들어온 값(여기서는 변경되기위해서 새로 변수에 들어온 값이라고 말하는게 더 정확하긴 하겠네요)이 잘 출력되는데 class의 property로 접근하니까 변화되기 전의 값인 0이 출력되는걸 알 수 있습니다

대체 이게 왜그럴까라고 찾아보니 @Published는 didSet이 아니라 willSet으로 만들어졌다고 합니다. 우리가 저기서도 1이 출력될거라고 예상했던 이유는 내부적으로 didSet이라고 생각했기 때문이었습니다

제 기준이긴한데 어떤 변수가 바뀌었을때 어떤 특정 action을 저는 didSet을 통해 호출해주거든요(사실 willSet을 써본적이 없습니다)

didSet : 값이 변경된 직후에 호출
willSet: 값이 변경되기 직전에 호출

하지만 subject의 경우엔 어떨까요?

class CurrentValueSubjectModel {
    var number: CurrentValueSubject<Int, Never> = .init(0)
}

let cvsModel = CurrentValueSubjectModel()

cvsModel.number.sink { number in
    print("Closure: \(number)")
    print("Object:  \(cvsModel.number.value) [read via closure]")
}

cvsModel.number.send(1)

print("Object:  \(cvsModel.number.value) [read after assignment]")

실제로 수행을 해보면 우리가 예상하는 결과 그대로 나오는걸 알 수 있습니다

Closure: 0
Object:  0 [read via closure]
Closure: 1
Object:  1 [read via closure]      <- 예상대로나옴
Object:  1 [read after assignment]

이건 전적으로 제 기준이긴 합니다만 반응형 UI을 FRP framework를 통해서 그리지 않는다고 가정했을때 저는 didSet만 사용해서 코드를 작성했었기때문에 우리가 생각하는 방식대로(didSet처럼) 동작하게 하려면 Subject를 선택하는게 조금더 side effect에서 자유로울 수 있겠다는 생각이 들었습니다

CurrentValueSubject를 사용한다면 변수가 바뀔때마다 그 변수가 저장이되어있으니까 마치 stored property처럼 쓸수 있기도하구요

또다른 차이점은 한 세가지 정도가 존재합니다

  1. 프로토콜에서 property wrapper를 사용할 수 없기때문에 Publised변수는 protocol로 추상화를 할 수가없습니다
  2. @Published는 클래스의 프로퍼티에만 사용할 수 있고, structure 같은 non-class 타입에서는 사용이 제한되지만 Subject는 struct같은 non-class 타입에서도 사용이 가능합니다
  3. @Published는 private(set)으로 설정함으로 인해서 외부에서 값을 직접 설정하는 것을 방지할 수 있지만 subject의 경우는 send를 막을 방법이 없다

이렇게만 보면 1,2번의 관점에서는 subject가 좋아보이니까 3번정도의 이유가 아니면 굳이 published를 선택할 이유가 없어보였습니다

근데 왜 @Published가 존재하는걸까요…? 단순히 3번이유때문은 아닌거같다라는 느낌이 들었는데 관점을 swiftUI로 넓혀봤더니 그 이유가 어렴풋이 보이기 시작했습니다

swiftUI에서의 @Published

swiftUI에는 ObservableObject 라는 protocol이 존재합니다(extenstion으로 필요한 메서드와 프로퍼티가 기본구현되어있기때문에 채택만하면 됩니다)ObservableObject 는 objectWillChange라는 프로퍼티를 사용할 수 있는데 해당 프로퍼티에 send()라는 메서드가 존재합니다. 해당 메서드는 변경사항을 알려주는 역할을 합니다

근데 우리가 변수가 많아지고 변수의 변경이 잦아지면 매번 send를 호출하기가 쉽지않을거고 실수할가능성이 높을겁니다…이런 문제를 해결해줄수있는게 @Published입니다

아래 overview를 보면 아래와같이 해석할 수 있습니다

기본적으로 ObjectableObject는 @Published 속성이 변경되기 전에 변경된 값을 내보내는 objectWillChange publisher를 synthesize합니다.

ObservableObject 프로토콜을 따르는 객체에서 objectWillChange 프로퍼티를 직접 사용하지 않고 변경 사항을 전달하는 방법이 @Published 인거고 해당 속성을 달아주면 변수의 값이 변경되었을때 자동으로 objectWillChange 프로퍼티를 사용하여 변경 사항을 subscriber에게 전달하게되는겁니다

이렇게까지 보고나니 Publisher는 swiftUI의 Observable Object와 함께 사용하기 위해 만들어진 녀석이아닐까라는 생각이들었습니다(UIkit에서 사용해도 되지만 의도에 조금더 맞는 사용은 swiftUI에서의 사용이 아닐까라는 생각인겁니다)

왜 didSet이 아니라 willSet인걸까?(자의적해석 세스푼 추가)

마지막 궁금증은 왜 굳이 didSet이 아니라 willSet으로 동작하게 만들어놨을까였는데요. 정말 제 개인적인 생각을 풀어보겠습니다

swiftUI에서 가장 중요한 특징중에 하나라면 view가 struct라는것일겁니다 그렇기때문에 우리가 어떤 view의 변경사항이 발생했을때 view를 새로그리는일은 기존의 view struct를 완전히 다시 만들어서 메모리에 올리는 일이될겁니다(작은 변경사항에서 struct자체를 다시 만들어야하는거죠) 실제로도 swiftUI관련하는 WWDC영상을 보면 이 부분을 가장 고려해서 만들고있다고 합니다

viwe자체가 struct라 변하지 않은 나머지부분에대한 reference를 가질수없기때문에 diffable datasource처럼 snapshot을 이용해 변경된 부분만 업데이트를 해주는 방식을 사용한다고 합니다

그러면 결국 우리가 알고싶은건 현재의 상태와 변하게될 상태일겁니다 결국 기존의 상태미래에 변하게될 상태를 비교하고싶기때문에 willset을 쓴게 아닐까 싶습니다. didset의 경우엔 결과적으로 변한 상태기존 상태을 비교할수 있는데 기존 상태미래에 변하게될 상태는 willset과 didset 모두 알 수 있지만 문맥상 기존값과 변하게될 값을 비교하는게 자연스러우니까요(이건 순도 100퍼센트 제 개인적인 의견입니다)

UIkit에서의 @Published?

좀 이야기가 왔다갔다하는데 정리를 해보자면 @Published가 굳이굳이 willset으로 동작하는 이유를 생각해보고 subject와 비교했을때 @Published가 확실한 장점과 이유를 가지게 되려면 observable object와 같이 사용해야한다는 점을 고려해봤을때 swiftUI가 아닌 UIkit에서는 subject를 사용하는게 의도에 맞는 선택이 아닐까라는 생각이듭니다


이런저런 포스팅에 오랜만에 swiftUI포스팅들까지 보느라 시간이 꽤나 걸렸는데 막상 글은 그렇게 길지가 않네요 ㅎㅎ...

제가 uikit에 combine을 함께 사용해서 진행했던 프로젝트에서 subject는 하나도 안쓰고 @Published만 썼었는데 그때는 @Published가 훨씬 편해서 그렇게 사용을 했던것 같긴합니다(combine에 대한 제대로된 선행 공부가 되어있지도 않기도 했지만요...)

그리고 실제로 객체 내부의 변수에 접근하기보다는 실제 변수에 바로 접근을 했다보니 willSet동작으로 인한 side effect가 발생하지 않았던거같아요
(stack overflow글을 보지 않았더라면 willSet처럼 동작한다는걸 애초에 몰랐을겁니다...ㅎㅎ)

그렇게생각하면 정말 단순한 데이터바인딩의 경우엔 published를 사용하는것도 편의성이나 사용성 측면에서 충분한 이유가 되지 않을까 싶은 생각도 듭니다

아무튼 오늘 이야기는 이정도로 마무리해보려합니다 다음에는 어떤글을 가져올지 모르겠지만 리팩터링 관련한 주제를 가져올것같습니다!

그럼 20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

4개의 댓글

comment-user-thumbnail
2024년 8월 6일

안녕하세요! 좋은글 잘 봤습니다
중간에 아래 코드가 중복으로 두번 들어가있는듯 한데, 혹시 하나는 Subject 에 대한 예시가 되어야했을까요?

@Published var count: Int = 0
$count.sink { print($0) }
self.count = 3
0출력, 3출력
1개의 답글
comment-user-thumbnail
2024년 8월 14일

👍

답글 달기
comment-user-thumbnail
2024년 9월 24일

너무 좋은 글 잘 읽었습니다!
개인적인 해석도 저는 굉장히 근거 있는 얘기인 것 같습니다.

답글 달기