Skip to content

Team-LionHeart/LionHeart-iOS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

🦁 하루 10분, 좋은 아빠가 되는 방법

LionHeart_MainImage

라이온하트는 ‘사자의 용기’를 의미합니다.
태어나 처음으로 아빠가 된 어른들이 강하고 똑똑한 부모가 될 수 있도록,
세상에서 가장 쉬운 방법을 제공합니다.


프로젝트 기간


LionHeart_Flow


리팩터링 참여 인원

ffalswo2 kimscastle cchanmi

3차 리팩터링

Unit test 목표

Test 적용 대상들 각각 커버리지 70% 이상

Unit test의 적용

Manager Layer


ViewModel Layer

  • NavigationDummy

    • ViewModel에서 Coordinator로 올바른 flow type을 전달하는 것 까지만 테스트를 진행하기 때문에, ViewModel에 필요한 의존성 객체의 자리만 채워주는 용도로 Dummy를 사용했습니다.
  • ManagerStub

    • URLSessionStub를 활용해 ManagerLayer의 로직 검증이 완료된 상태이기때문에 ManagerStub를 활용해 ViewModel Layer의 로직을 검증했습니다.
  • 관련 PR (변경 이전 ViewController Test 포함)


ViewController Layer

  • ViewModelSpy

    • ViewModel Layer의 unit test를 통해서 검증된로직은 input에 따른 올바른 값이 output으로 반환되는가 였기때문에 단순히 ViewController에서 동기적으로 특정Data가 들어왔다고 가정하고 unit test를 진행하려 했습니다.
    • 하지만 viewModel의 output이 ViewController로 원하는 시점에 잘 들어와 반영 되었는지를 검증해야 유의미하다고 생각해, viewModel의 output이 viewController로 잘 들어오는지 그리고 데이터가 UI 컴포넌트들에 잘 적용이 되었는지를 비동기 테스트하는 방식으로 변경했습니다.
  • 관련 PR

  • API Unit Test PR


ViewModel을 Stub가 아닌 Spy로 만든 이유

ViewController는 event을 ViewModel에 전달해주고, Output을 통해 값이 변하면 UI에 데이터를 올바르게 적용을 하는 책임을 가지고 있습니다.
따라서 ViewModel이 “ViewController가 전달한 이벤트”를 제대로 수신했는지를 확인하기 위한 행위 검증이 필요로 합니다.
추가로, 미리 지정한 Output을 보냄으로써 ViewController의 Output binding 동작을 검증하기 위해 가짜 객체를 받습니다. 이는 상태 검증에 속합니다.

따라서 “행위”와 “상태”를 모두 검증하기에 ViewModel Spy로 구성하였습니다.


2차 리팩터링

기존 MVC-C패턴에서 완전한 로직분리를 위한 MVVM-C패턴으로 리팩터링, 데이터 바인딩의 경우엔 combine을 활용

Lion Heart MVVM 리팩터링 원칙

viewModel은 input과 output의 구조로 설계후 combine을 활용해 data binding을 구현

  • ViewController쪽에서 받는 Publisher의 errortype은 항상 Never type으로 구성했습니다.

UI는 error를 알필요가 없이 단순히 action으로 ViewModel에 값만 넘겨주면되며, ViewController에서 completion에 해당하는 error를 받게되면 stream이 끊어지고, 끊어진 stream에서 본래 받고자 하는 user input은 화면을 재진입하지 않는 이상 어떠한 요청도 받지 못하게 되기 때문입니다.

  • navigation을 관리하는 publisher를 ViewModel내부에서 구현하고 특정 input에따라 화면전환 타입을 넘겨주고 navigation publisher가 타입에따라 화면전환담당 객체인 coordintor의 메서드 호출하는 구조를 가집니다.

여전히 ViewModel을 의존성주입하는 이유

DIP의 본질은 “변화하지 않는 것에 의존하는 것”이며 “추상화”에 의존한다는 뜻입니다. 그리고 이를 위한 도구로 protocol을 제공합니다. UI에 가장가까운 ViewModel의 경우 변화에 민감할수있기에, ViewModel protocol 또한 변화에 민감합니다. 이는 DIP의 본질에 맞지 않습니다. ViewModel을 구체 타입을 바라보는 구조도 고려를 했지만 현재 Input-Output구조를 채택하고있으며 Input, Output 구조체 타입과 transform이라는 메서드로 추상화된 protocol은 “변하지 않는 것”이라고 판단해 ViewModel또한 DIP를 적용하기로 결정했습니다



