티스토리 뷰

iOS

[iOS] RxSwift 예제로 알아보기 (2)

yanni13 2024. 11. 7. 19:56

 

 

오늘은 RxSwift를 예제로 한번 알아보자! 

지난번엔 기본적인 개념과 RxSwift에서 중요한 요소들을 한번 짚고 넘어갔었는데 

오늘은 예제를 통해서 RxSwift를 적용했을 때 어떤 장단점이 있는지를 살펴보고자 한다.

 

 

[iOS] RxSwift 알아보기 (1)

개발을 하다 보면 비동기 이벤트를 많이 다루게 된다. 버튼을 눌렀을 때나,api 호출을 했을 때 응답을 받아올 때 이외에도 여러 가지 기능에서 비동기 프로그래밍이 필요하다.  이 비동기 프로

yanni13.tistory.com

 

 

📚 들어가기 전

# Podfile
use_frameworks!

target 'YOUR_TARGET_NAME' do
    pod 'RxSwift', '6.8.0'
    pod 'RxCocoa', '6.8.0'
end

# RxTest and RxBlocking make the most sense in the context of unit/integration tests
target 'YOUR_TESTING_TARGET' do
    pod 'RxBlocking', '6.8.0'
    pod 'RxTest', '6.8.0'
end

 

Rxswift를 사용하려면 코코아팟을 깔고 해당 프레임워크를 Podfile에 넣어준 후에 pod install을 해줘야 한다

 

Rxcocoa는 RxSwift와 함께 사용되는 라이브러리로 주로 UIKit과 많이 사용한다고 한다. RxCocoa를 사용하는 이유는 RxSwift만으로는 데이터 바인딩을 UI와 직접적으로 할 수 없어서 RxCocoa를 사용하는데, SwiftUI에서는 @Published와 @ObservedObject를 사용해 UI와 직접적으로 데이터 바인딩이 가능하기 때문이다!

 


예제로 알아보기 

 

 

 

위와 같이 사용자 정보 조회 버튼을 눌렀을 때 가지고 있는 User 데이터를 view에 표시하고, 사용자 이름 변경하기 버튼을 통해 변경된 데이터를 뷰에 업데이트하는 기능을 RxSwift로 구현해보고자 한다.

 

핵심기능

  • 사용자 정보 조회
  • 사용자 이름 변경 후 업데이트

 

Model

import Foundation

struct User: Equatable, Identifiable {
    let id: Int
    var name: String
    let username: String
    let phoneNumber: String
    
    static let dummyUser = User(id: 1, name: "신얀", username: "yanni", phoneNumber: "01012341234")
}

 

사용자의 고유한 id값, 이름, 아이디, 휴대폰 번호를 담고 있는 User라는 모델을 구조체로 선언해주었다.

그리고 사용자 정보 조회에 필요한 더미데이터를 모델 내부에서 구현해 주었다

 

 

ViewModel

import Foundation
import SwiftUI
import RxSwift
import RxCocoa


class ViewModel: ObservableObject {
    @Published var data: String = ""
    @Published var user: User = User.dummyUser
    private let disposeBag = DisposeBag()
    
    func fetchData() {
        // Observable로 비동기 작업 처리
        Observable.just(user)
            .map { user in
                "이름: \(user.name), 사용자 이름: \(user.username), 전화번호: \(user.phoneNumber)"
            }            .delay(.seconds(2), scheduler: MainScheduler.instance)  // 2초 지연
            .subscribe(onNext: { [weak self] fetchedData in
                self?.data = fetchedData
            })
            .disposed(by: disposeBag)
    }
    
    func updateName(newName: String) {
        // 새로운 이름으로 사용자 정보 업데이트
        user.name = newName
    }
}

 

 

이전 블로그에서 설명했듯이 RxSwift에서 가장 중요한 3 요소가 존재한다!

바로 Observable, Observer, Operation이라고 설명했었는데 코드를 보면 disposed도 나오고 DisposeBag도 나온다.

 

설명하지 않았던 disposable과 dispose에 대한 관계에 대해 먼저 알아보자

 

 

