주말동안 심심해서 SwiftUI Beta를 설치해서 사용해 봤다. SwiftUI를 통해 간단한 UI를 구성해보았고, ViewController가 없어진 View가 어떤식으로 Logic을 구성하고 어떻게 Data와 Model과 관계를 맺어 동작하는지 살펴보았다. 

Declarative Syntax (선언적 표현)

  SwiftUI는 Declarative syntax를 사용하여 GUI를 구성한다. 예전에 Web나 React Native같은 Declarative 방식을 사용하는 프레임워크로 앱을 개발해 본 경험이 있는데, 개인적으로 GUI를 이러한 선언적 방식을 사용하여 구현하는게 내 취향에는 맞지 않았다. (여러 tool을 사용해봤지만 만족스럽지 못했던 기억이...) 그런데 SwiftUI도 Declarative 기반이라고 하여 첫인상은 썩 좋지 않았다. 

  경험적으로 봤을 때 Declarative 표현방식으로 구현하는 GUI는 보는 것보다 읽는 것에 비중이 크다 보니 iOS Storyboard같이 드래그 앤 드롭 방식으로 UI를 개발하는 것보다 인지적 노력이 더 필요했다. 이는 직관적이지 않음으로 내가 오래전에 구현했거나 다른 사람이 구현한 GUI를 수정하려고 할때 많은 애를 먹곤 했었다. 하지만, SwiftUI는 이러한 문제를 아래와 같이 꽤 괜찮은 GUI Tool을 통해 해결했다.

직관적인 GUI Tool

[그림 1. Inspector로 속성 바꾸기]

  Code Editor 영역 혹은 Canvas 영역에서 Cmd + Click -> Inspector를 선택하면 View가 가지고 있는 속성들을 손쉽게 바꿀 수 있다. 

 

[그림 2. Drag and Drop으로 View 추가하기]

  Drag and Drop으로 View를 직관적이고 쉽게 추가 하고 수정할 수 있다. 이 기능 역시 Editor와 Canvase에서 모두 사용할 수 있다. Declarative syntax 방식의 장점과 Drag and Drop 방식의 장점들이 잘 어울러졌다.

 

[그림 3. Preview]

  코드를 수정하면 거의 실시간으로 Preview 영역만 빠르게 컴파일하여 미리보기가 가능하다. 이는 내가 작성한 GUI의 유사한 화면을 보여주는 것이 아닌 live app으로 실제 빌드된 화면과 동일함을 보장한다.

  [그림 3] 25~31 Line에서 Preview를 설정하는 부분을 볼 수 있는데, 해당 부분(28Line)에서 View가 의존하는 Class들을 Mock으로 대체하여 전달 할 수 있다. 따라서 매번 앱을 Full build하여 실제 데이터와 유저 시나리오 시퀀스대로 따라 가면서 View를 디버깅 할 필요가 없게 되었다. 이로 인해 View의 개발 및 유지보수 시간을 굉장히 절약 할 수 있을 것으로 보인다.

Two-Way Data Binding

[그림 4. State and Data Flow]

  드디어 iOS에서도 Data Binding을 지원한다. SwiftUI에서 제공하는 Property를 사용하여 User의 액션이 데이터로 전달되고 변경된 데이터는 뷰를 갱신한다. 바인딩된 Property의 Memory capture(strong, weak, unowned)도 신경쓸 필요 없게됐다.

 

[그림 5. 기존의 ViewController]

  기존의 방식에서는 유저의 액션으로 데이터를 변경하고 View를 갱신하기 위해서 UIViewController가 필요했다. UIViewController는 여러 View와 Model들을 가지고 있으며, 그 둘의 데이터 상태를 동기화 해주는 코드가 필요했다. 그리고 이런 일련의 동작들을 좀 더 깔끔하게 하기위해 우리는 꽤 많은 수고를 들여 기반코드를 작성했어야 했다.

 

[그림 6. SwiftUI에서의 View]

  SwiftUI에서는 UIViewController가 사라졌다. View는 Struct가 되었고, 그 대신 SwiftUI는 상태를 유지하고 추척할 수 있는 @Binding,  @State, @ObservableObject, @Environment등을 제공한다. 이제 iOS는 MVC가 아닌 MVVM을 사용할 수 있게 되었다.

 

  아래는 아이디와 패스워드를 입력하면 로그인 버튼이 활성화되는 테스트 코드이다. 이제 View와 Data간의 Binding하는 글루코드가 한결 깔끔해질 수 있게 되었다.

 

 

