[RxSwift] RxSwift 개념 + 이를 활용한 동적인 로그인 입력창 만들기
👨🏻‍💻iOS 공부/RxSwift

[RxSwift] RxSwift 개념 + 이를 활용한 동적인 로그인 입력창 만들기

728x90
반응형

그 유명한 RxSwift...

귀에 딱지가 앉도록 RxSwift에 대한 말들을 들어왔을 것이다. 

실제로도 채용 공고 상에 자격요건/우대사항에 항상 존재하고 있는 것을 알 수 있다. 

 

대충 iOS와 관련된 채용 공고만 검색해봐도 Reactive, RxSwift라는 단어는 빠지지않고 등장하고 있는 걸 알 수 있다. 

 

K사
K 스타트업 
P 스타트업

 

 

이에 RxSwift 강의로(?) 유명하신 곰튀김님의 시즌0 RxSwift 강의를 듣고 내용을 정리해보겠습니다! 

(그저 빛...🐻)

 

강의를 기반으로 조금 더 실험할 수 있는 부분은 추가하면서 응용도 해보고 이해했는지 확인도 해볼 예정입니다. 

 


Rx 홈페이지 둘러보기

우선 Rx(Reactive Extention) 공식사이트를 살펴보자. 

 

상당히 많은 언어에 대해서 Rx를 제공하고 있는 모습을 볼 수 있다. 

즉 RxSwift는 Swift 전용 Rx인 것이며, Reactive programming을 돕기 위한 도구일 뿐인 것이다!

 

ReactiveX 메인 홈

An API for asynchronous programming with observable streams
: observablestream을 활용한 비동기 프로그래밍을 위한 API이다

대략 키워드가 3개 정도 보인다. 우선 문장 자체를 해석해보자. 

 

1. 비동기 프로그래밍을 위한 "도구"이다. 

2. 이 모든 것들이 stream상에서 이루어진다. 

3. 변화에 반응하기 위해 stream은 관찰(observable) 가능해야한다.

 

ID를 적었을 때 ID조건에 부합한지, 버튼을 눌러 이미지를 로드할 때, 오디오를 실행하는 경우 등 다양한 순간에서 비동기 프로그래밍이 필요하다. 

Rx의 구성 요소

