왜 RxSwift를 사용하나요?
왜 우리는 RxSwift와 같은 반응형 프로그래밍(Reactive Programming) 패러다임에 주목을 해야 할까요? RxSwift가 없더라도 우리는 지금까지 앱을 잘 만들어왔잖아요? RxSwift는 필수인가요? 이번 글에서는 이 질문에 대한 답을 찾고자 합니다. 결론부터 말씀드리자면 RxSwift는 프로그래밍 패러다임의 한 종류일 뿐 정답이 될 수는 없다고 생각합니다. 늘 그렇듯이, 유행은 항상 바뀌는 법이니까요.
RxSwift란 무엇인가요?
RxSwift는 Observable
시퀀스와 함수형 스타일의 Operator
를 기반으로 비동기적이고 이벤트 기반 코드를 구성하는데 도움을 주는 라이브러리로, 반응형 프로그래밍(Reactive Programming) 코드를 작성하는 데 많은 도움을 줍니다. 이 정의에서 반응형이라는 단어에 집중을 해보도록 하겠습니다. 반응형은 무슨 의미일까요?
반응형 vs. 명령형
명령형 프로그래밍
우리가 일반적으로 작성하는 코드는 명령형(Imperative) 스타일의 코드입니다. 명령형 프로그래밍은 프로그램이 어떻게 로직을 수행해야 하는지 세부적으로 지시합니다. 명령형 프로그래밍은 프로그램에게 할 일을 순차적으로 지시하며 원하는 결과물을 얻는 과정을 표현합니다. 마치 레시피를 따르는 것과 유사하죠. 레시피에는 우리가 어떻게 재료를 손질하고 볶으면 음식을 완성할 수 있는지 알려줍니다. 여기서 핵심은 어떻게 해야할 지라는 거죠. 아래는 일반적인 명령형 스타일의 코드를 보여줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var numbers = [1, 2, 3, 4, 5]
var newArray: [Int] = [] // 배열 선언 및 초기화
for number in numbers { // 배열을 순회하며
newArray.append(number * number) // 제곱한 요소를 새로운 배열에 추가
}
print(newArray) // 배열 출력
// numbers = [10, 11, 12, 13, 14, 15]
// ✏️ Console
// [1, 4, 9, 16, 25]
위 예제 코드에서 우리는 프로그램에게 새로운 배열을 선언・초기화하고, 배열을 순회하면서 요소를 제곱(^2)한 결과를 새로운 배열에 추가하고, 새로운 배열을 콘솔에 출력하라고 알려주고 있습니다. 이렇게 프로그램은 명시된 순서대로 로직을 수행하며, 최종적으로 새로운 배열을 콘솔에 출력하게 됩니다.
하지만 위 예제 코드의 끝에서 numbers
의 요소가 바뀐다고 가정해보겠습니다. numbers = [10, 11, 12, 13, 14, 15]
처럼 numbers
가 바뀐다고 하더라도 우리는 해당 배열 요소를 제곱한 결과를 콘솔에서 확인할 수 없습니다. 왜냐하면 우리는 프로그램에서 바뀐 numbers
에 배열을 순회하며 제곱한 요소를 새로운 배열에 추가하라는 명령을 주지 않았기 때문이죠.
명령형 프로그래밍의 가장 큰 특징 중 하나는 코드가 순차적으로 실행된다는 점입니다. 프로그램이 수행되는 도중에 일부 값이 바뀌더라도 이미 실행된 코드는 이를 알 방법이 없습니다. 이러한 접근 방식은 작은 규모의 프로그램에 적합하지만, 조금만 규모가 커지면 코드를 이해하고 유지 보수를 하는 데 어려움이 생길 수 있습니다. 이러한 문제를 해결하기 위해 나온 새로운 패러다임이 바로 반응형 프로그래밍입니다.
반응형 프로그래밍
반응형(Reactive) 코드는 프로그램 내 다양한 구성 요소 간 데이터의 비동기적 흐름에 중점을 두었습니다. 반응형 프로그래밍는 데이터를 단방향 스트림으로 처리하며, 데이터와 같은 주변 환경을 관찰하다가 변화가 감지된다면 연결된 실행 모델에게 이를 알려 필요한 작업을 수행하도록 지시합니다. 아래는 위 예제를 반응형 스타일의 코드로 바꾼 모습을 보여줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let squaredNumber = PublishSubject<[Int]>()
squaredNumber
.map { $0.map({ $0 * $0 }) } // 배열 요소를 제곱
.subscribe { print($0) } // 데이터를 관찰하는 실행 모델
.disposed(by: disposeBag)
squaredNumber.onNext([1, 2, 3, 4, 5]) // 변화된 데이터 전송
// do something...
squaredNumber.onNext([10, 11, 12, 13, 14, 15]) // 변화된 데이터 전송
squaredNumber.onCompleted()
// ✏️ Console
// next([1, 4, 9, 16, 25])
// next([100, 121, 144, 169, 196, 225])
// completed
위 예제 코드에서 우리는 배열 요소를 제곱하는 실행 모델을 한번만 정의해두고, 변화된 데이터를 전송할 때마다 이를 관찰하는 실행 모델이 필요한 작업을 수행해 콘솔에 출력하게 됩니다. 데이터가 변화될 때마다 배열 요소를 제곱하는 코드를 다시 작성하는 등 번거로운 작업을 할 필요가 없어졌습니다. 명령형 프로그래밍보다 코드가 더 간결해지며, 데이터의 흐름을 따라가기도 더 편해집니다.
비동기적 흐름
앞서, 반응형 프로그래밍는 다양한 구성 요소 간 데이터의 비동기적 흐름에 중점을 두고 있다고 언급한 바 있습니다. 여기서 비동기적 흐름(비동기 이벤트)이란 무슨 의미일까요? 왜 데이터를 비동기적으로 흐르게 해야 할까요? 데이터를 비동기로 흐르게 한다는 의미는 변화된 데이터를 전송하고, 이를 관찰하는 실행 모델이 메인 쓰레드가 아닌 쓰레드에서 이뤄진다는 뜻입니다.
아마 비동기적 흐름이라는 단어에 많은 의문을 표하시는 분이 계시리라 생각합니다. 만약 데이터가 동기적으로 흐르게 된다면 어떻게 될까요? 프로그램은 변화된 데이터를 전송받을 때까지 메인 쓰레드를 막을 겁니다. 이는 UI 업데이트를 블록(Block)해 프로그램을 멈추게 할 수 있습니다.
비동기 이벤트는 앞서 예제에서 살펴본 변화된 데이터 전송이 될 수도 있고, 키보드가 올라오거나 버튼을 클릭하는 행위가 될 수 있습니다. 유저가 언제 텍스트필드(Textfield)나 버튼을 클릭할 지는 아무도 모르기 때문에, 비동기적으로 관찰하다가 변화(클릭)이 감지되면 연결된 실행 모델이 필요한 작업을 수행합니다.1 이때, 실행 모델은 UI 업데이트와 같은 작업을 포함할 수 있습니다.
RxSwift를 사용하는 이유
반응형 프로그래밍의 특징을 살펴보았습니다. 이제 왜 써야 하는지 의문에 대한 답을 할 시간입니다. RxSwift를 프로젝트에 적용해보면서 아래와 같은 이점을 느낄 수 있었습니다.
비동기 코드 처리 용이: RxSwift는 비동기 작업을 간편하게 처리할 수 있습니다. 쓰레드 관리에 대한 복잡성을 줄여주며, 코드를 보다 직관적으로 만듭니다.
뷰 컨트롤러 간 데이터 전달 용이: RxSwift는
Observable
을 통해 데이터를 전달하므로 뷰 컨트롤러 간 데이터 전달이 간편해집니다. 이는 느슨한 결합을 촉진하며, 코드의 유연성을 향상시킵니다.코드 깊이를 줄일 수 있음: RxSwift를 사용하면 코드 깊이를 줄일 수 있습니다. 이는 코드의 가독성을 향상시키고 유지 보수를 더 쉽게 만듭니다.
선언적 코드 작성: RxSwift는 선언적인 스타일로 코드를 작성할 수 있습니다. 이는 코드의 의도를 명확하게 파악하고 가독성을 높여줍니다.
뷰 컨트롤러 간 데이터 전달이 쉬워집니다. 일반적으로 뷰 컨트롤러 간 데이터를 전달할 때는 Delegate 패턴이나 클로저를 사용합니다. 하지만 뷰 컨트롤러 간 의존성(Dependency)을 높이고, 테스트와 검증을 어렵게 만들 수 있습니다. 반면에 RxSwift는 이러한 패턴을 사용하지 않고도 단 몇 줄의 코드만으로도 목적을 달성할 수 있습니다. 이는 코드를 유지보수성을 향상시킵니다. 만약에 Delegate
패턴을 사용한다면
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
protocol CustomDelegate: AnyObject {
func changeNumber(_ number: Int?)
}
class FirstViewController: ViewController, CustomDelegate {
// <...전략...>
func changeNumber(_ number: Int?) {
guard let number = number else { return }
self.label.text = "\(number > 10 ? 10 : number)"
}
}
class SecondViewController: ViewController {
weak var delegate: CustomDelegate?
// <...전략...>
@IBAction func didTapButton(_ sender: UIButton) {
delegate.changeNumber(777)
}
}
위 예제 코드와 같이 작성해야 하겠죠. 이 코드를 RxSwift로 다시 작성하면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class FirstViewController: ViewController {
// <...전략...>
func bind() {
Repository.number
.compactMap { $0 }
.map { $0 > 10 ? 10 : $0 }
.map { "\($0)" }
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}
class SecondViewController: ViewController {
// <...전략...>
@IBAction func didTapButton(_ sender: UIButton) {
Repository.number.onNext(777)
}
}
이렇게 작성할 수 있습니다. 프로토콜이나 클로저를 별도로 선언해줄 필요가 없어집니다.
RxSwift의 또 다른 특징 중 하나는 선언적 스타일의 코드를 작성할 수 있다는 점입니다. 덕분에 코드의 의도를 명확하게 파악하고 가독성을 높여줍니다.
1
2
3
4
5
6
7
8
let _ = URLSession.shared.rx.data(request: url)
.retry(3)
.decode([Post].self)
.map { $0.count }
.subscribe {
print("Post Count: \($0)")
}
.disposed(by: disposeBag)
선언적 프로그래밍(Declarative Programming)은 무엇인가요?
명령형 프로그래밍 방식과 대비되는 개념으로 선언적 프로그래밍 방식이 있습니다. 명령형 프로그래밍 방식은 어떻게 해야 할지라면, 선언적 프로그래밍은 무엇을 할지에 더 초점을 맞추고 있습니다. 이 두 방식을 비유하자면 아래와 같습니다.
- 명령형 접근(How): “클러치를 밣고, 기어를 1단으로 올리세요. 그런 다음 액셀을 밣으면서 천천히 클러치를 떼세요. 그러면 전진할 수 있습니다.”
- 선언적 접근(What): “전진하세요.”
그러면 어떻게 모든 절차를 이렇게 줄일 수 있는지에 의문이 들 수 있습니다. 이는 선언적 접근을 위해서 명령형 방식으로 어떻게 전진하는지에 관한 내용이 추상화되어 있기 때문입니다. RxSwift는 선언적 프로그래밍 방식을 채택하고 있습니다. 덕분에 아무리 스트림이 길어지더라도 빠르게 의미하는 바를 파악할 수 있습니다.
마무리
반응형 프로그래밍은 프로그램의 규모가 커지면 커질수록 빛을 발합니다. 최소한의 노력으로 복잡한 로직을 쉽게 풀어낼 수 있습니다. 데이터의 비동기적 흐름은 뷰 컨트롤러 간 의존성(Dependency)를 줄여 유지보수성과 재사용성을 높여줍니다. 비동기 코드 처리를 용이하게 하며, 코드의 깊이도 줄여줍니다. 또한, 데이터의 비동기적 흐름은 데이터 바인딩(Binding)을 통해 추가 작업 없이도 UI를 최신 상태로 유지하도록 도와줍니다.
참고 자료
데이터가 비동기적으로 흐른다 하더라도 실행 모델이 무조건 백그라운드 쓰레드(Background Thread)에서 필요한 작업을 수행하는 건 아닙니다. (필요에 따라 백그라운드 쓰레드에서 수행하도록 스케줄러(Scheduler)를 변경할 수 있지만) 대부분 현재 작업 중인 쓰레드에서 필요한 작업을 수행합니다. ↩