해당 포스팅은 유튜브 영화&드라마 리뷰 영상 큐레이션 플랫폼
Plotz
를 개발하면서 도입된 기술 및 방법론에 대한 내용을 다루고 있습니다.
다운로드 링크 : 앱스토어 / 플레이스토어
Plotz 앱은 기본적으로 OOP(객체지향)
를 기반한 MVVM(Model-View-ViewModel)
아키텍쳐를 준수하고 있습니다. MVVM은 많은 장점이 있지만, 몇 가지 단점도 동반합니다. 그중에서도 프로젝트의 규모가 커지고 한 화면에서 다루어야 할 비즈니스 로직들이나 데이터들이 많을수록 ViewModel 무거워지고 복잡해지는 것
이 가장 큰 단점
이라고 할 수 있습니다.
객체치향 프로그래밍
이란 처음에 이루고자 하는 목표에서부터, 덩어리들을 차근차근 분리
하고 깍아내는 과정
이라고 합니다. 이러한 관점으로 복잡해진 ViewModel을 인간이 쉽게 이해할 수 있을 정도로 쪼개는 작업
이 필요합니다. 케이크를 먹을 때도 먹기 좋게 슬라이스하는 것처럼 말이죠.
ViewModel를 쪼개는(구조화) 방법은 여러 가지가 있습니다.
extension, part 키워드를 활용하여 ViewModel 리소스를 분리
상황과 목적에 따라 위에 방법들을 적절히 사용하는 게 중요하겠지만, 본 포스팅에서는 extension, part 키워드를 이용하여 ViewModel 리소스를 분리하여 관리
하는 방법과 그 이점에 대해 다루어보려고 합니다.
Plotz 앱의 '콘텐츠 상세 스크린'을 예시로 합니다.
먼저 섹션을 구분해야됩니다.
콘텐츠의 여러 정보를 보여주는 '콘텐츠 상세 스크린'을 크게 3가지의 섹션으로 정의했습니다.
NOTE
각 섹션은 보통 하나의 주제나 레이아웃으로 구분됩니다.
섹션을 정의했으면 이제 섹션별로 ViewModel의 데이터 접근을 구조화
해 볼 수 있습니다.
class ContentDetailViewModel extends BaseViewModel {
// 콘텐츠 정보(제목, 장르, 개봉년도, 출연진, 등등)
final Content contentInfo;
// 유튜브 채널 정보 (채녈명, 구독자 수, 등등)
final Video videoInfo;
//유튜브 영상 정보 (조회수, 좋아요 수, 업로드일)
final Channel channleInfo;
...
}
콘텐츠 상세 페이지에는 콘텐츠
, 채널
, 영상
이렇게 3가지 유형의 데이터가 존재하고 ViewModel 클래스에서 각 객체를 관리하고 있습니다.
final vm = ContentDetailViewModel()
// 헤더 섹션
Column(
children: [
Text(vm.contentInfo.title.korean), // 제목
Text(vm.contentInfo.genres.name), // 장르
Text(vm.videoInfo.title) // 비디오 제목
]
)
ViewModel에서 관리하고 있는 데이터의 모델이 위와 같이 중첩된 형태
이기 때문에 UI 위젯에서 데이터를 접근할 때 dot notation
, 즉 .(온점)을 이용하여 필요한 객체에 접근하고 있습니다. 이 방식도 문제가 있는건 아니지만, ViewModel 클래스에서 getter
메소드를 이용하여 데이터 접근을 구조화하면 조금 간편하고 쉽게 UI 위젯에서 데이터를 받을 수 있게 됩니다.
getter란?
getter
는 클래스 내부의 속성을 외부에서 읽을 수 있게 해주는 메소드입니다. Getter는 일반적으로 클래스의 내부 속성을 직접 접근하는 대신, 속성 값을 반환하는 데 사용됩니다.
class ContentDetailViewModel extends BaseViewModel {
/* Variables */
final Content _contentInfo;
final Video _videoInfo;
final Channel _channleInfo;
/* Getters */
String title => _contentInfo.title.korean;
String genres => _contentInfo.genres.name;
String videoTitle => _contentInfo.videoInfo.title;
...
}
위 코드와 같이 ViewModel 클래스 안에서 UI 위젯에서 필요한 데이터들을 getter메소드를 통해 접근할 수 있게 명확히 구분 지어 주면 여러 가지 이점
이 있습니다.
final vm = ContentDetailViewModel()
// getter 적용버전
Column(
children: [
Text(vm.title),
Text(vm.genres),
Text(vm.videoTitle)
]
)
// 이전 버전
Column(
children: [
Text(vm.contentInfo.title.korean), // 제목
Text(vm.contentInfo.genres.name), // 장르
Text(vm.videoInfo.title) // 비디오 제목
]
)
첫 번째로, getter를 이용해서 데이터 구조를 ViewModel에 캡슐화함
으로써 UI 코드가 더 추상화되고 단순해집니다. UI 코드에서는 데이터의 구체적인 구조를 알 필요가 없으며, 간결하게 접근
할 수 있게 됩니다.
/// ex) 데이터 모델이 변경되었을 때
/// 기존 값 -> _contentInfo.title.korean
String title => _contentInfo.title;
Column(
children: [
Text(vm.title),
]
)
ViewModel에서 관리하는 객체의 내부 구현을 변경하거나 확장할 때, getter를 통해 접근하는 UI 코드는 그대로 유지될 가능성이 높습니다. 새로운 데이터 속성을 추가하거나 기존 속성을 변경해도 getter의 시그니처는 유지되므로, ViewModel과 View의 의존성이 분리
되어 UI 코드 변경이 최소화되는 효과를 얻습니다.
// 날짜 포맷 변경 로직, 2008-01-20 --> 2008
String releaseYear => Formatter.dateToYear(_contentInfo.releaseDate)
getter는 단순히 속성값을 반환하는 것 이상의 역할을 할 수 있습니다. 계산된 값, 변환된 데이터, 다른 속성들의 조합 등을 반환하는 로직을 getter 내부에 넣을 수 있습니다. 이로써 코드의 가독성을 높이고 재사용성을 증가시킬 수 있습니다.
마지막 단계입니다. 이제 ViewModel 리소스들을 분리해 주면 됩니다. 이 단계에서 part
와 그리고 extension
키워드가 사용됩니다.
먼저 위에서 정의한 섹션별로 part 파일
을 각각 만들어 줍니다.
NOTE
part파일의 경로나 파일명은 유동적으로 변경하셔도 무방합니다.
content_detail_view_model.dart
part 'resources/header.p.dart'; // 헤더 영역
part 'resources/tab1.p.dart'; // tab1 영역
part 'resources/tab2.p.dart'; // tab2 영역
class ContentDetailViewModel extends BaseViewModel {
...
}
그다음 ViewModel 소스파일에서 part 파일
을 호출해 주고,
resources/header.dart
part of '../content_detail_view_model.dart';
extension HeaderResources on ContentDetailViewModel {
....
}
생성한 part 파일에는 part of
디렉티브를 사용하여 ViewModel 소스파일에서 part 파일의 리소스들에 접근할 수 있도록 연결해 줍니다. 그리고 가장 중요한 부분인데, 해당 part 파일에 ViewModel을 확장하는 extension
을 정의 해주면 됩니다.
이렇게 part파일 안에 ViewModel을 확장하는 extension
을 만들어 주는 이유는 part파일에서 ViewModel 자원에 접근하기 위함
입니다. part of
디렉티브를 사용하여 ViewModel에서 part파일 자원에 접근할 수 있게하고 extension을
만들어줘 part파일이 ViewModel 자원에 접근할 수 있는 형태인 거죠.
resources/header.dart
part of '../content_detail_view_model.dart';
extension HeaderResources on ContentDetailViewModel {
/* Getters */
String title => _contentInfo.title.korean;
String genres => _contentInfo.genres.name;
String videoTitle => _contentInfo.videoInfo.title;
/* Intent */
void heaerMethod1() {...}
void heaerMethod2() {...}
void heaerMethod3() {...}
}
마지막으로 각 part파일에 각 섹션에 해당하는 이벤트 메소드
와 getter 구문
을 기존 ViewModel 클래스로부터 옮겨주어 리소스를 분리합니다.
NOTE
extension 내부에서변수
를 선언하는 것은 허용되지 않은 점에 유의해주세요. extension은 기존 클래스에 새로운 기능을 추가하는 메커니즘으로, 클래스의 인스턴스 변수나 속성을 직접 추가할 수 없습니다. Extension은 클래스의 인스턴스 메서드나 getter, setter, 일부 특정 메타데이터만을 추가할 수 있습니다.
모든 섹션에서 공통으로 사용되는 리소스는 기존 ViewModel 클래스에서 관리하고, 각 섹션에서 독립적으로 사용되는 리소스들을 위와 같이 part 파일로 분리하여 관리하는 게 바람직합니다.
이렇게 ViewModel 리소스를 분리하여 관리하면 어떤 이점
이 있을까요?
ViewModel 크고 복잡해질수록 가독성이 중요해집니다. ViewModel 리소스를 분리하여 관리하면 정의한 섹션의 코드가 하나의 파일에 모여 있어서 코드의 목적
을 파악하기 쉬워집니다.
ViewModel이나 다른 로직이 part 파일로 분리되면, 해당 부분을 수정하거나 확장할 때 다른 부분에 영향을 덜 줍니다. 변경이 필요한 부분만 수정
하여 기능을 개선하거나 버그를 수정할 수 있습니다.
여러 명의 개발자가 협업
하는 경우, 코드의 구조를 일관성 있게 유지하고 변경 사항을 더 쉽게 추적하는 데 유리할 수 있습니다. 각각의 part 파일을 담당하는 팀원들이 독립적으로 작업할 수 있게 되는 거죠.
예를 들어 한 화면에서 각 영역별로 한명씩 구현 작업을 분담한다고 했을 때 part 파일
을 분리하면 각자의 작업영역
을 명확하게 할 수 있기 때문에 불필요하게 git conflict
가 나는 상황을 방지할 수 있습니다.
이번 포스팅에서는 MVVM 패턴에서 ViewModel 리소스를 구조화하여 관리하는 방법과 그 이점에 대해 알아보았습니다. ViewModel 리소스를 분리하면 여러 분명 이점이 있지만 상황과 목적에 맞게 적절히 사용해야 합니다. 필요 이상으로 많은 part 파일을 생성하여 ViewModel을 분리하다 보면, 오히려 코드의 문맥
을 이해하기 어려워질 수 있기 때문이죠. 상황과 목적을 고려하여 ViewModel을 깔끔하게 관리해 보시죠!
이런 유용한 정보를 나눠주셔서 감사합니다.