이러한 Rx는 세 가지 구성요소(Observer를 가지고 있는데, 하나하나 차례대로 봐보자. 

 

Observable

Observable은 이벤트 시퀀스를 비동기적으로 생성하는 기능을 갖고 있다. 여기서 observable가 이벤트를 발생시키는 것을 emit(방출)이라고 한다. 쉽게 이야기하면 관찰 가능한(observable) 데이터의 흐름(stream)이라고 보면 된다. 아래 그림으로 더 쉽게 이해해 볼 수 있다. 

 

Stream상의 Observable

화살표의 방향에 따라 데이터가 emit되는 것이다. observable은 세 가지 이벤트만을 emit할 수 있다. 

각 이벤트들은 subscribe(구독)을 통해 반환될 수 있다. 즉 stream의 변화를 구독, 관찰하며 상시 지켜본다는 것과 같은 의미이다. 

 

  • next : 새로운 항목을 방출(emit)할 때 마다 호출하는 이벤트
  • error : Observable이 값을 방출(emit)하다가 오류가 발생하는 경우 error를 반환하고 종료된다.
  • completed : Observable이 정상적으로 값들을 다 배출해내고 난 후 이벤트 시퀀스를 종료시키는 이벤트이다. 

자 예를 들어, 위처럼  1 2 3이 있다고 할 때, next를 통해 1 2 3이 배출될 수 있다. 만약 도중에 결함이 있어 중단된다면 error가 출력되고, 문제 없이 끝까지 정상적으로 배출이 된다면 completed가 호출된다. 

 

emailField.rx.text.orEmpty
    .subscribe(onNext: { str in
      // manage text
    },
    onError: { error in
      // Display error to user
    },
    onCompleted: {
      // Use text
    })

 

Operator

이름 그대로 연산자와 비슷하게, observable에서 받은 이벤트들을 Rx Operator를 변환하고 처리하여 출력 가능하게 한다. 

 

operator의 종류는 굉장히 많기 때문에 다 외우기는 어려울 것 같고, 필수적인 것들을 위주로 살펴보고 필요할 때 찾아보는게 좋을 것 같다. Operator들은 다음 사이트에서 확인 가능하다. [Rx의 Operators]

 

아래의 예시 중 filter나 map처럼 특정하게 변환해주는 역할의 것들을 말한다. 

idField.rx.text
    .filter { $0 != nil }
    .map { $0! }
    .map(checkEmailValid)
    .subscribe(onNext: { b in
    // b: Bool
    })
    .disposed(by: disposeBag)

 

위 코드를 해석해보자.

1. 텍스트필드에서 텍스트가 nil이 아닌 것을 필터링하여 아래 스트림에 넘긴다
2. 위 스트림에서 받은 텍스트를 강제 언래핑한다. 
3. checkEmailValid 메서드에 넘겨 값을 얻는다. 
4. subscribe를 통해 onNext에 작업을 명시해준다. 
5. 이후 종료를 위해 disposeBag에 담아준다. 
 
* dispose를 사용하지 않으면 등록된 observable이 사라지지 않아 메모리 누수(memory leak)이 발생한다. 매번 다 다른 observable을 dispose() 해주기 어려우니 disposeBag에 담아 한 꺼번에 메모리에서 해제되도록 도와준다. 

초기화는 새로운 DisposeBag 객체를 넣어주면 된다.
disposeBag = DisposeBag()

 

Schedulers

 

Rx의 스케줄러는 GCD의 DispatchQueue와 유사하다. 하지만 Rx가 좀 더 쉽고 강력하다는데 그 차이가 있다. 

마찬가지로 main 쓰레드에서 돌릴지, 혹은 병렬적으로 작업을 수행할지 정하는데 필요한 요소이며, 다음과 같이 사용할 수 있다. 

 

이 경우 observeOn 메서드를 통해 스레드를 정해준 케이스이다. 

Observable.just("800x600")
    .map { $0.replacingOccurrences(of: "x", with: "/") }
    .map { "https://picsum.photos/\($0)/?random" }
    .map { URL(string: $0) }
    .filter { $0 != nil }
    .map { $0! }
    .observeOn(ConcurrentDispatchQueueScheduler(qos: .default))  // 1번
    .map { try Data(contentsOf: $0) }
    .map { UIImage(data: $0) }
    .observeOn(MainScheduler.instance)  // 2번
    .subscribe(onNext: { image in
    self.imageView.image = image
    })
    .disposed(by: disposeBag)

observeOn의 경우 선언된 줄 이후 부터의 스레드를 정해준다. 즉 1번 아래부터는 concurrent queue에서 작업이 처리되고 UI 이벤트 처리가 필요한 부분은 2번에서 다시 메인 쓰레드로 작업을 옮겨서 수행하라는 코드가 있기에 메인쓰레드에서 실행되게 된다. 

 

즉 바로 아래부터 어떤 스레드에서 작업될 지 정해줄 수 있는 것이다. 

 

매번 이렇게 해주기 귀찮긴 할 것 이다.... 그래서 대신 사용할 수 있는 subscribeOn이라는 것이 있다. 

 

Observable.just("800x600")
    .map { $0.replacingOccurrences(of: "x", with: "/") }
    .map { "https://picsum.photos/\($0)/?random" }
    .map { URL(string: $0) }
    .filter { $0 != nil }
    .map { $0! }
    .map { try Data(contentsOf: $0) }
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default))  // 1번    
    .map { UIImage(data: $0) }
    .observeOn(MainScheduler.instance)  // 2번
    .subscribe(onNext: { image in
    self.imageView.image = image
    })
    .disposed(by: disposeBag)

자 다시 봐보자. 

 

Data로 변환하는 부분이 시간이 많이 걸려서 이전에 observeOn을 사용하여 concurrent 하게 작업을 처리해줬었다. 

근데 이제 보니 Data로 변환해주는 코드 아래에 subscribeOn이 적혀있는 것을 볼 수 있다...

 

에러가 반환될까..??

아니다!!

 

subscribeOn은 observable이 연산(operate)되는 시점에 특정 스케줄러를 정해주는 것이기 때문에, 아무데나 적어주어도 맨 위에 observeOn을 한 효과를 볼 수 있다. 그러니 subscribeOn을 사용하게 되면 해당 observable은 특정 스케줄러로 정해지게 되는 것이다.  이 점을 유의하면 상황에 맞게 좀 더 편리하게 사용할 수 있다. 

 

간략하게 정리해보자면 다음과 같다. 

  • observeOn : operators나 subscribe 작업을 개별적인 스케줄러에서 사용하고 싶을 때 
  • subscribeOn : observable을 특정 스케줄러에서 생성하고 싶을 때 사용
추가적으로 .rx가 UIKit 요소에 붙어있는 것을 볼 수 있는데 이는 RxCocoa덕에 가능한 것이다. 