기존 async/await을 통한 네트워킹 + Combine 결합

async / await과 Combine을 결합한 네트워크 방법

  1. 네트워킹시 completion을 통해 상위 stream의 끊어짐을 방지하기 위해 flatmap operator를 사용하고, 내부적으로는 비동기 적으로 stream을 생성하기 위한 future를 사용해서 async/await과 Combine을 혼합해서 사용했습니다.

2. 네트워크 통신시 발생하는 error를 combine의 catch operator를 통해서 최종적으로는 error를 never type으로하는 stream으로 바꿉니다.
해당 과정을 통해 catch가 없을때의 코드와 달라진점은 flaMap을 통해 return해주는 Publisher의 Error Type을 Never로 만들어줄 수 있어 1번 원칙에서의 UI는 error를 알필요가없다 라는 원칙을 지킬 수 있습니다.

해당 방식이 유의미한 이유는 어떤 error가 발생했을때 UI/UX적인 관점에서 유저에게 보여줄수있는 최소한의 UI는 구성을 해야하기에 default값을 넣어줘 유저가 보기에는 UI가 깨지지않은 view를 볼수있고, 내부적으로 error를 handling해주는 stream을 통해 에러에 대한 처리를 해주면 유저입장에서는 에러가 발생한 사실을 모르게 처리해줄수있어 긍정적인 사용자 경험을 얻을수 있게 됩니다.