[그림 7. DataBinding]

 

  ObvervableObject protocol을 사용하여 LoginViewModel를 만든 모습니다. @Published를 사용하여 필드를 만들면 해당 값이 변경될때마다 바인딩 된 뷰에 자동으로 값을 갱신 할 수 있다. 그리고 View에서 변경된 값이 ViewModel에도 자동으로 갱신된다.

 

  View에서는 @EnvironmentObject를 사용하여 ViewModel를 선언하고 environmentObject를 통해 외부(22 Line)에서 데이터를 받을 수 있다. 이렇게 전달 받은 ViewModel은 위 코드에서 보이는것과 같이 손쉽게 View와 바인딩되어 사용될 수 있다.

결론

  SwiftUI를 하루 정도 사용해 보았는데, Declarative과 Drag and Drop 방식의 UI구성을 적절히 잘 지원하는 것 같다. 그리고 Data Binding을 지원하게 되어 이제 MVC가 아닌 MVVM을 좀더 쉽게 사용할 수 있게 되었다.

  Beta라 그런지 아직 버그가 많은것 같고, 잘못 사용한 문법 때문에 런타임에 crash가 나는 경우가 종종 있는데 로그로 충분히 crash 정보가 표현되지 못하고 있어서 좀 애를 먹긴 했다. 그리고 Apple Developer 사이트가 튜토리얼을 기가막히게 잘 작성해 놨다. 

 

https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

 

Apple Developer Documentation

 

developer.apple.com

 

'iOS' 카테고리의 다른 글

Swift에서의 DI(Dependency Injection)  (0) 2020.08.11
Swift에서의 AOP  (0) 2020.08.03
Realm은 thread safe하지 않다.  (0) 2019.09.01

  통계에 따르면 프로젝트에 존재하는 코드 중 90% 정도는 예외를 처리하는 부분이다. 그렇기 때문에 애러 처리는 어플리케이션 개발에 있어 굉장히 중요한 부분을 차지하다. 내 생각에 코드는 성공하는 로직을 위주로 작성되어야 가장 깔끔해 보인다. 일관성 있어야 하며 일반적인 제어의 흐름과 예외 처리는 분리되어야 한다. 그렇기 위해선 각 함수들의 애러는 우아하게 처리되어야 한다. 

 

 애러의 가능성을 피할 수 있으면, 해당 설계대로 작성하는게 가장 좋다. 그렇지 않다면 애러는 명시적으로 처리 되어야 한다. 아래는 rawData를 Movie list로 파싱해서 description이 있는 경우 해당 text의 길이를 더해서 반환하는 함수이다. 함수가 실패하면 의도적으로 nil(Null)을 리턴하여 해당 함수의 실패를 알리는 방식으로 코드를 작성했을때, nil을 전달 받은 getMovieDescriptionCount함수의 코드를 보자.

 

[nil(Null)을 사용하여 코드가 망가지는 모습]

 

  우리가 애러 표현을 위해 습관적으로 nil을 적용 했을때 망가지는 코드의 모습이다. getMovieDescriptionCount는 예외처리 코드 때문에 성공하는 로직을 작성할 수 없다. 코드도 길어지고 가독성도 떨어졌다. nil check하는 if문이 별게 아닌 것 처럼 보이지만 최소한의 인지적 노력을 해야하기 때문에 코드를 읽는데 방해가 된다.

 

[애러가 정상적인 동착 처럼 보이도록 default  value를 적용]

 

  이런 경우에는 nil이 아니라 default 값을 넘기고 array인 경우 empty array를 넘겨서 정상적인 동작으로 보이도록 처리 될 수 있다. (class인 경우엔 Null Object Pattern을 사용할 수 있다.) 우리가 작성하는 대부분의 코드는 "if {성공} else {아무것도 하지 않음}" 인 경우가 많기 때문에 습관적으로 사용하는 nil 처리만 잘해도 코드가 굉장히 깔끔해 질 수 있다. 

 

  정상적인 동작처럼 보이도록 디자인 하기 어려운 경우에는 아래와 같이 여러 상태들의 조건을 확인하여 예외 처리 코드를 적용한다.

 