RxCocoa란 다양한 프로토콜을 확장한 것 뿐만 아니라 UIKit을 위한 rx 영역을 제공하는 프레임워크이다.
이후 값을 넘겨줄 때 사용할 bind(to: ) 같은 메서드를 제공해준다. 

RxSwift 로그인 정보 입력에 따른 동적 변화 예제

자 이제 간단하게 RxSwift를 활용한 로그인 예제를 봐보자. (초기 코드가 아닌 완성된 코드 위주로 봐보자)

 

우선 구현할 기능들은 다음과 같다. 

 

1. 이메일은 @와 점(.)을 포함하고 있어야 한다. 
    1-1. 조건에 충족할 경우 초록불 표시, 아니라면 빨간불
2. 비밀번호는 5자리 이상이어야 한다. 
    2-1. 조건에 충족할 경우 초록불 표시, 아니라면 빨간불
3. 로그인 버튼은 1,2번이 모두 충족되어야 활성화된다.  

이에 필요한 텍스트필드나, 뷰는 생성이 되었다고 가정한 상태로 시작해보자. 

 

우선은 ID를 검사하기 위해서는 다음과 같은 일련의 과정이 필요하다 

 

1. ID 텍스트필드를 통해 텍스트를 받아온다. 
2. 텍스트가 조건에 부합하는지 확인한다. 
3. 부합 여부를 가지고 초록색/빨간색 불을 켤지 정한다. 

 

우선 텍스트필드로 부터 텍스트를 받아 저장해보자. 

 

let idInputText: BehaviorSubject<String> = BehaviorSubject(value: "")

갑자기 처음보는 BehaviorSubject 라는게 등장했다... 뭘까.. 고민하고 좌절하지 말고 공식 문서를 보면 된다.

 

BehaviorSubject

 

observer가 BehaviorSubject를 구독(subscribe)하게 되면 가장 최근 아이템을 배출하게 된다. 즉, 초기값을 가지며 변경이 발생할 경우 최근 변경값을 저장하는 역할을 한다. 

 

위 그림에서처럼 핑크색 공을 초기값으로 가지다가, 빨간색 공이 들어오니 빨간색 공을 아래 스트림으로 보내준 것을 볼 수 있다. 쭉 진행하다가. 초록색 공을 받을 상태에서, 다시 구독하게 되면 초록색을 초기값으로 여겨서 아래 스트림을로 보내주게 되는 것이다. 앞서 말한 것 처럼 최근 변경값을 저장하고 있는 것이다. 

 

중간에 error가 있을 때

중간에 Error가 있었다면, 이후 구독했을 때 초기값이나 최근 변경값을 내보내는 것이 아니라 에러값을 내뱉게 된다. 

 

그렇기에 텍스트필드의 텍스트를 저장하기에 딱인 것이다! "감"을 치면 "감"이 저장되고, "감자"를 치면 "감자"가 저장되는 것이다. 이를 통해 조건에 부합하는지 여부를 파악할 수 있고, 검색창에 자동완성을 위해 사용할 수 있다. 

 

우선 dispose 해줄 것들을 담아줄 DisposeBag을 먼저 생성한다.

var disposeBag = DisposeBag()

자 이제 ID 텍스트 값을 담아줄 idInputText를 만들었으니 이제 텍스트필드에서 값을 할당해주면 된다. 

idField.rx.text.orEmpty
    .bind(to: idInputText)
    .disposed(by: disposeBag)

(bind의 경우 A의 정보를 B에 반영해준다 정도로 알고 있으면 된다. 이후에 더 자세하게 알아볼 것이다!)

위 경우 idField의 텍스트 값을 idInputText에 담아주는 역할을 한다. 

 

자 이제 1번은 끝났고 2번(텍스트가 조건에 부합하는지) 을 처리해주자. 

마찬가지로 최근 변경값을 담을 subject를 생성해주고 bind를 통해 값을 넘겨주자 

 

let idValid: BehaviorSubject<Bool> = BehaviorSubject(value: false)

idInputText
    .map(checkEmailValid)
    .bind(to: idValid)
    .disposed(by: disposeBag)
    
private func checkEmailValid(_ email: String) -> Bool {
    return email.contains("@") && email.contains(".")
}

 

자 이제 idValid라는 subject가 "ID가 조건에 맞는지 여부"를 저장하게 되었다. 이제 idValid를 가지고 켜질 불의 색을 정해주면 되는 것이다. 그러기 전에 비밀번호도 똑같은 과정으로 세팅해주면 된다. 

 

let pwInputText: BehaviorSubject<String> = BehaviorSubject(value: "")
let pwValid: BehaviorSubject<Bool> = BehaviorSubject(value: false)