Disposable

  • 구독을 해제하고 싶을 때 dispose()를 호출하여 구독을 해제한다.

subscribe를 통해 관찰하고 싶은 대상을 구독할 수 있었다. 하지만 이 더 이상 사용하지 않는데 계속해서 값이 변경되고 있는지를 구독하여 관찰하고 있는 것은 많은 메모리 누수를 일으켜 앱이 느려지게 하는 원인이 될 수 있다.

따라서 subscribe로 구독할 때 반환값으로 Disposable를 반환하게 되는데 Disposable은 프로토콜이므로, 구독을 해제할 수 있는 dispose 메서드를 호출하여 Observable에 대한 구독을 해제할 수 있도록 한다.

 

DisposeBag

  • Disposable 객체를 담는 배열
  • deinit 할 때 배열에 추가되어 있는 disposable들을 dispose 시켜주는 것

구독하고 있는 subscribe가 여러 개 있을 경우 그에 해당하는 구독을 하나하나 해제를 해주게 될 경우 순서가 꼬일 수도 있고 해제되면 안 되는 게 해제될 수도 있다. 뿐만 아니라 메모리 해제를 위해 필요한 코드(= 기능 적인 코드)가 아닌 코드들이 길게 차지해서 자칫하면 코드 내에서 혼동을 야기할 수 있다!

 

분명 폴더이름은 뷰모델인데 메모리 해제하고 있는 코드가 대다수를 차지하게 될 수도 있음

이런 점들을 방지하고자 메모리가 해제돼야 하는 것들을 배열에 모아두고 구독을 해제할 때 한꺼번에 없애겠다!라는 의미!!

 

그럼 Disposebag이 호출되는 시점은 언제일까?

메모리 구독 해제를 하려고 할 때 heap에 할당한 변수에 nil값을 직접적으로 주거나, heap 영역에서 disposebag을 내리도록 구현할 텐데 그럼 메모리에서 내려간 disposebag이 담고 있는 disposebag 또한 없어지게 되는 원리이다.

 

 

이제 코드로 돌아가서 살펴보자

 

fetchData()

뷰모델의 fetchData를 호출했을 때 사용자의 정보를 담고 있는 user객체가 반환되어야 한다. 따라서 Observable.just(user)를 통해서 user객체를 방출하는 Observable을 생성하고 있다.

 

그리고 문자열과 user model의 데이터를 맵핑시켜 주며, 임시의 지연시간 2초를 통해 비동기 로직을 확인하고자 하였다.

 

SubScribe를 통해 Observable이 구독되게 된다.

그럼 Observable이 방출하는 값인 User객체를 받아 처리할 수 있게 된다.

 

그리고 dispose를 통해서 최종적으로 구독을 해제하게 되는 흐름이다!

 

updateName()

updateName 메서드는 view에서 받아온 이름이 변경되었다면 그 값으로 user 객체를 업데이트시키는 로직이다.

 

 

🤔 장단점

RxSwift가 이렇게 사용하는구나~ 정도를 알아보긴 했지만 여전히 비동기 처리로 하면 할 수 있을 거 같은데 굳이?? 싶었다.

굳이 지금도 비동기 처리를 할 수 있는데 RxSwift를 써야 하는 이유와 장점을 한눈에 파악하기 어려웠기 때문에 DispatchQueue와 Notification을 사용해서 위에 기능을 똑같이 구현해 보고 장단점을 파악해보고자 한다.

 

 

1️⃣ DisPatchQueue와 Notification을 사용했을 때 

View

.edgesIgnoringSafeArea(.all)
.onReceive(NotificationCenter.default.publisher(for: .dataFetched)) { _ in
	isDataFetched = true
}

 

view는 변경사항이 거의 없고 위의 코드 한 줄 추가되었다.

 

onReceive를 통해 NotificationCenter의 dataFetch를 구독하여 dataFetched라는 애의 이벤트가 발생했을 때마다 isDataFetched값이 true가 되어 뷰를 자동으로 업로드해준다.

 

ViewModel

import Foundation
import SwiftUI