[애러를 분기 하여 예외 처리]

 

  이 경우도 마찬가지로 if의 인지적 노력이 필요함으로 가독성이 매우 떨어지고 예외 처리 코드 때문에 코드의 일관성을 잃어 버리게 된다. 또한 2~10 line은 예외에 대한 내용이 구체적이지 않기 때문에 문제를 식별하기 굉장히 어렵다. 12~28 line은 Error Code를 정의하여 예외처리를 처리를 하였다. Error Code는 실패에 대한 명확한 이유를 알 수 있기 때문에 문제를 식별하고 구체적인 예외처리를 할 수 있다. 하지만 이 역시 일반적인 제어의 흐름 속에 예외 처리 코드가 섞여 있어서 코드의 일관성을 잃어 버렸다. 

 

[사용자 정의 Error]

 

  이런 경우 사용자 Error 정의하고 do try catch구문으로 처리한다. 사용자 Error의 사용은 오류를 명시적인 이름으로 정의 할 수 있으며, 오류의 가능성이 있는 함수를 사용하는 클라이언트 코드의 일반적인 제어의 흐름과 오류 처리 코드를 물리적으로 분리 시킬 수 있다.  따라서 이제 do try 블록 안에는 성공하는 로직만 존재한다. 

 

  사용자 정의 Error는 상위 레이어로 throw 될 때 OCP를 위배 할 수 있다. 상위 레이어로 오류를 전달은 wrapping하여 rethrow 하도록 디자인 할 수 있다.

 

'CleanCode' 카테고리의 다른 글

javascript AOP  (0) 2016.10.18
boolean type parameter의 모호성  (0) 2016.10.15

[그림 1. 우리가 기대하는 Singleton]

  Runtime에 유일한 상태를 공유하기 위해서 singleton을 많이 사용하는데, 우리가 singleton class를 만들때 기대하는 바는 위 [그림 1]과 같다.  Singleton은 다른 object들과의 dependency 관계가 쉽게 맺어지고 사용될 수 있도록 구현되어 있다. 이게 singleton의 장점이자 단점으로 다가오는데, 시간이 지나면서 개발자들은 singleton이 존재해야 할 레이어에 대한 인지가 점점 떨어진다. 특히 급하게 처리되어야 할 문제들이 쉽게 해결할 수 있는 singleton으로 모이게 되는데, 이런식으로 처음 의도와는 다르게 여러 service들이 하나의 singleton을 의존하게 된다. 그리고 여러 service들에 의해 많은 기능들이 추가 되면서 점점 large class가 되어 간다. 심지어 callback을 주기위해 역참조하는 경우도 발생한다.

 

[그림 2. Large class가 되어버린 Singleton과 Dependeny rot]

  [그림 2]처럼 service A를 위해 만들어졌던 func1이 다른 service들이 사용하기 시작한다. service A의 요구 사항이 변경되어 func1이 변경된다면 이를 사용하는 모든 service들에게 변경 전파(Shotgun Surgery)가 이루어진다. 그리고 이러한 coupling 문제를 개선하기 위해 singleton을 리팩토링 할 때의 영향도(Force)는 어플리케이션 전체가 된다.

 

  Singleton은 class dependency라서 mock으로 대체 될 수 없다. 이는 singleton을 사용하는 service들의 unit test를 작성하는데 어렵게 만든다. singleton의 변경에 따른 영향도는 굉장히 높지만 그 변경에 대한 검증이 매우 부족한 상태라는 의미다.

 

  Singleton에 대한 class dependency 문제를 해결하기 위해서는 의존 관계가 외부로부터 injection 되어야 한다. 그리고 client는 이렇게 주입받은 object가 singleton instance인지 일반 object intacne인지 모르는 상태에서 사용되어야 한다. 또한 이 object는 interface에 의존하도록 디자인 되어야 한다. 이는 통제된 인터페이스를 통해 정의된 서비스를 제공하도록 디자인됨을 의미한다. 그리고 해당 object의 유일성은 Service Locator같은 녀석이 책임지고 dependency wiring은 IoC Container로부터 injection 받도록 디자인 해볼 수 있다.

+ Recent posts