3. 기존의 delegate pattern대신 combine의 stream을 활용한 data passing방식으로 통일했습니다 > cell 내부의 button action을 처리할때 datasource로 인해 생성되는 data stream이 누적되는 문제와 cell내부에서 data stream이 생성되는 문제를 prepareForReuse와 stream을 저장하는 위치를 조정함으로써 해결합니다. - [[REFACTOR] CurriculumView Diffable 및 MVVM(Combine)-C로 리팩터링 (#179)](#187)

기존 TableView, CollectionView를 DiffableDataSource로 변경

  • 해당 앱에서는 데이터의 변화에 따른 애니메이션이 필요한 상황이 존재하고 해당 경우에 Snapshot을 활용해 UI/UX적으로 보다 나은 경험을 제공해주는 DiffableDataSource를 활용해 보다 더 나은 유저 경험을 제공할 수 있는 방향으로 리팩터링했습니다.

Cell Reuse Trouble Shooting

MVVM 원칙

ResultType을 통한 error처리에 대한 고민




1차 리팩터링

기존 MVC 패턴에서 ViewController의 책임을 분리하기 위한 여러가지 디자인패턴 및 구조 적용


1. 기존 네트워크 레이어를 singleton에서 의존성주입방식(Dependency Injectcion)으로 변경

추후에 Unit Test 도입을 고려하여 응집도는 높고 결합도는 낮은 객체 설계를 목표로 설계에 임했습니다. 기존의 싱글톤 방식은 SRP 원칙과 OCP 원칙에 위반되고 특정 값에 대한 테스트를 진행하는데 어려움이 있으며 Data race의 위험성 또한 존재할 수 있습니다. 따라서 기존 싱글톤에서 Dependency Injection을 통한 의존성 주입 방식을 도입 및 적용했습니다.


2. 전체 UI 컴포넌트 분리, 디자인시스템 구축 및 적용

팀 내에서 반복해서 쓰이는 UIComponent의 경우에 반복되는 코드가 많아지는 상황이 발생해 가독성 측면에서 피로도가 너무 심해졌습니다. 자주 쓰이는 Component들에서 공통적으로 쓰이는 파라미터들을 인자로 받아 편하게 사용할 수 있도록 라이온하트의 디자인시스템을 구축하고 적용했습니다.


3. ViewController의 화면 전환 책임을 담당해 줄 Coordinator Pattern 도입

ViewController는 UI 관련 객체이기 때문에, 사용자 흐름을 처리하는것은 역할 범위(scope)를 벗어난다고 생각했습니다. 또한, MVC의 단점인 ViewController의 역할이 비대해지는 문제를 해결하기 위해 화면전환 책임 전담을 위한 Coordintor Pattern을 도입 및 적용했습니다.


4. Coodinator 객체 내에서 ViewController 객체 생성 책임 분리를 위한 Factory Pattern 도입

DI로인해 ViewController 객체 생성시 외부 객체 생성 및 주입의 불편함이 발생했습니다. 이를 해결하기 위해 Custom DI Container, Swinject, Factory Pattern 같은 여러 방법론을 통한 문제 해결을 고민했습니다. 결론적으로 Factory Pattern이 라이온하트 프로젝트 규모와 구조를 고려했을 때, 가장 적합하다고 생각해 도입 및 적용했습니다.


5. ViewController와 Coordinator간의 완전한 관심사 분리 및 캡슐화를 위한 Adaptor Pattern 도입

Delegate 패턴으로인해 Coordinator가 ViewController에서의 User Action을 추론할 수 있게 되었고 따라서 완전한 관심사 분리가 불가능하게 되었습니다. Coordinator는 flow에 대한 책임만을 가지고 있어야 한다고 생각했고, Coordinator와 ViewController의 완전한 관심사 분리를 위해 두 가지 Interface를 연결해 주는 Adaptor Pattern을 도입 및 적용했습니다.



UI 설계 및 구현

UI 설계 및 구현(1차 프로젝트)



🍎 LionHeart-iOS Developers

김민재 김의성 김동현 곽성준 황찬미
ffalswo2 kimscastle BrickSky sjk4618 cchanmi
프로젝트 설계
로그인 유저 플로우
스플래시 뷰
아티클 상세뷰
프로젝트 설계
온보딩
메인 뷰
디자인시스템
탐색 뷰
챌린지 뷰
커리큘럼 뷰
주차별 리스트 뷰
북마크 뷰
마이페이지 뷰

💻 Development Environment


📖 Using Library

Library Tag Tool
SnapKit Layout SPM
Lottie Splash, Animation SPM
FireBase 푸시알림 SPM
KaKaoOpenSDK 소셜로그인 SPM

📝 Coding Convention, Git flow

📁 Foldering

LionHeart-iOS
├── LionHeart-iOS
│   ├── Application
│   │   ├── AppDelegate.swift
│   │   ├── Base.lproj
│   │   │   └── LaunchScreen.storyboard
│   │   └── SceneDelegate.swift
│   ├── Global
│   │   ├── Config.swift
│   │   ├── Extensions
│   │   │   └── Encodable+.swift
│   │   ├── Literals
│   │   ├── Resources
│   │   │   └── Assets.xcassets
│   │   │       ├── AccentColor.colorset
│   │   │       │   └── Contents.json
│   │   │       ├── AppIcon.appiconset
│   │   │       │   └── Contents.json
│   │   │       └── Contents.json
│   │   ├── UIComponents
│   │   └── Utils
│   ├── GoogleService-Info.plist
│   ├── LionHeart-iOSDebug.entitlements
│   ├── Network
│   │   └── Base
│   │       ├── BaseResponse.swift
│   │       └── HTTPHeaderField.swift
│   ├── Scenes
│   │   ├── Article
│   │   │   ├── ArticleCategory
│   │   │   │   ├── Cells
│   │   │   │   ├── ViewControllers
│   │   │   │   └── Views
│   │   │   ├── ArticleDetail
│   │   │   │   ├── Cells
│   │   │   │   ├── ViewControllers
│   │   │   │   └── Views
│   │   │   ├── ArticleListByCategory
│   │   │   │   ├── Cells
│   │   │   │   ├── ViewControllers
│   │   │   │   └── Views
│   │   │   └── ArticleListByWeek
│   │   │       ├── Cells
│   │   │       ├── ViewControllers
│   │   │       └── Views
│   │   ├── BookMark
│   │   │   ├── Cells
│   │   │   ├── ViewControllers
│   │   │   └── Views
│   │   ├── Curriculum
│   │   │   ├── Cells
│   │   │   ├── ViewControllers
│   │   │   └── Views
│   │   ├── Login
│   │   ├── MyPage
│   │   │   ├── ViewControllers
│   │   │   └── Views
│   │   ├── Onboarding
│   │   │   ├── Cells
│   │   │   ├── ViewControllers
│   │   │   └── Views
│   │   ├── Splash
│   │   ├── TabBar
│   │   └── Today
│   │       ├── Cells
│   │       ├── ViewControllers
│   │       └── Views
│   ├── Settings
│   │   ├── Configurations
│   │   │   └── Development.xcconfig
│   │   └── Info.plist
│   

🦁 라이온하트 노션이 궁금하다면?

아요 라이옹 노션

💥 Troble Shotting

김민재 트러블 슈팅 🐨
김의성 트러블 슈팅 🦈
곽성준 트러블 슈팅 🦦
김동현 트러클 슈팅 🐙
황찬미 트러블 슈팅 🐧

Releases

No releases published

Packages

No packages published

Languages