RxSwift 곰튀김 강의 정리 (기초)

2023. 4. 11. 22:12iOS

곰튀김님의 RxSwift 강의를 정리했습니다.

 

유튜브 링크

깃허브 링크

공부하게 된 이유

팀 프로젝트에서 RxSwift를 사용하게 되었다. MVC에서 C가 비대해지고 비즈니스 로직을 분리하기 위해서 MVVM 사용, MVVM에서 데이터 바인딩을 쉽게 하기 위해서 RxSwift사용이라는 팀원의 말에 설득되었다.

팀 안에서 스터디도 하고 빠르게 실제로 프로젝트에 적용도 했지만, 애자일하게 진행되는 스프린트다 보니 겉핥기식 공부만 하게 되었다. 2주 가량의 개발이 끝나고 RxSwift를 미처 적용하지 못한 코드들을 리팩토링을 하려 하니 전혀 손 댈 수 없이 막막해서 곰튀김님의 4시간짜리 강의를 듣기로 결심했다.

 

1교시 — RxSwift를 사용한 비동기 프로그래밍

모든 강의가 실습과 함께 이루어지기 때문에 모든 예시를 적기엔 힘들어서 최대한 이해한 개념을 적으려고 노력했다.

 

기존 코드에서 느끼는 불편한 점들

  • 동기로 작동하는 코드를 비동기로 바꿔주고 싶다. → 어떤 일들이 동시에 일어났으면 좋겠다
  • UI는 메인에서 그려져야 한다. → 코드마다 사용하는 스레드가 달라서 불편하다
  • 비동기적으로 이뤄지는 부분만 함수로 따로 빼고 싶다. → 그런데 함수 안에서 DispatchQueue를 사용하면 값을 리턴하지 못해서 completion을 사용해야 한다
  • 비동기로 생성되는 데이터도 completion 대신 리턴 값을 받고 싶다. → 일반적으로 여러 번 호출을 하면 depth가 깊어질 수 있어서 불편하다
  • 기타: @escaping은 본체 함수가 끝나고 나서 나중에 실행되는 함수라는 의미 → 비동기적으로 코드가 처리되니 본 함수가 끝난 후에 completion 이 실행된다

결과적으로 비동기로 생성되는 데이터를 어떻게 리턴 값으로 만들지? 하는 고민이 생기게 된다.

 

비동기로 생성되는 데이터를 리턴 값으로 만들기

비동기적으로(나중에) 오는 json(String) 값을 받아서 그게 오면 UI 를 업데이트 해 보자.

let json: 나중에생기는데이터<String> = downloadJSON(...)

json.나중에오면 {
  // UI 바꾸기(텍스트 변경) 등의 작업
}

 

여기서 나중에생기는데이터 는 아래와 같은 모습을 하고 있을 것이다.

 

class 나중에생기는데이터<T> {
    //// 여기서 나중에 생기는 데이터를 넣어 줄 거다
    private let task: (@escaping (T) -> Void) -> Void

    init(task: @escaping (@escaping (T) -> Void) -> Void) {
        self.task = task
    }

    //// 여기서는 나중에 생긴 데이터를 가지고 어떤 작업(f)을 해 줄 거다
    func 나중에오면(_ f: @escaping (T) -> Void) {
        task(f)
    }
}

 

여기서 클로저가 너무 많아서 이해하는 데 정말 오래 걸렸다..

그래서 코드를 써 보면서 이해를 했다. 우선 결론은 아래 처럼 쓰면 된다.

 

let 나중에생길데이터1 = 나중에생기는데이터<String> { f in
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
        // 여기서 f는 나중에 데이터가 오면 할 작업
        f("나중에 생긴 데이터 1")
    }
}
        
let 나중에생길데이터2: 나중에생기는데이터<String> = 나중에생기는데이터 { f in
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
        // 여기서 f는 나중에 데이터가 오면 할 작업
        f("나중에 생긴 데이터 2")
    }
}
        
나중에생길데이터1.나중에오면 { str in
    print(str)
}
나중에생길데이터2.나중에오면 { str in
    print(str)
}

 

포인트는 데이터가 나중에 생기더라도 그 리턴 값을 가지고 원하는 동작을 해 줄 수 있다는 것! 출력 결과는 아래와 같다.

 

나중에생길데이터2 가 2초 뒤, 나중에생길데이터1 이 3초 뒤에 전달이 되니까 순서가 저렇게 출력된다.

이제는 영상의 예제 코드 안의 downloadJSON 이라는 함수를 예를 들어서 살펴 보자.