pwField.rx.text.orEmpty
    .bind(to: pwInputText)
    .disposed(by: disposeBag)
        
pwInputText
    .map(checkPasswordValid)
    .bind(to: pwValid)
    .disposed(by: disposeBag)
    
private func checkPasswordValid(_ password: String) -> Bool {
    return password.count > 5
}

자 이제 데이터를 입력하는 단계는 모두 마쳤고 이제 이 값을 활용하여 UI에 변화를 주면 된다. 

 

// IdValid(ID가 유효한지) 값을 기준으로 UI변경
idValid.subscribeOn(MainScheduler.instance) // UI 변경은 메인 쓰레드에서
    .subscribe(onNext: { b in
    if b {
    	self.idValidView.backgroundColor = .green
    } else {
    	self.idValidView.backgroundColor = .red
    }
}).disposed(by: disposeBag)

// pwValid(pw가 유효한지) 값을 기준으로 UI변경
pwValid.subscribeOn(MainScheduler.instance) // UI 변경은 메인 쓰레드에서
    .subscribe(onNext: { b in
    if b {
    	self.pwValidView.backgroundColor = .green
    } else {
    	self.pwValidView.backgroundColor = .red
    }
}).disposed(by: disposeBag)

자 그러면 이제 1,2번에 대한 처리는 모두 마친 것이다. 

이제 ID, pw 입력창에 정보를 써나가면 조건에 따라서 알맞는 불이 켜지게 된다. 

 

마지막으로 로그인 버튼을 활성화하는 과정을 보자.

 

로그인 버튼의 경우 ID도 유효하고, 비밀번호도 유효해야한다. 조금 감이 오는가..?

바로 이전에 저장해두었던 idValid와 pwValid를 활용해야 한다!

 

Observable.combineLatest(idValid, pwValid, resultSelector: { $0 && $1 })
    .subscribe(onNext: { b in
        self.loginButton.isEnabled = b
        if b {
            UIView.animate(withDuration: 1) {
            	self.loginButton.backgroundColor = .black
            }
        } else {
            self.loginButton.backgroundColor = .gray
        }
    })
    .disposed(by: disposeBag)

자 여기서도 combineLatest라는 새로운 키워드를 볼 수 있다. 

 

combineLatest는 예를 들어, 2개의 Observable이 배출(emit)된다고 할 때, 가장 최근에 배출되었던 값을 기준으로 하여 정의된 함수에 따라 실행되는 것이다. 말이 조금 어려울 수 있는데 예시를 보고 이해해보자.

combineLatest

공식 문서에 있는 마블을 가져온 것이다. 

 

자 정의를 다시 보면, "최근값"을 기준으로 연산된다고 했었다. 이를 염두해두고 위 사진을 봐보자. 

지금 최상단 스트림에서는 1이 먼저 나오고 그 다음 두 번째 스트림에서는 A가 나오고 있다. 이에 A를 기준으로 두 observable의 최근 값인 1과 A가 더해져서 1A가 나오게 되는 것이다. 그 이후 2가 등장하여 2A가 되고, B가 등장하여 2B... 계속 반복되는 것이다. 

 

표로 살펴보면 아래와 같다. 

첫 번째 스트림 두 번째 스트림 결과
1 - -
1 A 1A
2 A 2A
2 B 2B
3 B 3B
: : :

 

아무튼 이러한 operator를 가지고 idValid와 pwValid를 비교하는 것이다. resultSelector에 보면 논리연산자( {$0 && $1 } )가 들어있는 것을 알 수 있다.

 

즉, 두 개 모두 유효해야 true가 반환되는 것이다. true가 반환되면 isEnabled 또한 true가 되고, 이에 따라 색이 변하는 애니메이션이 작동하게 되는 것이다. 이후 로그인 버튼은 활성화되며, 로그인 할 수 있는 것이다. 

 

자 이제 어떻게 작동하는지 살펴보자!

 

로그인 뷰

 

정상적으로 잘 동작하고 있는 모습을 확인해 볼 수 있다!


이렇게 RxSwift에 입문해버렸다... 

 

생각보다 편리하고, 많은 곳에 다양하게 활용될 수 있을 것 같다. 그리고 항상 따라다니는, 따라오는 MVVM에 결합하여 토이 프로젝트에 사용해봐야겠다. 

 

Ref: 

https://www.youtube.com/watch?v=gzJj_4X28wM&list=PL03rJBlpwTaAh5zfc8KWALc3ADgugJwjq&index=10

728x90
반응형