클린 아키텍쳐 1편 - UI Layer 원리 이해하기
UI(presentation) Layer 구성요소 알아보기
다음 예시를 통해 UI Layer의 개념에 대해 쉽게 알아보자.
위 이미지에서 UI의 구성요소들을 나눠서 생각해볼 수 있는데,
먼저 위 뉴스 앱에서 표시된 부분은 서버에서 data를 받아와서 UI에 표시되는 것으로 이렇게 UI에서 서버에서 Data를 받아와 필요한 데이터로 가공해 표시하는 데이터들을 UI State라고 한다.
그리고 그 data들을 시각적으로 표현하는 것들을 UI elements 라고 한다.
이렇게 우리가 흔히 말하는 UI란 화면의 UI Elements들에 UI State를 할당한 결과물을 의미하는 것이다.
UI State란?
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List
data class NewsItemUiState( val title: String, val body: String, val bookmarked: Boolean = false, … ) 위 뉴스 앱에서 볼 수 있었듯이 UI에 표시되는 데이터들을 말한다. 여기서 중요한 것은 사용자와의 상호작용(예를 들어, 버튼 클릭으로 인한 이벤트 발생)이나 다른 이벤트로 인해 UI에 표시되고 있는 데이터가 바뀐다면 UI State라고 생각하면 된다.
변하지 않는 특성에 주목! 안드로이드 공식문서에 따르면 UI State의 중요 이점 중 하나로는 불변 객체(즉, 한 번 생성되면 data가 변경되지 않음. 없애고 새로 만들어야 하는 것)인 것이라고 한다.
Why? UI State가 변하지 않는다는 것은 UI 요소가 이 데이터들을 임의로 조작해서 데이터 변경이 일어날 수 없다는 것이다. 이게 왜 중요하냐면 UI에서 임의로 데이터를 조작할 경우 서버에서 받아온 데이터를 조작한 데이터 중에 무엇을 쓸 것이냐가 문제가 되기 때문이다. 따라서 UI의 역할이 ui에서 사용할 데이터를 받고 이를 UI Elements에 전달해주기만 하면 되는 것이다.
데이터를 관리하는 역할을 분리하기!
우리가 살펴본 UI State의 경우 ViewModel 이라는 객체가 관리하게 되는데, 1) Data Layer에서 받아온 데이터를 2) ViewModel에서 UI Elements가 필요로 하는 형식으로 가공해서 3) UI Elements에 전달하고 4) UI Elements에서 발생한 이벤트를 받아 5) UI State가 변경되었음을 Data Layer에 알리고 6) 다시 받아서 가공해 UI Elements에 전달하는 것이다.
위에서 살펴본 뉴스 앱 이미지에서 사용자가 북마크를 하는 경우를 생각해볼 수 있다.
- 현재 유저의 북마크 상태를 서버에서 받아오고
- 이를 ViewModel에서 가공해 UI elements가 필요한 방식으로 가공해 전달
- 유저가 북마크 상태를 변경
- ViewModel에서 해당 북마크 상태의 변경을 data layer에 알리고
- Data Layer에서 변경된 데이터를 수정해서
- 새로운 북마크와 관련된 data를 ViewModel로 보내고
- 새로운 UI State를 UI elements에 전달하는 것이다.
이렇게 관리하면 데이터가 시작되고 변환되고 이를 받아서 표시하는 것까지 역할이 깔끔하게 분리될 수 있다는 점에서 사용하는 것이다!
Logic 이해하기
2가지 로직을 이해해야 해요.
- Business Logic: 제품의 요구사항에 맞도록 데이터를 가공하는 로직. 보통 domain 또는 data layer에 사용되고 UI Layer에서는 사용하지 않아요.
- UI Behavior logic or UI logic: 화면에 데이터의 변화를 어떻게 표시할 것인지를 말해요. 예를 들어, 유저가 버튼을 클릭했을 때 특정 화면으로 이동시킨다던지, snackbar 등을 이용해 유저에게 화면에 메시지를 알려준다던지 등입니다!
UI Elements에서 UiState 사용하기
UiState를 정의하고 state를 어떻게 관리할지 정했다면, 그렇게 생성된 state를 UI에 표시해줘야 해요.
이 때, UiState는 시간이 지나면서 유저와 상호작용 등을 통해 계속 변경되기 때문에 이를 자동적으로 감지해서 대응할 수 있도록 UI state를 Stream과 같은 객체에서 expose 하도록 해야 해요. 이는 UI에서 Stream 객체에서 관리하는 state를 구독함으로써 데이터 변경이 감지될 때마다 UI를 Rebuild함으로써 자동으로 대응할 수 있습니다.
즉, 플러터에서는 다음과 같이 사용할 수 있어요.
1. UiState 객체 정의
class NewsUiState {
List<NewsItem> newsItem;
NewsUiState({this.newsItem = const []});
// UiState는 변하지 않는 객체이므로 데이터를 변경시키려면
// copyWith 메서드를 사용해 기존의 객체를 복사하되 변경된 데이터만 해당 속성에 할당해 새로운 객체를 생성해 update해야 함.
NewsUiState copyWith({
List<NewsItem>? newsItems
}) {
return NewsUiState(
newsItems: newsItems ?? this.newsItems
);
}
}
2. ViewModel 정의
class NewsViewModel extends ChangeNotifier {
//Data Layer에서 NewsItem을 받아옴.
final NewsRepository repository;
NewsViewModel(this.repository);
//Data Layer에서 받아온 데이터를 할당해 관리할 객체 생성
NewsUiState _uiState = NewsUiState();
//관리하는 UiState는 불변객체이므로 외부에서 수정이 불가능하도록 해야 하므로 Getter를 사용해 해당 데이터에 접근할 수 있도록 함.
NewsUiState get uiState => _uiState;
void fetchArticles(String category) async {
try {
final newsItems = await repository.newsItemsForCategory(category);
_uiState = _uiState.copyWith(
newsItems: newsItems,
);
notifyListeners();
} catch (e) {
_uiState = _uiState.copyWith(
errorMessage: e.toString(),
);
notifyListeners();
}
}
3. ChangeNotifierProvider에 해당 ViewModel을 등록하고 Consumer위젯을 사용해 UI 객체에서 접근하여 사용가능하도록 함.
}
댓글남기기