-
[Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#3 (Presentation-Layer)Flutter/project 2024. 1. 30. 10:31
이전 글에서는 클라이언트단의 비즈니스 로직을 다루는 Application-Layer에 대해 말하면서 Result 패턴을 활용해 레이어 간의 독립성을 강화시키는 법 등을 소개했었습니다. 이번에는 사용자와 가장 근접해 있는 view, screen, widget 등을 다루는 Presentation-Layer에 대해 이야기를 나눠보겠습니다. 제 프로젝트를 기반으로 설명하는 거라 아키텍처와 무관한 내용이 포함될 수 있습니다.
❗️주의: 스스로 학습하며 구성한 프로젝트라 현직자 분들이 보시면 이상하게 느끼실 수 있습니다. "쟤는 저런 식으로 해봤구나" 정도로만 봐주시고 만약 틀린 부분을 발견하셨다면 피드백 남겨주시면 감사하겠습니다
해당 글은 이전 글에서 계속 이어집니다. 순서대로 보시고 싶은 분은 아래 글을 참고해 주세요!
[Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#1 (Data, Domain)
창업 아이템이 정해지고 나서 Flutter를 학습했기 때문에 프로젝트의 모든 과정이 새로웠다. 특히 아키텍처의 도입은 볼륨이 커질수록 생각만큼 매끄럽게 적용하기 어려웠습니다. 이때 제가 겪었
nomal-dev.tistory.com
[Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#2 (Application)
❗️주의: 스스로 학습하며 구성한 프로젝트라 현직자 분들이 보시면 이상하게 느끼 실수 있습니다. "쟤는 저런 식으로 해봤구나" 정도로만 봐주시고 만약 틀린 부분을 발견하셨다면 피드백 남
nomal-dev.tistory.com
저번 글에서는 Application layer의 service는 repostory와 controller 사이에서 중개자 역할을 하며, 컨트롤러에게 상태(state)를 반환해준다고 했습니다. 그럼 컨트롤러는 무엇이고, 컨트롤러는 상태를 가지고 어떤 것을 하는 걸까요?
바로 알아보도록 하겠습니다.
✓Presentation Layer
프레젠테이션 레이어는 이름에서부터 알 수 있듯이 사용자 인터페이스(UI)를 담당하는 레이어입니다. 그렇기 때문에 화면을 구성하는 요소인 widget, component, view, screen을 정의하고 있으며 위젯에 종속된 간단한 로직(helper)이나 상태를 관리하는 controller를 포함하고 있습니다.
UI 구성요소에 대해서는 다룬 글이 있으니 참고해 주세요.
[Flutter] 위젯, 뷰, 컴포넌트, 스크린 어떤 차이일까? UI 구조를 잡아보자!
처음 flutter 개발을 시작하면 가장 먼저 접하는 것이 widget을 이용해 간단한 UI를 제작하는 것입니다. 학습을 하는 단계에서는 위젯을 간단하게 사용해서 화면을 만들지만, UI/UX디자이너와 협업을
nomal-dev.tistory.com
📌 Controller
위와 같이 장바구니 화면에 진입을 하면 서비스에서는 Error 또는 Data 또는 Empty 상태를 컨트롤러에 전달합니다.
그리고 컨트롤러는 전달받은 상태로 최신화를 시켜주고 이를 Observes(ref.watch)하고 있던 screen에서는 상태에 맞는 view 또는 widget를 보여줍니다.
시나리오를 정리하자면 다음과 같습니다.
(현재 상태는 Loading이라고 가정하겠습니다)
- 유저가 장바구니 버튼을 클릭한다.
- repostory에서 api 통신이 일어나고 결과를 반환한다.
- 반환된 결과를 service에서 controller로 상태로써 전달된다.
- controller는 현재 상태(Loading)를 전달받은(Loading ➡️ Data)로 업데이트한다.
상태(state)에 대해 자세히 알고 싶으면 아래 글을 참고해 주세요
[Flutter] 상태관리는 어떻게 해야하는 걸까요? feat. sealed class
퍼블리싱만 하던 단계에선 상태관리가 무엇인지 신경 쓰지 않고 setState()를 남발하면서 만들었습니다. 하지만 api와 연동할 때쯤에 프로젝트가 난잡해져서 결국 눈물을 머금고 setState()를 걷어내
nomal-dev.tistory.com
✍🏼 역할
- UI와 비즈니스 로직을 분리함
- Controller는 Service의 비즈니스 로직을 실행시키면서 위젯의 상태를 변화시키고 관리함
- MVVM의 ViewModel과 동일하며, Bloc의 cubit과 같은 역할을 한다.
✍🏼 특징
- 컨트롤러는 상태를 변경시키는 로직을 모두 포함하기 때문에 때론 api 호출 필요 없이 인터페이스 조작만으로 상태를 변경(혹은 copyWith()함수를 통해 해당 상태의 Model 속 데이터를 변경) 하는 경우엔 컨트롤러에 클라이언트에만 존재하는 메서드가 있을 수 있습니다.
controller를 riverpod으로 구성하면 다음과 같이 간단하게 만들수있습니다.
@Riverpod(keepAlive: true) class UserCartController extends _$UserCartController { @override Cart build() { return const CartLoading(); } //📌 유저 카트목록 가져오기 getUserCart() async { final result = await ref.read(cartServiceProvider).getUserCart(); state = result; } }
📌 UI 반영
이해를 돕기 위해 바로 장바구니 스크린 부분 코드로 확인해 보겠습니다.
class UserCartScreen extends ConsumerStatefulWidget { static String get routeName => 'user_cart'; const UserCartScreen({ super.key, }); @override ConsumerState<UserCartScreen> createState() => _UserCartScreenState(); } class _UserCartScreenState extends ConsumerState<UserCartScreen> { @override void initState() { ref.read(userCartControllerProvider.notifier).getUserCart(); super.initState(); } @override Widget build(BuildContext context) { final state = ref.watch(userCartControllerProvider); return DefaultLayout( // 바텀시트 생략 child: CustomScrollView( slivers: [ //📌 [ Loading ] if (state is CartLoading) CartLoadingView(), // 📌 [ Error ] if (state is CartError) CartErrorView(), //📌 [ Empty ] if (state is CartEmpty) CartEmptyView(), //📌 [ Data ] if (state is CartData) CartDataView(), ], )); } }
widget은 컨트롤러의 상태를 Observes 하고 있기 때문에 언제든지 해당 상태, 데이터에 맞게 유저에게 보여줄 준비가 되어있습니다.
- final state = ref.watch(userCartControllerProvider);
그리고 상태가 변할 때마다 어떤 view(또는 widget)를 띄울 것인지 정의합니다.
- if (state is CartLoading)
- return CartLoadingView();
- if (state is CartError)
- return CartErrorView();
- if (state is CartEmpty)
- return CartEmptyView();
- if (state is CartData)
- return CartDataView();
저 같은 경우에는 화면 전체가 바뀌는 상태(ex. 네트워크 통신 상태)의 경우 일반적으로 생각하는 widget보다 큰 범위고 screen보단 작은 범위라고 생각해서 view라고 따로 지칭했습니다.
관련 내용은 상단에 링크된 "위젯, 뷰, 컴포넌트, 스크린 어떤 차이일까?"라는 글에 정리해 뒀습니다.📌 State 파일 관리 (개인적인 생각)
그럼 상태를 정리해 둔 파일은 프로젝트의 어느 위치에 두는 게 좋을까요? 상태를 관리하는 controller가 있는 프레젠테이션 레이어에 두어야할까요, 아니면 상태를 리턴하는 service가 있는 어플리케이션 레이어에 두는게 좋을까요?
저는 프로젝트 초기엔 프레젠테이션 레이어에 두고 진행을 했었습니다. 아무래도 상태를 관리하는 것은 컨트롤러의 영역이라고 생각했기 때문이었죠. 하지만 실제 개발이 진행될 때는 service를 작성할 때 리턴해야 하는 상태가 필요하기 때문에 service를 작성하는 시점에 상태도 작성해야 했습니다. 현재 내가 작업 중인 공간은 애플리케이션 레이어이기때문에 상태를 작성할때도 어플리케이션 레이어의 관심사에 영향을 받을 수 밖에 없었습니다. 이런 개발 흐름대로라면 유지보수 측면에서 그 상태를 리턴받는 프레젠테이션 레이어에도 어플리케이션 레이어에 있는 관심사와 동일한 관심사를 생성했어야 했죠. 레이어 간에 독립성을 보장하는 리버팟 아키텍처에서는 어떤 레이어가 다른 레이어에 종속되는 듯한 느낌을 주는 해당 방식은 지양해야 한다는 생각이 들었습니다.
그래서 저는 상태를 작성한 파일들은 어느 레이어 속해있지 않도록 레이어 바깥으로 뺐습니다.
이렇게 구조를 잡으니 개발할 때 가시적으로 파일을 확인하고 효율적으로 관리할 수 있어서 마음에 들었던 방식이었습니다.
드디어 프로젝트 구조 및 아키텍처에 대한 이야기를 모두 마쳤습니다.
프로젝트 구조를 잡는 방법엔 정답이 없습니다. 구조를 잡는 이유, 아키텍처를 도입하는 이유는 크게 보면 결국 개발자에게 편리함을 주기 위함이라고 생각하기 때문에 꼭 어떤 이론적인 부분에 핏 하게 맞추려고 하기보단 때론 상황에 따라 본인, 팀이 편한 방향으로 러프하게 프로젝트를 꾸려가는 것이 좋은 방향이라고 생각합니다.
이 글이 꼭 정답은 아닙니다. 잘못된 부분이나 부족한 부분을 알려주시면 학습 후 수정하겠습니다.
'Flutter > project' 카테고리의 다른 글
[Flutter] JWT 토큰관리 및 자동로그인 구현하기 feat. Dio Interceptor, Social Login (2) 2024.02.06 [Flutter] 위젯, 뷰, 컴포넌트, 스크린 어떤 차이일까? UI 구조를 잡아보자! (0) 2024.01.30 [Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#1 (Data, Domain) (1) 2024.01.22 [Flutter] 관심사가 우선일까, 레이어가 우선일까? (0) 2024.01.18 [Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#2 (Application-Layer) (3) 2024.01.17