brunch

매거진 iOS 작업실

You can make anything
by writing

C.S.Lewis

우리는 한다 리팩토링을

UIKit+RxSwift 에서 SwiftUI+Combine 로의 여정

안녕하세요, 카카오헤어샵 iOS 개발을 담당하고 있는 아이작이라고 합니다.

오늘은 저희 iOS 파트에서 진행하고 있는 디자이너앱 리팩토링 작업에 대해 이야기를 해보려고 합니다.

고퀄리티의 이미지를 편집해 준 C사의 KYS 군에게 이 자리를 빌려 감사함을 표합니다


현재 저희는 UIKit + RxSwift 기반의 디자이너앱을 SwiftUI + Combine으로 리팩토링(이라 쓰고 전면 수정이라 읽는) 작업을 진행하는 중인데요.


디자이너앱을 먼저 리팩토링 하려는 이유는 전체적인 볼륨이 헤어샵앱 보다 작기 때문에 빠르게 대응해나갈 수 있고, SwiftUI 와 Combine 의 완성도가 최초 발표 되었을 때보다 많이 발전해서 실무에서 사용해도 무리가 없을 것 같다고 판단이 되었기 때문입니다. (안되면 롤백하기도 쉽고)


대부분의 UIKit + RxSwift 기반의 앱은 MVVM 디자인 패턴을 채택하여 사용하고 있고, 저희 앱 역시 그렇게 되어 있습니다. 따라서 최초에 리팩토링을 진행할 때는 아주 단순히 UIKit 기반의 UI를 SwiftUI로 수정하고 RxSwift를 Combine으로 옮겼으니 디자인 패턴도 역시 MVVM을 그대로 유지해서 쓰면 되지 않을까?라는 단순한 생각의 흐름이었습니다. 생각은 단순하게, 실행은 재빠르게 를 삶의 모토로 삼고 있기 때문에, 


일단 해보겠습니다.

가장 먼저 리팩토링을 진행한 곳은 API 호출하는 부분이었는데요. 기존 코드는 아래의 형식을 따릅니다.



간단하게 Network의 결괏값을 Single로 내보내는 함수입니다.

이 부분을 Combine으로 바꾸면 어떻게 될까요?



Combine의 Future를 통해 통신 작업에 대한 결괏값을 성공/실패에 따라 내보내고, 

이를 eraseToAnyPublisher를 이용해 최종적으로 깔끔한 형태의 Publisher를 내보냈습니다.


참 쉽죠? eraseToAnyPublisher를 사용하지 않았을 때의 단점에 대해서는 잘 정리되어 있는 블로그가 있어서, 아래 글을 참고하시면 좋을 것 같습니다.


https://0urtrees.tistory.com/366


이제 만들어진 request를 가지고 간단한 Service 단을 아래와 같이 만들어보겠습니다.



id와 text를 가지고 있는 아주 간단한 구조체의 리스트를 받는 서비스 단입니다.

이 부분은 기존에 쓰던 Single로 받던 부분을 AnyPublisher로 변경하는 것 외에는 크게 달라지는 부분은 없네요.


다음으로는 로직 부분을 살펴 보도록 하겠습니다.

기존 디자이너앱은 ViewModel에서 Input - Output 구조를 가지고 있습니다.

간단하게 버튼을 탭 하면 서비스의 get 함수를 호출하여 items로 내보내는 구조입니다.



해당 ViewModel을 Combine을 이용해서 리팩토링을 해보도록 하겠습니다.

UIKit + RxSwift에 적합한 Input-Output 패턴 대신에 SwiftUI + Combine에서 제공해 주는 @Published를 사용해서 진행해 보도록 하겠습니다.



왜 ViewModel 이 아니라 Store 인가요..?

저 역시 처음에는 아무 생각 없이 ViewModel이라고 명명하고 진행해 봤는데, 이게 진짜 뷰모델이 맞나.. 싶고, 뭔가 두 번 일하는 기분을 지울 수가 없었는데요. 다행히 저 혼자만 이런 걸 느끼는 게 아니고 다른 개발자분들도 비슷하게 생각하셨던 것 같습니다. 애플 개발자 포럼에 있는 글이 대표적인데요.


https://developer.apple.com/forums/thread/699003


이 사진 하나로 설명할 수 있을 것 같습니다.


SwiftUI를 사용할 때는 굳이 ViewModel을 억지로 만들 필요는 없다..!라는 게 이 스레드의 주요 골자입니다.


SwiftUI의 View 가 @State를 통해 기존의 ViewModel 이 하는 역할을 대신할 수 있기 때문이죠!


하지만 View에 모든 것을 진행하려고 하니, 로직이 뷰와 분리되지 않아 향후 Unit Test를 진행할 때 문제가 생길 수도 있을 것 같아, 이 부분을 Store라는 부분으로 분리하였습니다. 


ViewModel 이 원래 의미하는 View를 추상화하는 개념이 아닌, View의 상태를 ‘저장’하고 있겠다는 의지가 담김 네이밍으로 봐주시면 좋을 것 같고, 전체적으로 아래의 스레드와 글을 참고하여 제 입맛에 맞게 바꾸어보았습니다.


https://developer.apple.com/forums/thread/699003?answerId=716813022#716813022

https://gist.github.com/unnnyong/439555659aa04bbbf78b2fcae9de7661


자 이제 다 와 갑니다.

UIKit를 SwiftUI로 바꾸러 가봅시다!

아래는 간단한 버튼과 테이블뷰로 구성된 기존의 뷰컨트롤러입니다.



MVVM 구조이다 보니 bind 함수에서 button과 input을, output을 tableView에 바인딩을 해주었습니다.

하지만 SwiftUI에서는 위에서 말씀드렸듯이 State를 통해 상태를 관찰할 수 있기 때문에 별도의 binding 함수가 필요 없습니다.



훨씬 간단하게 Button의 action에 store.loadData()을 넣어주고,

store의 items를 리스트에 그대로 넣어주면 리스트 뷰가 완성이 됩니다.

SwiftUI의 직관적인 선언적 UI 방식이 눈에 띄네요.


이렇게 TestViewController의 모든 부분을 SwiftUI와 Combine으로 깔끔하게 리팩토링을 해냈습니다.

생각보다는 어렵지 않죠?


시작이 반

리팩토링 작업은 이제 시작해서 삽을 뜬 정도이고, 아직 모든 화면과 로직을 변경하려면 갈길이 멀지만 시작을 했다는 것에 큰 의의를 두고 있습니다. 아직 SwiftUI를 UIKit 마냥 쓸 수 있지도 않아서 iOS 팀 내에서 자체적으로 스터디도 진행하려고 하고 있는데요. 2023년 동안 열심히 달려서 나머지 반도 잘 완료하고 싶다는 생각입니다.


앞으로 종종 SwiftUI와 Combine에 대한 글을 써보려고 하니, 

많은 관심과 구독 좋아요 알람설정도 부탁드립니다.


그럼 다음에 또 만나요 안녕~~~~~~

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari