Share
Sign In

우당탕탕 iOS 공부

한결
[SwiftUI] 스유에서 뷰를 그리고(draw) 업데이트하는 데이터 흐름 관리 - @propertyWrappers 1
@propertyWrapper..? propertyWrapper는 Swift 5.1+ 에 소개된, 이름 그대로 property를 감싸는 역할을 하는 어트리뷰트다. 어떤 상태를 가지는 property를 정의하고 값을 반영하는 과정을 추상화 할 수 있게 해준다. 제네릭을 이용해서 다양한 타입을 가질 수 있는 멤버 변수도 동일한 로직을 타게 할 수 있다. 말이 너무 어려운데, propertyWrapper로 많이 관리되는 UserDefaults가 역시 나에게도 이해가 쉬웠다. UserDefaults는 String으로 된 키와 저장할 값을 기반으로 값(상태)을 저장하거나 불러올 수 있다. 저장은 새로운 값으로 업데이트가 될 수 있다는 뜻으로도 쓰일 수 있다. (덮어 쓸 수 있다는 말) UserDefaults에 값을 저장, 업데이트,불러오는 등의 로직은 아주 단순하지만 사용할 때마다 그 구문을 반복해야 한다. 그래서 보통 UserDefaults를 관리하는 객체를 만들어서 활용하는데, 이때 propertyWrapper 개념을 접목하면 조금 더 활용도가 높아진다. propertyWrapper 어트리뷰트가 붙은 객체는 내부적으로 wrappedValue라고 하는 연산 프로퍼티를 구현해주어야 한다. 이 wrappedValue 로직을 통해 멤버에 대해 동일한 로직을 반영해줄 수 있다. 생성자를 통해서 기본값도 넣어줄 수 있다. 정의한 propertyWrapper 객체는 어트리뷰트처럼 활용할 수 있다. @로 호출한 propertyWrapper의 생성자에 필요한 초기 값을 넣어준 형태로 멤버에 반영해주면, 따로 해당 멤버의 값(상태)을 불러오거나 업데이트 하는 로직을 신경쓰지 않아도 된다. SwiftUI에서도 다양한 propertyWrapper들이 활용되고 있는데, 특히 뷰를 그리고, 뷰를 업데이트하는 데이터(상태 값)를 효율적으로 관리할 수 있게 도와주는 로직이 구현되어 있다. @State 이름에서 알 수 있듯이, View 객체의 데이터 상태를 관리하는 멤버 앞에 붙는다. View struct 안에서 활용되기에 당연히 View 객체 안에서 초기화 되어야 한다. 특정 View 객체 안에서 사용되고, 초기화되기 때문에 외부에서 주입을 받아 상태가 관리되지 않아야 한다. (사실 주입은 받을 수 있겠지만, 내부에서 그 주입받은 상태가 관리되지 않기 때문에 private 사용이 권장된다.) 뷰 객체 내부에서 활용되는 자식 뷰 객체에게 그 상태값을 바인딩 프로퍼티와 함께 전달해줄 수 있다. 가장 좋은 점은, @State로 관리되는 상태값에 따라 뷰가 fresh-render되고 뷰가 살아있는 동안 그 상태값이 유지된다는 것이다. class의 인스턴스와 같은 참조형을 상태값으로 활용할 수는 있지만, 해당 인스턴스의 값을 변경한다고 하더라도 상태가 변경되었다고 인지되지는 않는다. (@Published, @ObservableObject와 같은 wrapper가 붙으면 말이 달라지긴 하지만) State 의 구현부를 타고 가보면 DynamicProperty 라는 프로토콜을 채택하고 있는 것을 볼 수 있다. DynamicProperty 프로토콜은 내부적으로 update라고 하는 메서드를 가지고 있고, 이 메서드를 통해서 변경된 상태를 인지하고 뷰를 새롭게 업데이트 해주게 된다.
한결
[UIKit + RxSwift] MVVM 구조에서 Input-Output 패턴으로 UI 컨트롤하기
RxSwift의 기본 컨셉은 아래 콘텐츠를 통해 간단하게 정리해보았습니다. 커스텀한 Observable 객체를 이용해서 MVVM 패턴으로 간단한 iOS 앱을 만들어왔다. View 객체에서 발생하는 이벤트를 인식하고 이벤트에 따른 비즈니스 로직을 ViewModel에서 처리하고, 그걸 View가 원하는 Output으로 돌려주는 형태로 코드를 작성했다. 자연스럽게 앱 전반에서 '이벤트 방출 - 구독 - 연산 - 결과 도출'의 매커니즘으로 모든 이벤트를 Asynchronous하게 처리하는 RxSwift의 여러 API를 학습하게 되었고, 이를 통해 간단하게 UI를 컨트롤 하는 방식을 기록한다. MVVM(Input-Output 패턴) + RxSwift 로 코드를 구성한 화면은 아래의 이미지와 같다. 로그인 화면에서는 email, password를 입력받는 TextField의 텍스트 입력 이벤트를 구독하여 조건에 따라 UI를 컨트롤한다. shopping list 라는 타이틀을 가진 화면에서는 역시 TextField의 텍스트 입력 이벤트를 구독해서 테이블뷰에 셀을 보여주고, 컬렉션 아이템 터치 이벤트를 통해 테이블뷰를 컨트롤한다. Input - Output 패턴 MVVM 아키텍쳐의 주요 골자는 View를 컨트롤 하는 View 객체, 앱에서 활용될 데이터에 대한 명세~메서드를 구축하는 Model 객체, 그리고 그 두 객체 사이를 연결하여 이벤트에 대한 로직을 처리하는 ViewModel 객체의 조합이다. Input - Output 패턴도 이런 골자를 지키면서, 이벤트가 들어오는 Input 객체와 로직의 처리 결과로 View에게 반환되는 Output 객체를 각각 구분하는 컨셉을 가진다. ViewModel에서 활용되는 패턴으로, View에서는 Input 객체에 이벤트를 실어 ViewModel이 이벤트를 구독할 수 있게 한다. Input과 Output 객체를 연결하는 통로는 transform(Input) → Output 이라고 하는 메서드를 보통 활용한다. 프로토콜로 Input - Output 패턴으로 ViewModel 구현의 제약을 정해줄 수도 있겠다. Input - Output 패턴이 적용된 ViewModel의 전형적인 모습은 아래와 같다. 내가 파악한 Input - Output 패턴의 핵심은 요정도이다. View단에서 전달하려고 하는 Event Input과 View단에서 필요로 하는 Data(Disposable한) Output을 각각의 구조체에 묶어 보다 명확하게 인지할 수 있다. 결국 Output 객체의 프로퍼티로 전달하는 값도 Disposable을 반환하는 Observable 객체이다. 이벤트에 따라서 변화하는 Output 값을 View 단에서도 Observer에 구독시켜서 UI를 업데이트 한다. RxSwift, RxCocoa 라이브러리가 가지는 API를 잘 활용하면 보다 편하게 Event Emit을 컨트롤 할 수 있다.
한결
Swift - 의존성 주입과 MVVM
의존성과 주입 의존성 : 다른 것에 의지하여 생활하거나 존재하는 성질. 이라고 국어 사전에 정의되어 있다. 코드에 의존성이 생긴다는 것은 어떤 것일까? 쉽게 생각해보면, 어떤 클래스 내부에서 다른 클래스로 인스턴스를 생성하고, 그 인스턴스를 활용하는 것을 코드적인 의존성이 만들어졌다고 볼 수 있을 것 같다. 위의 SomeViewController 코드에서 보면, SomeViewController라고 하는 클래스는 APIService라고 하는 특정한 클래스에 의존성을 가지고 있다. APIService 내부에 구현된 fetch 라고 하는 메서드를 내부에서 활용하는데, APISerivce 클래스 구현부에서 해당 메서드의 이름을 fetches 라고 바꾼다고 하면 어떻게 될까? 당연하게도 SomeViewController 코드에서도 그 이름에 맞게 코드를 바꿔줘야 한다. 이것만이 아니라, 해당 인스턴스를 다른 클래스의 인스턴스로 바꾼다고 가정하면 코드로 바꿔줘야 하는 부분은 더 늘어날 수 있다. 코드에서는 의존성이 발생하지 않게 하는 것은 쉽지 않다. *싱글톤으로 작성된 객체를 타입 멤버로 접근한다면, 클래스 내부에서 의존성이 생기지 않을 수는 있겠다. 주입 : 흘러 들어가도록 부어 넣음. 이라고 국어 사전에 정의되어 있다. 코드에서 '주입'을 한다는 것은 쉽게 생각해보면 인스턴스를 생성할 때, 생성자에 값을 넣어(=주입하여) 내부의 저장 속성의 값을 초기화 시켜주는 것이라고 생각해볼 수 있다. 그러면 의존성을 주입한다는 것은, 외부에서 특정 클래스의 인스턴스를 만들어서 생성자로 전달을 하는 행위를 말하는 걸까? 아래의 코드처럼 APIService의 인스턴스를 단순히 생성자로 넣어준다고 하더라도 SomeViewController는 여전히 APIService에 의존성을 가지고 있다. 의존성을 넣어주는 것이기 때문에 의존성 주입이라고 볼 수 있겠지만, 정확하게 APIService 라고 하는 타입의 인스턴스를 넣어줘야 해서 확장성이 떨어진다. Swift에서의 의존성 주입 그러면 확장성을 갖추면서도 의존성을 외부에서 분리시키는 방법은 Swift에서 어떻게 할 수 있을까? 혼자 질문을 던지고 바로 답을 내려버리는 것 같아서 좀 그렇지만, Protocol as a Type의 개념을 활용할 수 있을 것 같다. Swift에서 프로토콜은 정말 다양한 방식으로 활용되는데, 단순히 어떤 객체에 구현부를 강제하는 것을 넘어서 비슷한 역할을 하는 객체들을 하나의 타입으로 묶어줄 수 있는 모듈러 역할로 활용할 수 있다. 즉, 특정 프로토콜을 동일하게 채용하는 클래스는 어느정도 '아 그 프로토콜의 일을 하겠구나'를 쉽게 유추할 수 있다. 이렇게 동일한 프로토콜을 채용한 객체(=클래스의 인스턴스)를 생성자로 주입시키면 어떨까? 이럴경우, SomeViewController는 이제부터 SomeDataRelatedProtocol이라는 프로토콜 타입에 의존성을 가지게 된다. 그러면 외부에서 해당 프로토콜을 채용한 어떤 객체 타입도 주입할 수 있게 된다.(Protocol as a Type) 주입하는 객체를 쉽게 교체할 수 있고, 쉽게 확장할 수 있다. 역으로, 주입되는 객체 역시 SomeDataRelatedProtocol이라는 프로토콜에 의존성을 가지기 때문에 오히려 역의 의존성 체계가 설계된다. 즉 구체적인 객체가 추상적인(=프로토콜 타입) 요소에 의존하게 되는 의존 관계 역전 원칙을 따르게 된다. MVVM 아키텍처는 왜 쓰는걸까? 아니 아키텍처는 왜 나누는 건가? 작은 앱을 혼자서 만들어간다면 ViewController 안에 데이터 모델링 코드, Network API 코드, 뷰를 그리는 코드 등을 모두 구현하는 것도 나쁘지 않을 수 있다. 어차피 ViewController가 어떻게든 접근해야 하는 객체나 뷰를 만들기 때문이다.