RxSwift는 왜 쓰는걸까?(부제: DispatchQueue의 늪) - 1

2022. 11. 29. 20:53IT/iOS

그동안 RxSwift를 공부하지 않았었다. 당장 스스로 Reactive Programming에 대한 필요성을 못 느꼈기 때문이다. 현업에서 어쩌면 가장 활발히 쓰이고 있는 라이브러리인 것이 자명할 지라도 아직 초보 iOS 개발자가 뭣도 모르고 유명하니까 쓰는 건 왠지 기분이 나빴다. 그러다 최근에서야 그 필요성을 느끼기 시작했다.

 

흔한 설명이지만 일단 RxSwift가 무언인가. 하면 Swift에서 Reactive Programming을 가능하게 해주는 라이브러리로 요약할 수 있다.  Reactive Programming은 데이터의 흐름의 방향 및 그 동작 방법이 완벽하게 잘 짜여졌던 기존의 명령형 프로그래밍과 달리 '상태 변화'와 그 '전파'에 초점을 맞춘다.

 

잘 이해가 안간다면 정상이다.

 

간단하게 말해 코드의 실행이 짜여진 코드에 맞추어 일련의 과정을 따라 수동적으로 이루어지는 것이 아니라, 유저의 인터렉션, API 호출 등에 따라 일정 변수 값(정확이는 이를 포괄하는 상태 값)이 변경되었을때 해당 상태 혹은 상태 변화가 전파되어 연관된 요소들의 코드가 능동적으로 수행되는 것이다.

 

현재 Swift 코드를 작성해나가는 데 너무도 중요하고, 웬만한 기업 공고에 RxSwift가 없는 것을 보기 힘들정도이다. 심지어 애플은 최근 Swift에 RxSwift와 정확히 같은 기능을 하는 Combine이라는 이름의 자체 First-party 라이브러리를 제공하기 시작했다. 간단하게  그냥 우리가 요즘 보는 웬만한 사이트들이 동적인 이유도 이러한 프로그래밍 방법론이 적용되었기 때문이다. 그럼 더 멋지게 보여주기 위해서 Rx가 필요한 건가? 하는 의문이 들었다. 어쩌면 궁극적으로 틀린말이 아닐지 모른다.

 

궁극적으론 UX의 개선. 그러나 그 과정에서 얻는 부수적인 이익이 크기에 쓰지 않을 수 없는 것이다. 내가 필요성을 느낀것은 이 부수적 이익들에 있다.

 

'개발하는 정대리'님의 Alamofire 유튜브를 보면서 Alamofire를 이해해보다가 중간에 유료로 바뀌기도 하고 더 해보고싶은것도 있어서 이것저것 시도를 해보던 중 Dispatch Queue의 늪에 빠지게됐다. 

 

let urlString = photoData?[indexPath.row].thumbnail
let url = URL(string: urlString ?? "")
DispatchQueue.global().async {
	let data = try? Data(contentsOf: url!)
    
    DispatchQueue.main.sync {
    	if let imageData = data {
        	let image = UIImage(data: imageData)
            cell.cellImageView.image = image
        }
    }
}

 

코드는 간단하다. 해당 코드 상단에서 API 통신으로 띄워줄 사진의 url 값을 받아오고, 이 사진을 Data method를 통해 collectionView의 cell에 패칭해주는 작업이다. 온전히 동작하도록 하기 위해서 이렇게 DispatchQueue 안에서 다시 DispatchQueue를 써야만 했는데, 비동기적으로 데이터를 가져오고 싶다하더라도 iOS에서 UI에 대한 조작은 main 쓰레드에서 해줘야하기 때문이다. 그래서 비동기 global 쓰레드 안에서 다시 main 쓰레드로 코드를 가져오는 작업을 한다.

 

 

문제가 어떻게 발생하는지 잠깐 보고넘어가자면, 원래 코드는 아래와 같았기 때문에

 

let urlString = photoData?[indexPath.row].thumbnail
let url = URL(string: urlString ?? "")
let data = try? Data(contentsOf: url!)
if let imageData = data {
	let image = UIImage(data: imageData)
	cell.cellImageView.image = image
}

 

