이번 글에서는 프로젝트를 진행하며, 제가 경험했던 내용에 대해 작성할 예정입니다.
처음 프로젝트를 하며 다양한 상황을 경험해보고 있습니다.
이 중에서 이번 2주차 때 가장 인상 깊었던 내용에 대해 공유해보고자 합니다.
목차
- MVVM 패턴을 도입하며
- 나의 첫 PR
- 리팩토링 과정을 경험해보며
이 글의 목차는 위 3가지와 같습니다.
첫 번째 글에서는 프로젝트에서 도입할 아키텍처를 설정하는 과정에 대해 리마인드 겸, 정리를 해보려고 합니다.
두 번째 글에서는 제가 맡은 Task에 대해 첫 PR을 올리고, 어떤 피드백들을 받았는지에 대해 정리할 예정입니다.
마지막으로는 리팩토링 과정을 통해 어떻게 변했는지, 어떤 장점을 얻게 되었는지에 대해 서술할 예정입니다.
MVVM 패턴을 도입하며
프로젝트 하임은 클린 아키텍처를 기반으로 하고, 디자인 패턴은 MVVM을 적용하기로 하였습니다.
MVVM은 Model / View / ViewModel로 이루어진 패턴입니다.
각 그룹의 역할을 간단히 이야기하면
- Model은 다루게 될 데이터를
- View는 화면에 보일 UI를
- ViewModel은 Model의 정보를 View에 보이는 값들로 변경합니다.
물론 iOS에서 UIKit Framework를 사용한다면 ViewController도 빼놓을 순 없습니다.
- ViewController의 역할은 무엇일까요?
- 리마인드 겸 UIKit에서 VC의 역할을 간단히 이야기하면 ‘앱의 뷰 계층 구조를 관리하는 역할’을 수행합니다.
그렇다면 MVVM을 어떤 방식으로 구현하고자 했을까요?
protocol ViewModel {
associatedtype Action
associatedtype State
var state: State { get }
func action(_ action: Action)
}
프로젝트 하임에서는 위와 같은 ViewModel 프로토콜을 만들어서 사용하기로 결정했습니다.
왜 Protocol을 정의했냐면, 클린 아키텍처를 사용하며 프로토콜로 추상화를 진행한 후 의존성을 주입할 수 있도록 하기 위함입니다.
이에 대한 자세한 내용은 아래 제 예전 포스팅에 나와있어 첨부하였습니다.
의존성, 의존성 주입(1) ?
의존성?INtro...네이버 부스트캠프 그룹분들이랑 이야기를 하는 과정에서의존성 주입과 관련된 이야기가 나왔다. 사실 개발을 깊이 공부해본 적도 없고, 컴퓨터공학과를 다니면서 많은 수업을
foden2000.tistory.com
의존성, 의존성 주입?(2)
시작하기 전..구글링 + 여러 포스팅을 통해 작성된 내용을 저만의 방식으로 해석한 것이라 틀린 부분이 있을 수도 있습니다.언제든 댓글로 의견, 조언 환영입니다~!지난 시간에..의존성 주입의
foden2000.tistory.com
Clean Architecture 적용해보기
개요네이버 부스트캠프 마스터 클래스, 미션을 수행하는 과정에서 아키텍처에 관한 이야기를 많이 접하게 되었다.기능 구현에만 급급하던 상황을 보냈다.다시 한 번 SOLID 원칙을 점검하며 리팩
foden2000.tistory.com
그렇다면 저 하나하나 들어있는 Action, State… 이런 말들은 무엇일까요?
그에 대한 내용도 아래 글에 나와있습니다.
단방향 / 양방향 데이터 흐름
오늘은 프로젝트에서 데이터 바인딩을 설계하는 과정에서, 아래와 같은 항목에 대해 정리해볼 예정입니다.데이터 바인딩이란?단방향 데이터 바인딩양방향 데이터 바인딩현재 프로젝트에서는
foden2000.tistory.com
나의 첫 PR
MVVM에서의 데이터 바인딩을 어떻게 할 지도 결정하였습니다.
이젠 저의 첫 PR에 대해서도 간단히 이야기를 해볼 예정입니다.
제가 2주차에서 맡았던 Task는 커스텀 탭바 구현입니다.
커스텀 탭바 구성 내용에 대해서는 추후 정리하도록 할 예정입니다.
저의 첫 PR입니다.
[Feat] #2 Create CustomTabBarController by ParkSeongGeun · Pull Request #4 · boostcampwm-2024/iOS05-Heim
🔑 연관된 Issue 📌 [Feat, Design] 커스텀 탭바 컨트롤러 구현 #2 📘 작업 유형 Custom 탭바 구현 📙 작업 내역 Presentation 모듈 하위에 디렉토리를 생성하여 파일을 생성하였습니다. ├── Presentation
github.com
- 지금은 Closed 되어 있습니다..
여기서 받은 피드백 중 다음과 같은 리뷰가 있었습니다.
- 간단히 이야기하자면 ViewController에서 너무 많은 책임을 지고 있어
- Unit Test를 진행하거나, 유지 보수 차원에서 좋지 않은 코드인 점을 고려하는 것이 좋을 거 같다.
라는 리뷰를 받게 된 것이다.
그렇다면 원래 코드는 어땠길래 이와 같은 피드백을 받게 되었을까요?
- Before Refactoring
현재의 CustomTabBarViewController의 역할에 대해 정리해 보면
- 커스텀 탭바 디자인
- 레이아웃 구성
- 사용자 인터렉션 관리
- 화면 전환
이 모든 역할을 수행합니다.
너무나도 많은 책임을 지고 있습니다. 그리고 앞서 설정하기로 했던 MVVM은 도입을 하지도 않았습니다.
사실 이에 대한 근거를 가지고 있었지만, 현재 내 코드만을 보고 알 수 있는 문제점에 대해 알아보겠습니다.
1. View에 대한 재사용성이 부족합니다.
- 물론 이 부분은 아니지만(현재 프로젝트에서 탭바를 재사용하진 않지만)
- 뷰에 대한 UI, 레이아웃의 수정만이 일어나야 된다면
- VC에서 하나하나 찾아서 수정을 해야 합니다.
2. ViewController가 UI 표시, 상태 관리, 비즈니스 로직을 모두 처리합니다.
- 책임이 너무 많은… 즉, SRP에 위반되는 객체입니다.
3. Unit Test를 하기에 적합하지 않습니다.
- 하나의 예시를 보겠습니다.
private func updateSelectedViewController() {
let previousVC = tabItems[previousIndex].viewController
previousVC.willMove(toParent: nil)
previousVC.view.removeFromSuperview()
previousVC.removeFromParent()
let currentVC = tabItems[currentIndex].viewController
addChild(currentVC)
view.insertSubview(currentVC.view, belowSubview: tabBarView)
currentVC.view.snp.makeConstraints {
$0.edges.equalToSuperview()
}
currentVC.didMove(toParent: self)
updateButtonAppearance()
}
이 함수는 UI와 비즈니스 로직이 섞여 있습니다. (+SnapKit에 대한 의존성도 존재)
- 그렇다면 해당 함수에 대해서 테스트를 진행하려면 어떻게 할 수 있을까요?
func testTabSelection() {
// 문제점
// 1. UIViewController 의존성으로 인한 테스트 환경 구성의 복잡성
// 2. UI 상태와 비즈니스 로직이 결합되어 독립적인 테스트 불가
// 3. 상태 변경을 검증하기 위해 UI 이벤트를 시뮬레이션해야 함
// 4. SnapKit 의존성으로 인한 제약
}
- Tab이 원하는 대로 선택되는지에 대해서 테스트를 진행하려면 수많은 난관을 이겨내야.. 합니다.
4. 상태 관리의 분산
- 상태 변경에 관련된 코드들이 여러 메서드에 분산되어 있어 동기화가 어렵습니다.
- 이는 디버깅에서의 추적 어려움, 상태가 동시에 변경될 때 문제 발생 지점이 어려울 수 있습니다.
이렇게만 봐도 너무 많은 개선점이 보입니다.
리팩토링을 경험하며
리팩토링을 하며 주요 개선점으로 삼은 항목은 다음과 같습니다.
- 객체의 책임 분리
- MVVM 적용
개선된 이후의 코드는 다음과 같습니다.
구체적인 개선 사항은 아래와 같습니다.
- UI / 비즈니스 로직의 분리
- View: UI 컴포넌트 / 레이아웃 관련 로직
- ViewController: 앱의 뷰 계층 구조를 관리, 생명주기 관리
- ViewModel: 탭 선택 / 상태 관리 로직
- [상태 변경과 UI 업데이트가 단방향 데이터 흐름으로 개선되었습니다]
- 상태 관리
- 이전에는 ViewController에서 분산되게 상태를 관리했다면,
- 리팩토링 이후엔 ViewModel의 State 구조체에서 관리를 합니다.
- 테스트가 용이하도록 개선
- 비즈니스 로직만을 위한 테스트를 작성하기 쉬워졌습니다.
final class CustomTabBarViewModelTests: XCTestCase {
var sut: CustomTabBarViewModel!
override func setUp() {
super.setUp()
sut = CustomTabBarViewModel()
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_TabBar_InitialState() {
XCTAssertEqual(sut.state.currentTab, .home)
}
func test_WhenSelectingSameTab_StateRemains() {
// Given
sut.action(.tabSelected(.home))
let initialState = sut.state
// When
sut.action(.tabSelected(.home))
// Then
XCTAssertEqual(sut.state, initialState)
}
}
마무리하며
사실 PR 리뷰에 대한 댓글이 이에 대한 항목만 존재하는 것은 아니었습니다.
팀원 분들 모두 첫 PR인 만큼 더욱 꼼꼼히 봐주시고 / 피드백을 주셨습니다.
- private let / lazy var의 차이
- 앱 종료 방법
- 관성 코딩에 대한 피드백..
등 여러 항목이 있었는데, 하나하나 주옥같은 리뷰였습니다.
프로젝트는 학습 스프린트 때처럼 나만의 관성대로 작업을 진행하는 곳이 아니라는 사실, 조금 더 유지보수성에 대해서 고려를 할 필요가 있다는 것을 느끼게 된 리뷰가 된 것 같습니다.
앞으로의 4주간의 프로젝트 여정에서, 잊지 못할 경험이 되었으면 좋겠습니다. 팀원 분들과 함께 한 지 2주밖에 되지 않았지만, 너무나도 뛰어나시고, 좋은 팀원분들을 만난 것 같습니다.
다시 한번 감사하다고 느낍니다.
'iOS' 카테고리의 다른 글
DynamicStackView 간단히 만들어보기 (0) | 2024.12.29 |
---|---|
단방향 / 양방향 데이터 흐름 (0) | 2024.11.10 |
MVVM, Binding (0) | 2024.10.30 |
Clean Architecture 적용해보기 (2) | 2024.09.29 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |