들어가며..
엄청난 슈퍼 개발자가 아니라면, 빠른 개발과 좋은 설계 사이의 트레이드오프는 분명 있을 것이다.
앱을 직접 배포하면서 느낀 것이고, 그리고 아마 현업에서도 이럴 것이라고 생각한다.
보통 새롭게 추가해야하는 기능이 무한정 추가되지는 않는다.
서비스가 어느 정도 궤도에 오르면 새롭게 기능을 추가하는 것에 시간을 쓰기보다는, 유지보수를 하게 되고 좋은 코드를 고민해보게 된다.
나의 앱도 어느 정도 궤도에 올라서 좋은 코드와 설계에 대해 고민하고 싶어졌는데 항상 확신을 가지지 못했던 MVVM구조로 잘 설계되었는가에 대한 고민을 한번 해보고 글로 작성해보려 한다.
내 앱은 Provider를 이용하여 MVVM 구조로 나름 설계되어 있다.
앱의 기능상 문제는 전혀 없지만, 그건 아직 앱이 크지 않아서, 처리할 데이터가 많지 않아서라는 느낌이 강하게 들었다.
또한, 최근에 Riverpod을 알아보면서 어떻게 상태관리 툴과 MVVM 구조를 사용하는게 맞는 건지 다시 생각해보았다.
예를들어 예제들은 보통 List<Model>마다 provider를 생성해주지만, 나의 앱은 ViewModel이라는 클래스에 여러 개의 변수가 있고 해당 클래스가 너무 커졌다는 느낌이 들었기 때문이다.
이번 글에서는 ChatGPT를 내 스승으로 삼아서 여러 대화를 해보도록 하겠다.
물론 ChatGPT가 알려주는 것이 100% 정답은 아니지만 방향성은 잡아갈 수 있다.
MVVM?
우선 MVVM
Model - View - ViewModel 로 이루어진 형태이다.
핵심은 뷰와 뷰모델이 데이터 바인딩 되어 있어서 뷰모델에서 업데이트가 일어나면 자동으로 뷰를 업데이트 해준다.
Flutter에서는 뷰에 해당하는 위젯을 다시 빌드해주는 것이다.
결국 데이터 바인딩이 되어있어야 하는데 상태관리 툴은 이를 쉽게 해준다.
그럼 어떻게 쉽게 해주는걸까?
MVVM에 Provider(혹은 다른 상태관리 툴)가 필요한 이유?
우선 Provider부터 다시 살펴보고 가자면, provider에는 다양한 provider가 있다.
기본 provider 랑 ChangeNotifierProvider 의 차이는 무엇일까?
앞서 MVVM 구조는 상태 변화에 따라 자동으로 위젯을 업데이트 해주어야 한다고 했다.
즉, Provider 보다는 ChangeNotifierProvider가 MVVM에 알맞다고 추측해볼 수 있다.
관련 질문을 하나 해본다.
역시 ChangeNotifierProvider를 통해 MVVM을 효과적으로 구현할 수 있다고 한다.
즉, 단순 Provider 패키지를 사용한다고 MVVM을 효과적으로 구현할 수 있는 것이 아닌 ChangeNotifier의 상태가 변경되었을 때 알림을 보내는 기능, 그리고 ChangeNotifierProvider가 관련 위젯을 다시 빌드해주는 기능 때문에 MVVM에 효과적인 것이다.
내 앱의 뷰모델
그럼 내 앱의 뷰모델을 보자..
class SmokingRecordListViewModel with ChangeNotifier {
late final SmokingRecordRepository _smokingRecordRepository;
LinkedHashMap<DateTime, List<SmokingRecord>> get smokingRecordLinkedHashMap => _smokingRecordLinkedHashMap;
LinkedHashMap<DateTime, List<SmokingRecord>> _smokingRecordLinkedHashMap = LinkedHashMap<DateTime, List<SmokingRecord>>(
equals: isSameDay,
hashCode: getHashCode,
);
DateTime? get lastSmokingTime => _lastSmokingTime;
DateTime? _lastSmokingTime = null;
SmokingRecordListViewModel() {
_smokingRecordRepository = SmokingRecordRepository();
updateSmokingRecordLinkedHashMap();
getLastSmokingTime();
notifyListeners();
}
/*
...
다양한 메소드들
...
*/
}
SmokingRecordListViewModel이라는 클래스 내에
- _smokingRecordRepository
- _smokingRecordLinkedHashMap
- _lastSmokingTime
세 가지 변수가 있다.
_smokingRecordRepository는 모델과 연결해주는 변수이고, 뷰모델이 생성될 때만 초기화된다.
핵심은 흡연 기록이 담긴 _smokingRecordLinkedHashMap와 마지막 흡연 시간이 담긴 _lastSmokingTime 변수이고 이 변수가 업데이트 될 때마다 관련 위젯을 빌드해주어서 UI를 바꿔주는 형태인 것이다.
나의 결론
내가 이해한 바에 따르면 이건 틀린 설계는 아니라고 결론을 내었다.
왜 그렇게 생각했냐면, 결국 UI의 변경이 필요한 시점은 흡연기록이 추가되어 흡연기록리스트가 바뀔 때고 나의 뷰모델은 해당 기능을 잘 수행해준다고 생각한다.
다만 고민할 점과 개선할 점은 있다.
고민
그럼 _lastSmokingTime을 _smokingRecordLinkedHashMap 하나의 클래스에 관리하는게 맞을까?
그럼 어떤 기준으로 변수를 같은 뷰모델에 넣을지 다른 뷰모델에 넣을지 정할까?
같은 고민을 할 수 있을 것 같다.
결국 정답이 없는 문제다.
하지만 나의 경우 _lastSmokingTime이 _smokingRecordLinkedHashMap와 연관있는 smokingRecordList를 통해 구해지기 때문에 분명 같은 클래스 놓는 것이 맞다.
만약에 갑자기 음주 기록을 저장하고 싶어서 _drinkingRecordLinkedHashMap 을 쓰고 싶다면, ViewModel을 하나 만들어주는 것이 맞을 것이다..
개선점
SmokingRecordListViewModel _smokingRecordListViewModel = context.watch<SmokingRecordListViewModel>();
개선할 점은 분명 보였다.
뷰모델을 사용할 때 위와 같이 context.watch를 이용하는데 이러면 _lastSmokingTime가 업데이트 될 때만 빌드할 필요가 있는 위젯에서도 _smokingRecordLinkedHashMap이 업데이트 되었을 때도 빌드가 일어나는 쓸데없는 빌드가 일어나서 성능이 저하될 것 같다. 그 반대의 경우도 마찬가지이다.
context.select<T, R>(R cb(T value))
이 경우 context.select를 통해 특정 부분만 지켜보도록 해야할 것이다.
이를 염두에 두고 리팩터링을 진행하도록 하겠다.
정리
나의 뷰모델이 이상한 건 뷰모델의 변수들 때문이 아니다. 잘 살펴보니 메소드들을 리팩터링 해야 할 것 같다.
이러한 관점을 가지고, 중복되거나 쓸데 없는 내용을 줄여 뷰모델을 리팩터링 해봐야겠다..
'프로젝트 - 담타 > 리팩터링' 카테고리의 다른 글
[담타 - 리팩터링] 필요 없는 주석 제거 (0) | 2023.02.04 |
---|---|
[담타 - 리팩터링] 테마 및 컬러 관리 (0) | 2023.02.04 |