class ViewModel: ObservableObject {
    @Published var data: String = ""
    @Published var user: User = User.dummyUser

    func fetchData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let fetchedData = "이름: \(self.user.name), 사용자 이름: \(self.user.username), 전화번호: \(self.user.phoneNumber)"
            self.data = fetchedData
            NotificationCenter.default.post(name: .dataFetched, object: nil)
        }
    }
    
    func updateName(newName: String) {
        user.name = newName
        NotificationCenter.default.post(name: .nameUpdated, object: nil)
    }
}


extension Notification.Name {
    static let dataFetched = Notification.Name("dataFetched")
    static let nameUpdated = Notification.Name("nameUpdated")
}

 

viewModel에서 데이터를 가져오는 fetchData메서드에서 비동기처리하는 로직이 DispatchQueue.main.asyncAfter로 대체되었고, NotificationCenter를 사용해서 dataFetched라는 이름으로 알림을 전송하게 된다.

 

그럼 아까 뷰에서 봤던 onReceive를 통해 알림을 받은 뷰에서 구독하고 있다가 준비된 데이터를 보여주게 되는 것이다.

 

이름 변경도 마찬가지로 updateName메서드가 호출되고 나서 NotificationCenter를 사용해서 nameUpdated라는 이름으로 알림을 전송하게 되고 그걸 구독하고 있던 다른 뷰에서 이름 변경이 감지되면 준비된 업데이트된 데이터를 보여주게 되는 형태가 된다.

 

더보기

참고:  object: nil은 알림이 특정 객체와 관계없이 모든 대상에 전송된다는 의미이다.

 

사실 이렇게만 보면 DispatchQueue + Notificenter를 사용해서 구현한 게 훨씬 더 코드가 간결하고 직관적으로 보인다.

 

하지만 프로젝트 규모가 조금 더 커지고 기능들이 복잡해지면 어떻게 될까?

 

📍RxSwift의 장점

 

1. RxSwift가 비동기 프로그래밍에서 closure나 notification보다 강력한 이점을 가지고 있는 것이 선언적인 방식으로 앱을 구축할 수 있고 다양한 이벤트가 발생했을 때 일관된 값을 보여줄 수 있음이다.

 

2. NotificationCenter 흐름이 observer를 처리할 때까지 대기상태로 들어가 동기적으로 처리되는 반면 RxSwift는 비동기적으로 흐름이 흘러가기 때문이다. 

또한 위에 코드로 확인해 봤을 때 Notification보다 RxSwift가 구독해제하는 방법이 간단하고 쉬워서 메모리 관리 측면에서 훨씬 유용하다.

 

3. Thread관리가 쉬워서 콜백지옥에서 벗어날 수 있다.

 

보통 api를 호출할 때 completion으로 계속 성공과 실패여부를 나눠서 성공했을 때와 실패했을 때의 로직을 구현하곤 한다.

Single, Completable과 같은 RxSwift의 특징적인 타입을 활용하면 콜백 지옥을 탈출하는 데 도움이 된다는데 이건 RxSwift가 어떻게 활용되는지를 더 공부해 봐야 알 수 있을 거 같긴 하다.

 

 

📍RxSwift의 단점

1. 위에 예시처럼 간단한 기능만을 담고 있는 프로젝트에서 적용할 시에는 부적절하다. 

 

지금 위에 예시처럼 사용자 기본 정보를 가져오고, 이름만 업데이트하는 프로젝트에서 RxSwift를 도입하는 것은 도입하지 않았을 때와의 차이를 느끼지도 못하게 될 수도 있다(공부목적이 아니라면)

 

2. 학습비용이 요구된다.

 

RxSwift를 사용하지 않고도 비동기를 사용할 수 있는 반면 RxSwift를 사용하고자 했을 때는 학습비용이 요구된다. 

따라서 어떤 걸 적용해야 할지는 프로젝트 규모와 시간, 자원을 보면서 잘 활용하며 선택해야 한다.

 

 


참고

https://babbab2.tistory.com/186

https://velog.io/@yoogail/RxSwift-DisposeBag-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0-1

https://hello-developer.tistory.com/82

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함