기존 구조는 아래와 같다.

 

func downloadJSON(_ url: String, _ completion: ((String) -> Void)?) {
    DispatchQueue.global().async {
        let url = URL(string: url)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
                
        DispatchQueue.main.async {
            completion?(json)
        }
    }
}

 

아까 정의한 나중에생기는데이터 를 사용하면 아래처럼 바꿀 수 있을 것이다. 이 때 completion 대신 나중에생기는데이터 라는 것 자체를 리턴할 수 있다.

 

func downloadJSON(_ url: String) -> 나중에생기는데이터<String?> {
    return 나중에생기는데이터() { f in
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
                
            // f는 메인 스레드에서 실행되어야 하는 작업 (UI를 바꿀 것임)
            DispatchQueue.main.async {
                // json 데이터가 오면 f에 넣어준다
                f(json)
            }
        }
    }
}

 

RxSwift 문법을 써서 바꿔 보면 아래와 같다.

 

func downloadJSON(_ url: String) -> Observable<String?> {
    return Observable.create() { f in
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
                
            DispatchQueue.main.async {
                f.onNext(json)
            }
        }
        return Disposables.create()
    }
}

 

그래서 이제 과정을 좀 정리해 보면 아래와 같다.

  1. 비동기로 생기는 데이터를 Observable로 감싸서 리턴한다.
  2. Observable로 오는 데이터를 받아서 처리한다.

 

비동기로 생기는 데이터를 Observable로 감싸서 리턴하기

RxSwift의 옵저버블에는 생명 주기가 있다.

  1. Create (생성)
  2. Subscribed (구독됨) ⇒ 여기 부터 동작하기 시작한다
  3. Next (데이터 전달)
  4. Complete (완료) or Error (에러) ⇒ 종료
  5. Disposed (메모리 해제)

 

앞서 보았던 코드를 제대로 바꿔 보면 아래와 같은 모양을 하고 있을 것이다.

 

return Observable.create() { emitter in
    let url = URL(string: url)!
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard error == nil else {
            // 에러 방출
            emitter.onError(error!)
            return
        }
                
        if let data = data, let json = String(data: data, encoding: .utf8) {
            // 데이터 전달
            emitter.onNext(json)
        }
				
        // 옵저버블 할 일 완료
        emitter.onCompleted()
    }
            
    task.resume()
            
    return Disposables.create() {
        task.cancel()
    }
}

 

Observable 로 오는 데이터를 받아서 처리하기

let observable = downloadJSON(MEMBER_LIST_URL)

// subscribe 부터 동작
let disposable = observable.subscribe { event in
    switch event {
    case .next(let json):
        // json 가지고 작업
    case .error(let error):
        // 에러 처리
    case .completed:
        // 끝났을 때 할 일
    }
}
        
// 메모리에서 해제
disposable.dispose()

 

앞서 나중에오면 에서 했던 것과 큰 차이가 없다!

 

ReaxtiveX 오퍼레이터

옵저버블 데이터랑 subscriber 사이에서 뭔가 작업을 해 주는 녀석들을 operator 라고 한다.

 

옵저버블
    .observeOn(MainSchedular.instance) // 메인 스레드 사용하는 operator
    .subscribe {
  	...
    }

// observeOn 말고도
.map{...}
.filter{...}
// 등등이 있다.

 

https://reactivex.io/documentation/operators.html

 

ReactiveX - Operators

Introduction Each language-specific implementation of ReactiveX implements a set of operators. Although there is much overlap between implementations, there are also some operators that are only implemented in certain implementations. Also, each implementa

reactivex.io

 

 

여기 사이트를 가면 Marvel 그림과 함께 오퍼레이터들의 동작이 잘 설명되어 있다.

예를 들어 just는 데이터를 가지고 옵저버블을 생성할 수 있도록 해 주는 녀석인데 아래 그림처럼나와 있다.

 

 

그리고 Map은 방출된 옵저버블 데이터에 함수를 적용할 수 있게 하는 녀석이다.

 

아래는 observeOn subscribeOn의 차이를 알려주는 그림인데, observeOn은 즉각 스레드 적용이 되는 데 반해 subscribeOn은 그렇지 않다는 것을 알 수 있다. 왜냐하면 subscribeOn은 처음 스레드로만 적용이 되기 때문! 그림과 함께 너무나도 깔끔하게 이해할 있다.

 

 

여기까지 1교시로 분류되는 것 같다. 정말 유익했다.. 곰튀김님 최고.