이렇게 작동했다.

어? 문제가 없는 것 같은데? 싶지만

 

 

이렇게 Xcode는 보라색 불만을 토로하고 있었다. 구체적으로 읽어보면 "너가 이거 메인 쓰레드에서 동기적으로 불러서 UI작업 해야될 거 못하니가 빨리 비동기로 바꾸든지 어떻게 해봐" 라면서 실제로 cell에 적용한 레이아웃도 안먹고 UI 반응성도 최악이 되어버린다. 지금 운이 좋아 그렇지 때로는 그냥 뻗어버리기도 한다. 그래서 순순히 말을 듣기로 하고 메인 쓰레드가 아닌 다른 쓰레드에서 처리를 해준다.

 

 

그래서 원하는 대로 이렇게 메인쓰레드에서 비켜주었더니 여전히 불만을 토로하고 있는 Xcode...말인 즉슨 UI처리는 좀 main 쓰레드에서 해라! 하는 것이다. 그래서 결론적으로 아까 위에서 보았던 형태로 쓰레드 전환이 겹겹이 이루어지는 것이고, 여기서 단순 return으로 끝나지 않고 도출된 결과로 비동기 처리를해야하는 상황이라면? 무한 DispatchQueue 루프를 그리게 되는 것이다.

 

let urlString = photoData?[indexPath.row].thumbnail
let url = URL(string: urlString ?? "")
DispatchQueue.global().async {
	let data = try? Data(contentsOf: url!)
    
    DispatchQueue.main.sync {
    	if let imageData = data {
        	let image = UIImage(data: imageData)
            cell.cellImageView.image = image
        }
    }
}

이제 원하는 대로 잘 작동하는 것을 볼 수 있다. 이제 남은 문제는 이 사랑스러운 DispatchQueue 덕분에 코드가 지저분해진다는 점이고, 이걸 해결해본다.

 

첫번째로 시도해보는 건 일단 함수로 빼보자! 하는 자연스러운 발상. 이미 iOS를 잘 다루시는 분들이 보면 우스울 법한 시도들을 꽤나 많이 해봤는데, 예컨데

이렇듯 DispatchQueue 자체가 non-void return 함수인데도 내부에서 아싸! 하고 패칭된 데이터를 불러온다든지,

그러면 이렇게 밖으로 빼버리면 되는거 아닌가? 했다가 기존 함수의 종료와 함께 다른 쓰레드로 이사갔던 코드가 값을 반환하기도 전에 함수는 그냥 끝나버려 실제로 아무 일도 image는 아무것도 담지 못하고 종료되어버리는 결과 등, 말이다. 

 

그래서 보통 이럴때 내부에서 패칭된 데이터를 동적으로 사용하기 위해 swift는 completion을 제공한다.

 

그래서 completion에 클로져를 통해 원하는 데이터를 원하는 곳에 활요하고 마무리해주는 방식이다. 참고로 completion에 들어가는 함수 정의에 @escaping이 들어가는 것은 위처럼 정상함수가 비동기 처리를 분기하고 마무리 되면서 비동기 처리된 부분이 강제로 종료되지 않고 정상함수의 종료 이후에도 본인의 작업을 온전히 마무리 할 수 있도록 해주기 때문이다.

 

그런데 여전히 아름답지 못하다.

 

결국 completion과 DispatchQueue 때문에 코드가 지저분하고, 자연스럽게 반환된 image 변수만으로 UI를 조작해야만 할 것 같다는 생각이든다. 이것을 가능하게 해주는 것이 RxSwift이다. 마치 delegate을 통해 우리가 cell의 selected 여부를 확인하는 것처럼, 이벤트의 값이 변화되는 것에 따라 동적인 변주가 가능하도록 하는 것, 이것이 Reactive Programming이다. 리액트를 처음할때 State에 따라 스리슬적 모습을 바꾸던 virtual DOM에서 느꼈던 아름다움이 다시금 느껴진다.

 

이제 이 코드를 RxSwift를 통해 보다 유기적이고 동적이며 간결하게 구성해보고자 한다.

 

 

 

 

부족한 부분은 언제든 댓글을 통해 알려주시면 열심히 배우겠습니다.