안녕하세요!
오늘은 ReactiveX입니다!
각 핵심 개념들, 사용되는 연산자 그리고 RxSwift까지 갈 길이 멀지만, 이 글을 정리하면서 기초를 다져보려고 합니다.
ReactiveX를 보기 전에 Reactive programming, 반응형 프로그래밍이 뭘까요?
반응형 프로그래밍이란, 데이터 흐름과 변경 사항을 전파하는 데에 중점을 둔 선언형 프로그래밍 패러다임입니다.
처음에는 뭔 말인가 싶을 수도 있는데, 저는 ReactiveX의 개념을 정리하면서 아 그래서 그런 정의가 내려졌구나 싶었습니다.
그래서 여기서 깊은 설명보다는 아래에서 점점 알아가는 방향으로 설명해보겠습니다.
Rx는 관찰 가능한 흐름을 사용하여 비동기 및 이벤트 기반 프로그램을 작성하기 위한 라이브러리입니다.
관찰 가능한 데이터 스트림을 생성하는 역할을 가진 주체를 Observable이라고 표현합니다.
정확히는 Observable은 이벤트 혹은 데이터를 발행하고, 이 발행되는 것들을 관찰하기 때문에 관찰 가능하다라고 표현합니다.
(공식 문서에서는 Item을 발행한다고 표현합니다.)
여기서 발행되는 Item을 관찰하는 것이 Observer입니다.
Observer는 Observable을 구독합니다.
왜 구독한다는 표현을 쓸까요?
Observable에 데이터를 조회하고 변환하는 메커니즘을 설정합니다.
그리고 이 Observable이 이벤트를 발행하면, 해당 Observable의 Observer가 그 발행을 감지하고 준비된 연산을 실행해 결과를 냅니다.
Observer가 이벤트 발행을 하는지 안 하는지 계속 보다가 발행되면 감지해서 연산을 처리하는 이 메커니즘 때문에 구독한다라는 표현을 사용합니다.
앞서 Observable이 이벤트 혹은 데이터 / 아이템을 발행한다는 표현을 사용했습니다.
Rx에서는 이 발행하는 것을 Push라고 표현합니다.
기존 프로그래밍은 Pull 기반 방식입니다.
데이터 소비자가 필요할 때마다 데이터를 요청해서 받아냅니다.
하지만 Rx는 데이터가 변화할 때, 즉 Observable이 데이터를 변환할 때 Observer(소비자)에게 데이터를 전달합니다.
데이터를 변환하고 그걸 필요로 하는 소비자에게 전달하는 방식입니다.
소비자가 데이터 전송을 요청하는 방식과 데이터를 바꾼 주체가 전송 하는 방식으로 비교하면 Push와 Pull이 이해되죠?
여기까지 읽으셨다면 반응형 프로그래밍의 정의에서 변경 사항을 전파라는 표현이 이해되실 겁니다.
Observable이 변경 사항을 전파하고 그걸 받는 게 Observer인 것까지 이해되면 잘 읽어오신 겁니다!
보통 반응형 프로그래밍을 학습하고 Rx를 이해하겠지만, 저는 이 흐름대로 학습하는 게 두 개념을 익히기에 더 좋았습니다! (개인적인 방법일 수 있겠네요)
위 마블 다이어그램은 Rx 공식 문서에서 Observable과 Observer의 transformation을 설명하는 그림입니다.
이 그림을 읽어보니까 Rx의 큰 흐름을 이해하기 쉬웠습니다.
앞서 Observable이 Item을 발행한다라는 설명을 했는데, 그림의 맨 위 선에 위치한 도형들이 그 발행된 Item들입니다.
흐름 혹은 스트림과 같은 표현을 쓰는 이유를 그림에서 볼 수 있습니다.
시간이 흐르면서 Observable은 계속 Item을 push할 것이고, 그걸 데이터의 흐름으로 볼 수 있습니다.
그리고 저는 flip을 거치고 내려온 밑의 도형들에 대해 잠깐 혼란이 왔었습니다.
Observer의 연산까지 마친 Item들인가? 싶었습니다.
공식 문서의 Observable 문서를 쭉 읽고 Operators 문서를 잠시 보니 이해가 됐습니다.
공식 문서의 말을 가져오자면, 거의 모든 연산자들은 Observable 상에서 동작하고 Observable을 리턴합니다.
결론은 위 그림에서 result of the transformatioin이라고 설명되는 도형이 Observable에서 Operator가 실행되고 반환되는 Observable이라는 것입니다.
여기서 또 혼란이 왔습니다.
Observable 내에서 연산 처리해서 방출하는 게 있다까지는 이해가 됐습니다.
근데 왜 또 방출하는 걸 Observable이라고 표현할까요?
데이터가 아닌가요?
그래서 윗 부분만 읽었던 Operators 문서를 더 읽어봤습니다.
연산자는 기존 Observable을 받아서 연산하고, 그 결과를 새로운 Observable로 반환합니다.
그림 상에서 밑의 도형들도 흐름으로 연결되어 있는데, Rx에서는 연산 결과도 흐름으로 연결해서 다음 연산자에게 전달하는 것입니다.
이렇게 연산자들끼리 엮여있어 연산자 체인이라는 표현을 사용합니다.
그래서 연산자 체인은 원본 Observable과 독립적으로 실행되지 않고 순서대로 실행됩니다.
연산자는 먼저 실행된 연산자가 반환하는 Observable을 기반으로 동작합니다.
사실 연산자는 후속 글에서 공부하며 정리하려고 했지만, 이렇게 맛을 한 번 보고 지나가게 되네요. 😦
반응형 프로그래밍 정의에서 데이터 흐름이라는 표현이 기억나시나요?
"데이터 흐름과 변경 사항을 전파하는 데에 중점을 둔 ..."
이제 데이터 흐름과 변경 사항을 전파한다는 표현은 이해가 되실 겁니다!
이제 그 뒤에 나오는 표현, 선언형 프로그래밍 패러다임을 이해하기 위해 해당 개념이 Rx에 적용된 부분을 살펴봅시다.
선언형 프로그래밍은 어떻게(HOW) 결과에 도달할지 보다는 무엇을(WHAT)을 할지에 중점을 두는 프로그래밍입니다.
뭔가 함수형 프로그래밍이 떠오르는 모습을 보이길래 알아보니 선언형 프로그래밍의 하위 개념에 함수형 프로그래밍이 있었습니다.
추측해보자면, 무엇을 할지에 집중하는 것은 동일하지만 선언형 프로그래밍은 함수를 1급 객체로 여기고 무조건 순수함수를 사용해야 한다는 규칙은 없는 개념이 아닐까요?
(추측은 추측일 뿐 나중에 한 번 정리해보겠습니다.
함수형 반응형 프로그래밍과 반응형 프로그래밍을 비교하는 자료도 있더라구요.)
그러면 Rx는 절대 명령형 프로그래밍(↔️ 선언형 프로그래밍)이 아닐까 궁금했습니다.
절대 아니라기에는 명령형 프로그래밍 방식으로 코드를 처리하는 부분은 존재했습니다.
그래서 전체적인 흐름이 선언형 방식으로 진행되는구나로 이해하고 넘어갔습니다.
Rx에서는 연산자의 조합으로 데이터를 변환하고 조합하는 방식을 미리 선언합니다.
// RxSwift 예시 - 선언적 스타일
Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter { $0 % 2 == 0 } // "짝수만 필터링하고 싶다"
.map { $0 * 2 } // "각 값을 2배로 변환하고 싶다"
.filter { $0 > 5 } // "5보다 큰 값만 원한다"
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 선언적 방식 (RxSwift)
button.rx.tap
.take(3) // "최대 3번까지만 반응하고 싶다"
.flatMap { _ in // "탭할 때마다"
Observable.just("Tapped")
.concat( // "먼저 'Tapped' 보여주고"
Observable.just("Button")
.delay(.seconds(2), scheduler: MainScheduler.instance)
) // "2초 후에 'Button'으로 되돌리고 싶다"
}
.bind(to: button.rx.title(for: .normal))
.disposed(by: disposeBag)
해당 사례는 명령형 방식과 비교해보기 위해 코드를 같이 가져왔습니다.
// 명령형 방식
func fetchUserData(userId: String, completion: @escaping (String?) -> Void) {
// 1단계: 사용자 정보 가져오기
APIService.fetchUser(id: userId) { userResult in
switch userResult {
case .success(let user):
// 2단계: 사용자의 프로필 이미지 URL 가져오기
APIService.fetchProfile(userId: user.id) { profileResult in
switch profileResult {
case .success(let profile):
// 3단계: 유효한 이미지 URL인지 검증
if profile.imageURL.hasPrefix("https://") {
completion(profile.imageURL)
} else {
completion(nil)
}
case .failure:
completion(nil)
}
}
case .failure:
completion(nil)
}
}
}
// 선언적 방식 (RxSwift)
func fetchUserImageURL(userId: String) -> Observable<String> {
return APIService.fetchUser(id: userId) // "사용자 정보를 가져오고"
.flatMap { user in // "그 사용자로부터"
APIService.fetchProfile(userId: user.id) // "프로필 정보를 가져오고"
}
.map { $0.imageURL } // "이미지 URL을 추출하고"
.filter { $0.hasPrefix("https://") } // "HTTPS URL만 허용하고 싶다"
}
// 사용
fetchUserImageURL(userId: "123")
.observe(on: MainScheduler.instance)
.subscribe(
onNext: { imageURL in
// UI 업데이트
},
onError: { error in
// 에러 처리
}
)
.disposed(by: disposeBag)
명령형 방식의 코드는 비동기 처리를 위해 기존의 콜백 처리 방식을 사용합니다.
예시 코드를 보면 수직적으로 확장된 중첩 구조가 보이시나요?
이를 콜백 지옥(Callback Hell)이라고 표현하기도 합니다.
성공 로직과 에러 처리 로직을 살펴보면, 명령형 방식에서는 에러 처리 코드도 중복됩니다.
반면 선언 방식은 에러 처리를 한 곳에서 하는 것을 확인할 수 있습니다.
비동기 처리에서의 예시를 보면, 선언형 방식이 중복 로직을 방지하고 흐름을 파악하기 좋은 깔끔한 모습을 보인다는 것을 알 수 있었습니다.
import RxSwift
// 상태 관리 클래스
class StateManager {
private let disposeBag = DisposeBag()
// 현재 상태를 나타내는 BehaviorSubject
let currentState = BehaviorSubject<String>(value: "초기 상태")
// 상태 업데이트 메소드
func updateState(newState: String) {
currentState.onNext(newState)
}
}
// 사용 예시
let stateManager = StateManager()
// 상태 변화에 반응하는 옵저버 등록
stateManager.currentState
.subscribe(onNext: { state in
print("상태가 변경됨: \(state)")
// UI 업데이트 또는 다른 작업 수행
})
.disposed(by: disposeBag)
// 상태 변경 실행
stateManager.updateState(newState: "새로운 상태")
stateManager.updateState(newState: "또 다른 상태")
예시 코드는 Claude 4.0 Sonnet의 도움을 받았습니다.
예시 코드를 보면서 Rx는 데이터 흐름과 비동기 로직을 "연산자와 바인딩"으로 선언하여 “상태가 바뀌면 자동으로 반응하는 흐름”을 구성하는 선언형 프로그래밍의 모습을 보이는 것을 알 수 있었습니다.
이렇게 ReactiveX의 기초 개념을 다져봤습니다!
이거 기초 맞나요
자료를 찾다가 발견한 깊은 개념은 눈 감고 넘어간 상태입니다.
Rx 공식 문서 Observable, Operators, Subject, schedular를 기준으로 각각 깊게 공부하는 글을 이어서 올릴 예정입니다.
정보를 소개하는 글일 수도 있지만, 제가 학습해온 내용을 설명하는 글이라서 혹시 틀린 개념이 있을 수도 있습니다.
혹시 발견하신다면 피드백은 사랑입니다!
🍎 참고한 자료들
https://reactivex.io/intro.html
https://reactivex.io/documentation/observable.html
https://reactivex.io/documentation/operators.html
https://babbab2.tistory.com/185
https://yozm.wishket.com/magazine/detail/1334/
https://medium.com/@kevalpatel2106/what-is-reactive-programming-da37c1611382
https://www.geeksforgeeks.org/theory-of-computation/difference-between-imperative-and-declarative-programming/