이슈트래커 iOS 버전 클라이언트 프로젝트 입니다.
프로젝트의 이슈 생성 및 관리를 쉽게 도와주는 어플리케이션입니다.
Current Brach = feature/settingApplying
포트폴리오를 위한 프로젝트입니다. SnapKit+FlexLayout 조합으로 유연한 UI 코드 작성, ReactorKit 으로 유지보수성 높은 프로젝트 구성, UI+Unit-Test 작성 등을 진행하였습니다.
라이브러리를 사용하는 것 만큼이나 채택하게 된 이유가 중요하다고 생각합니다. 아래는 각 라이브러리를 선정하게 된 이유들을 정리해 놓았습니다.
- SnapKit = AutoLayout 을 사용하기 위해서 입니다.
| 이름 | 설명 |
|---|---|
| SnapKit | CodeBased-UI 에서 쉽게 AutoLayout 을 사용하기 위해서 입니다. NSLayoutConstraint, NSLayoutAnchor, Storyboard 3 가지 방법이 있지만 코드 가독성이 떨어진다는 문제점이 있었습니다. |
| FlexLayout | CodeBased-UI 에서 많은 subview 의 Constraint 를 정의하기 위해서 입니다. 대부분의 View 들이 Grid-Layout 으로 정의되는데, FlexLayout 을 이용하여 간단히 구성할 수 있었습니다. |
| RxSwift | Concurrency 를 빠르고 효율적으로 구현하기 위해서 입니다. 러닝커브가 심한 것은 사실이지만 우선 웹 상에 자료가 많고 RxSwift 를 활용한 ReactorKit, RIBs 등을 고려해보면 아주 좋은 옵션이라고 생각하였습니다. |
| ReactorKit | 기존 MVC 패턴의 문제점을 보완하기 위해 도입하였습니다. |
| BE | BE | iOS | FE | FE |
|---|---|---|---|---|
| Hoo | Ader | Beck | Dobby | Dotori |
https://sphenoid-fight-243.notion.site/1f7abecd77004e76b4adefff2db3624a
클린 소프트웨어에서 가장 좋아하는 구절인 "작게 시작하라" 는 말을 따라 MVC 패턴부터 시작해보기로 하였습니다.
논란이 많은 디자인 패턴임을 잘 알고 있었으나 실제 구현해보지 않으면 알 수 없다는 생각에 직접 구현해보면서 아래와 같은 문제점을 발견할 수 있었습니다.
- Test 코드 작성이 힘들었습니다. 많은 객체가 Test Target 으로 설정되면서 반복적인 테스트 코드만 늘어갈 뿐이었습니다.
- 객체 간 Coupling 이 너무 두드러지고 있었습니다. 소스 코드를 수정할 때 많은 불편함이 있었습니다.
- Coupling 으로 인해 객체 자체가 너무 커지는 경우가 있었습니다.
- 코드 재사용성이 떨어졌습니다.
좋은 구조를 고민하던 중 코드스쿼드 마스터즈 코스에서 배운 단방향 아키텍처 Flux 를 떠올리게 되었습니다. Flux, Redux 를 iOS 개발에서 사용한 예시를 찾아보다 ReactorKit 을 적용하기로 결심하였습니다. 결심하게 된 계기는 다음과 같습니다.
- 많은 기업에서 사용중인 점이 인상적이었습니다.
- 문서화가 잘 되어 있었기 때문에 단방향 아키텍처를 체험해보기 좋다고 생각하였습니다.
- RxSwift 에 대한 의존성을 가지고 있기 때문에 필연적으로 RxSwift 를 적용해야 합니다. RxSwift 는 여러번 사용해 본 경험이 있습니다.
┌──────────────────┐ 1:N ┌───────────┐ 1:N ┌─────────┐ CRUD ┌──────────────────┐
│ ViewController | ───────▶ | Reactor | ───────▶ | Model | ◀────────▶ | Persistent |
└────────┬─────────┘ └─────┬─────┘ └────┬────┘ | |
| ▲ | Mutate | | AWS Server |
┌─────┴─────┐ | Bind ┌───┴───┐ Reduce | | (issue Data) |
| @IBOutlet | └───────────── | State | ◀───────────────┘ | |
└───────────┘ (Reactive) └───┬───┘ (Reactive) | Core Data |
... | | (Setting Data) |
┌──────▼──────┐ └──────────────────┘
| Entity(s) |
└─────────────┘
...
물론 프로젝트 전체를 단방향 패턴인 ReactorKit 으로 프로젝트 구조를 전부 변경하는 것은 굉장히 많은 작업량을 소모하게 됩니다. 하지만 반드시 해야 할 일이라고 생각했습니다.
Technical Debt (기술 부채) 라는 말을 들은 적이 있습니다. 제가 이해한 바로는 "현 시점의 Task 에 대해 쉬운 솔루션을 택하는 대신 나중에 기술적으로 해결해야 될 요소로 남겨두는 것" 을 뜻하는 것으로 이해하고 있습니다.
즉, MVC 로 진 빚을 갚을 때가 된 것입니다.
이러한 과정을 통해 다음의 효과를 얻을 수 있었습니다.
- 더욱 Testable 한 코드 작성이 가능해졌습니다.
- 유지보수 비용이 많이 절감되었습니다. 여러 개의 화면을 만드는 상황에도 비슷한 로직으로 기능 구현을 할 수 있었기 때문입니다.
- 객체의 역할이 더욱 명확해져서 추상화가 용이해 졌습니다. 많은 Duplicate 를 제거할 수 있었습니다.
UILabel 처럼 컨텐츠 크기에 따라 뷰의 레이아웃이 변경되는 경우 SnapKit 을 통한 AutoLayout 을 정의해 놓을 시 Dynamic 한 뷰를 작성하는 것이 굉장히 용이합니다.
func makeUI() {
contentView.addSubview(titleLabel)
contentView.addSubview(valueSwitch)
titleLabel.snp.contentCompressionResistanceHorizontalPriority = UILayoutPriority.defaultLow.rawValue
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalToSuperview().offset(8)
make.bottom.equalToSuperview().inset(8)
}
}고정된 크기와 높이를 가진 UITableViewCell 은 내부를 Frame-Based UI 로 구성하는 것이 유지보수 및 가독성 측면에서 유리합니다.
final class IssueListTableViewCell: MainListTableViewCell {
override func makeUI() {
super.makeUI()
let isBigSizeScreen = ["13","12","11","X"].reduce(
false,
{
$0 || (UIDevice.modelName.contains($1))
}
)
let padding: CGFloat = isBigSizeScreen ? 4 : 2
contentsLabel.numberOfLines = isBigSizeScreen ? 3 : 2
[titleLabel, dateLabel, contentsLabel].forEach({ label in
label.textAlignment = .natural
})
contentView.flex.paddingVertical(padding).define { contentsFlex in
contentsFlex.addItem(paddingView).padding(padding).define { flex in
flex.addItem().direction(.row).height(50%)
.marginHorizontal(padding).define { flex in
flex.addItem().width(65%).define { flex in
flex.addItem(titleLabel).height(75%)
flex.addItem(dateLabel).height(25%)
}
flex.addItem(profileView)
.width(35%)
.paddingTop(padding).paddingHorizontal(padding)
.justifyContent(.center)
}
flex.addItem(contentsLabel).height(50%)
.marginHorizontal(padding)
}
}
}
}UIScrollView 를 superview 와 일치시키는 것은 SnapKit 을 이용하고, subview 에 Grid Layout 을 구성하는 것은 FlexLayout 으로 간편히 적용할 수 있었습니다.
FlexLayout 은 AutoLayout 이 적용된 superview 를 기반으로 정의하였기 때문에 viewDidApplear(_:) 내에서 레이아웃 되도록 하였습니다.
override func viewDidLoad() {
super.viewDidLoad()
emailArea.textField?.isUserInteractionEnabled = false
emailArea.textField?.backgroundColor = .lightGray
view.addSubview(_containerView)
_containerView.flex.alignContent(.stretch).paddingHorizontal(padding).define { flex in
flex.addItem(titleLabel).height(60)
flex.addItem().define { flex in
flex.addItem(emailArea)
flex.addItem(nicknameArea)
}
flex.addItem(acceptButton).height(60)
}
_containerView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaInsets)
}
_containerView.layoutIfNeeded()
_containerView.flex.layout()
_containerView.reloadContentSizeHeight()
acceptButton.setCornerRadius()
}
UI-Test 로 회원가입 후 로그인하여 데이터 출력되는지 확인하는 중
-
Unit-Test
- 모든 Model, Reactor 클래스는 테스트 대상으로서 분류합니다.
- Unit-Test 를 진행하는 이유는 버그율 감소, Model 동작 여부 확인 후 View/Controller 개발에 집중하기 위함입니다.
- Stress 테스트 등을 진행하지는 않고, 기능 동작 여부만을 미리 테스트합니다.
-
UI-Test
- Visible-Test(UI 가 의도한 대로 렌더링 되는가), Function-Test(UI 에 대한 Response 가 의도한 대로인가) 를 진행합니다.
- ViewController 를 초기화하는 것이 아닌, UIApplication 을 동작시켜서 테스트를 진행합니다.
- UI-Test 를 진행하는 이유는 시뮬레이터 실행 빈도를 줄이고 빠르게 테스트를 진행하는 것입니다.
- 각 Test-Case 당 테스트를 실행시키는 것이 아닌 사용자의 Scenario 를 진행시키도록 합니다.
- Test-Coverage 가 증가해야 합니다.
- UI 테스트의 UI 요소를 식별하는 기준으로 staticText 는 적합하지 않습니다. 현재 고려하고 있는 후보는 다국어 적용 시 사용될 키 값입니다.
- Unit 테스트의 Unit 에 대한 정의를 다시 내릴 필요가 있습니다. 추상화된 객체만 테스트하는 것으로 충분하다면 구체 타입의 클래스들은 Unit-Test 의 Target 으로 정의될 필요가 없습니다.
다음의 블로그 게시글([Medium]iOS에서 언어를 localization하는 Gorgeous 한 방법)을 참고하였습니다
Issue 를 단순히 나열하는 것과 달리 설정 기능의 항목들은 다국어(Localization) 적용이 필요하였습니다.
Localizable.strings 파일을 통해 정적인 객체를 만든다는 아이디어가 Human Error 를 줄일 수 있을 것이라고 생각하였습니다
하지만 새로운 기술 구현에 따른 부담 말고도 설정 목록을 저장함에 있어 Trade-Off 이슈가 발생하였습니다
- Localization 된 문자열을 저장 → 국가 설정을 바꾸면 대응 불가, 문자열을 불러올 때 Logic 추가 불필요
- Localization 에 필요한 키 값을 저장 → 국가 설정을 바꿔도 대응 가능, 문자열을 불러올 때 Localization 설정을 적용할 Logic 필요
고민 끝에 Localized 된 문자열을 저장하기로 하였습니다. 개발 과정에서 하나의 이슈라도 줄일 수 있다면 미리 진행하는 것이 기술 부채를 줄이는 길이라고 생각하였습니다.
개발을 비롯한 IT 엔지니어링에는 수많은 Trade-Off 가 항상 따른다는 점을 배웠습니다.
유니버설 링크로 구현된 앱 링크는 다음과 같습니다.
유니버설 링크는 특히 Back-End 개발자와의 협업이 중요하였습니다.
Back-End 개발자분들께 협업을 요청드렸을 때 “재미있어 보이니 한번 해보자” 라고 답변 주셔서 감사히도 진행이 가능하였습니다. Front-End 개발자 분들은 기능을 하나 더 추가할 수 있어서 테스트에 적극 참여해주셨습니다.
Back-End 개발자분들께는 요청드린 사항은 다음과 같습니다.
- AASA 파일 업로드
- AASA 파일을 정해진 URL 로 다운로드 받을 수 있도록 기능 추가
- https 적용
이는 많은 작업을 요구하기 때문에 프로젝트를 세팅한 후 아래의 과정을 통해 자체 테스트를 진행했습니다.
- Firebase Dynamic Link 를 통해 앱 링크를 생성(https://sanghwiback.page.link/openApp)
- Issue-tracker 프로젝트 Universal Link 기능 추가 → 앱 실행 성공
- Test-Flight 에 앱 배포 후 이메일로 링크 테스트 → 앱 실행 성공
여기까지 진행해보니 Back-End 개발자 분들께 작업 요청을 드려도 괜찮겠다고 판단하여 작업 요청을 드리고 기능 구현은 성공적이었습니다.
많은 블로그에서 Universal Link 가 예기치 못한 이슈들을 발생시키는 것을 확인하였습니다(예: 애플 CDN 서버 캐싱 문제 등).
이번 사례를 단순히 기능 구현 성공에만 한정짓고 싶지는 않습니다. 다른 팀과 협업을 해야 할 때 어떠한 자세로 협업하는 것이 도움이 되는지 배웠습니다.

