brunch

매거진 Delightroom

You can make anything
by writing

C.S.Lewis

by Chris Lee Nov 17. 2018

RxSwift 활용기 1. Form Validation

Reactive Extensions

최근 핫한 Reactive Programming을 우리 알라미 앱의 피드백 보내기 화면에 적용해보았다. 피드백 보내기 화면은 완전히 새로 개발하는 데다가, 유저가 하나의 폼을 채워서 보내는 화면이기 때문에 RxSwift 적용이 해볼 만했다. 이 글은 Reactive Programming의 장점과 구현 방법을 상세하게 다룬다.


Reactive Programming, RxSwift가 뭔가요?


Reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change. - Wikipedia

간단히 이야기하여 데이터 스트림과 그 변화의 전파에 기반한 비동기 프로그래밍 패러다임이다. 모든 이벤트나 데이터, 그리고 map, filter 등을 이용한 그 변형들이 스트림으로 전달되고 이 스트림은 최종적으로 자료를 처리하거나 UI 반영에 사용된다.


RxSwift는 ReactiveX - Observable stream 기반의 Reactive programming을 위한 API의 Swift 구현체이다. Opensource project로 이 링크에서 소스 및 소개글을 확인할 수 있다.


이 글의 목적은 Reactive Programming이나 RxSwift의 개론은 아니기 때문에, 더 자세한 내용은 위의 링크들을 참고 바란다.


왜 form validation에 RxSwift를 썼나요?


Validation은 기본적으로 다양한 input control로부터 입력들을 종합하여 최종적으로 form이 valid 한 지 결정하는 과정이다. 각 input control에 대해 개별적인 에러 표시나 비활성화 등 사용자가 form을 의도한 대로 사용할 수 있도록 가이드를 주는 것도 필요하다. RxSwift와 RxCocoa를 사용하면 이 입력들을 (이벤트) 데이터 스트림으로 해서, declaritive 하게 데이터 유효성을 검사하고, 손쉽게 종합하여 에러 메시지 표시, 확인 버튼 활성화 등 view layer에 결과를 반영할 수 있다.


구현해야 할 validation 규칙


다음 스크린샷은 우리 앱의 피드백 보내기 form이다. 각 form element마다 다음과 같은 validation규칙을 적용하고자 한다.


피드백 보내기 화면


(1) 유형은 반드시 선택되어야 함


(2) 메시지는 비어있지 않아야 함. 단 맨 처음에는 무조건 비어있을 것이기 때문에 그때는 에러 메시지가 나오면 안 되고, 한번 이상 수정한 후에 메시지가 비어있으면 에러 메시지가 나와야 함.


(3) 이메일은 비어있어도 됨. 이메일 주소가 입력되었다면 유효한 주소이어야 함. 이때 첫 번째 수정 중에는 유효성을 검사하여 보여주지 않고, 입력 완료 후에 유효성을 검사함. 처음 입력 시에는 부분적으로는 유효하지 않은 앞부분부터 입력할 수밖에 없기 때문.


(4) 보내기 버튼은 위의 3가지 조건중 하나라도 불만족하면 비활성화 상태이어야 함. 모두 만족하면 활성 상태로 변경.


RxSwift를 이용한 구현


1. 유형 유효성


유형의 유효성은 다음 Observable로 표현할 수 있다.


let isTypeValid: Observable<Bool> = selectedType.asObservable().map { $0 != nil } 

selectedType의 정의는,

var selectedType: Variable<FeedbackType?> = Variable(nil) 

로 UITableView에서 FeedbackType을 선택할 때 위 Variable을 다음과 같이 업데이트해준다.


func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {   
...    
    self.selectedType.value = feedbackTypes[row]      // or     self.selectedType.value = nil
 ...

선택이 되면 FeedbackType 타입의 값이 들어가고 선택이 안되면 nil이 들어가기 때문에 위의 Observable로 아무것도 선택이 안되었는지 체크가 가능하다.

*참고로 Variable은 현재는 deprecated된 API로, 이제는 BehaviorRelay를 써야 한다.


2. 메시지 유효성


메시지를 입력하는 UITextView 컨트롤 textView가 있을 때, 메시지 유효성은 다음과 같이 표현할 수 있다.

let isMessageValid = textView
    .rx
      .text
    .throttle(0.1, scheduler: MainScheduler.instance)
    .map { text -> Bool in
    return !(text?.isEmpty ?? true) // text should not be nil or empty 
}.distinctUntilChanged() 

무슨 일이 일어나고 있는가?


RxCocoa는 UITextView, UITableView 등 UIKit control들에 rx extension을 준다. 이 extension의 text property는 textView에 입력되는 텍스트 값의 stream이다.

여기서 throttle 은 너무 빠른 연속된 입력에 다 일일이 반응할 필요는 없기 때문에 걸어두었다. 0.1초 안에 하나의 이벤트만 통과시키게 된다.

다음 map에서 실제로 validation결과를 보고한다. textView의 text String 값이 비어있거나 nil이면 false, 아니면 true이다.

distinctUntilChanged()는 true가 false, 혹은 false가 true가 될 때만 이벤트를 발생시키도록 하기 위해 걸어주었다.

결국 isMessageValid는 textView의 내용이 바뀔 때마다 우리가 정한 (2)번 규칙에 따라 validation을 하여, 그 validation결과가 바뀔 때 보고해 주는 observable이 된다.


에러 메시지 표시는 isMessageValid를 다음과 같이 subscribe 해서 결과가 false이면 에러 메시지를 보여주고, 아니면 에러 메시지를 비운다.

isMessageValid.skip(1).subscribe(onNext: { [weak self] isValid in
    self?.messageValidationLabel?.text = isValid ? "" 
    : "*\(Localizations.MESSAGE_SHOULD_NOT_BE_EMPTY)"
}).disposed(by: disposeBag)


3. 이메일 주소 유효성


유효성 검사 부분은 기본적으로 메시지 유효성과 동일하다.


let isEmailValid = emailTextField
    .rx
    .text
    .throttle(0.1, scheduler: MainScheduler.instance)
    .map { [weak self] text -> Bool in
        guard let text = text, let isValid = self?.validateEmail(text) else { return false }
        return text.isEmpty || isValid  // email text should be either valid email or empty
}.distinctUntilChanged()

단 email validation을 위해  validateEmail()이라는 함수를 만들어 사용한다. 다음과 같이 생겼다. (이 함수는 추후 재사용과 separation of concern을 위하여 ValidationHelper와 같은 별도의 클래스로 뺴는것이 더 적절할 것이다.)

private func validateEmail(_ string: String) -> Bool {
    let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    return string.range(of: emailRegEx, options: .regularExpression) != nil
}


에러 메시지 표시 부분은 조금 더 복잡해진다.


먼저 emailTextField의 editingDidBegin 이벤트는 false, editingDidEnd 이벤트는 true로 바꿔 전달해주는 emailControlEvents 라는 Observable를 만들었다.

let emailControlEvents: Observable<Bool> = 
    Observable.merge([
        emailTextField.rx.controlEvent(.editingDidBegin).map { false }, 
        emailTextField.rx.controlEvent(.editingDidEnd).map { true }
    ])


isEmailValid와 emailControlEvents를 combineLatest로 묶어서 최신 validity정보와 emailTextField의 control event상태를 이용해 에러 메시지를 보여준다.


Observable.combineLatest(isEmailValid, emailControlEvents).flatMap { (isValid: Bool, editingDidEnd: Bool) -> Observable<String> in
    if isValid {
        return Observable.from(optional: "") // Immediately reflect valid result
    } else if editingDidEnd {
        return Observable.from(optional: "*\(Localizations.INVALID_EMAIL_ADDRESS)") // If
invalid, show at the end of editing
    } else {
        return .empty() // No need for change in other cases
    }
}.distinctUntilChanged().subscribe(onNext: { [weak self] msg in
    self?.emailValidationLabel?.text = msg
}).disposed(by: disposeBag)


4. 보내기 버튼 유효성


보내기 버튼 유효성은 위의 3개의 유효성을 and(&&) 연산자로 묶으면 완성된다.

let isEverythingValid = Observable.combineLatest(
        [isMessageValid, isEmailValid, isTypeValid]
    ) { $0[0] && $0[1] && $0[2] }

isEverythingValid 값을 UI에 반영하는 것은 bind를 사용해 간편하게 가능하다. sendButton은 isEverythingValid 값에 따라 자동으로 enable/disable 된다.

isEverythingValid.bind(to: sendButton.rx.isEnabled).disposed(by: disposeBag) 



RxSwift를 이용했을 때의 장점


1. 통합된 이벤트 컨트롤: 각 UIControl 마다 입력, 상태변화 이벤트 등을 여러 delegate 함수, Notification 등을 통해서 처리해야 했을 것이다. ViewController의 크기가 커지고 여러 함수 간의 state sharing을 위한 global variable이 필요했을 것이다.        

2. UI 업데이트의 간소화: 모든 변화가 결국  isEverythingValid라는 스트림으로 수렴하여 그 값에 따라서만 UI를 업데이트해주면 된다. RxSwift가 없이 택할 수 있는 가장 순진한 방법을 생각해본다면, 최신 validity state를 계산하고 UI를 업데이트해주는 루틴을 만들어놓고, 모든 가능한 데이터 변화 시점에 수동으로 UI 업데이트를 해야 했을 것이다.

3. 스트림 조작의 용이성: skip(), throttle(), distinctUntilChanged()와 같이 스트림 조작에 유용한 함수들이 미리 만들어져 있다. RxSwift가 없이는 비슷한 효과를 내는 코드를 직접 짰어야 할 것이다.

4. Declrative 코드: functional progamming을 이용하여 declarative 하게 validity check를 정의할 수 있다. 더 간결하고 원리를 이해하기 쉬우며 가독성이 더 좋은 코드가 된다.


결론


이 경험으로 RxSwift가 주는 장점을 확실히 느낄 수 있었다. RxSwift가 모든 것을 하기에 적절하지는 않을 수도 있고, 리소스 관리 등에 약점이 있는 것은 사실이다. 하지만 장점이 단점을 상회하는 경우 특히 비동기적인 작업을 통해 앱의 UI나 데이터 모델에 변화를 주는 부분에서는 점점 RxSwift를 채용하고 있다. 이 글이 RxSwift나 Reactive Programming에 관심을 가지려는 분들에게 조금이나마 도움이 되었으면 한다.


We’re hiring!


Reactive Programming과 같은 새로운 개발 패러다임을 마음껏 배우고 도전해 볼 수 있는 딜라이트룸에 관심이 있다면 아래 링크를 클릭!


지원 가능 포지션 확인하기 >> We're hiring!










⌘ + Shift + J




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