flutter MVVM 아키텍처 패턴

Angela Jeong·2024년 1월 2일

Flutter 아키텍처 

목록 보기
1/3
post-thumbnail

데이터통신 시리즈 를 통해 플러터에서 데이터를 통신하여 UI를 그려내는 것까지 성공을 했다.
하지만 거기서 끝나는 게 아니라 구조적인 설계를 하는 것까지 나아가야 한다.

간단하게 말하면, 지금까지 UI와 로직을 한 페이지에 다 때려넣어 완성했다면 앞으로는 기능 별로 폴더를 나눠 작업을 해야한다.

그러니까 지금까지 이렇게 main, screen, model로 간단하게 나누어 개발을 했다면

앞으로는 이렇게 기능 별로 구분되어야 한다는 것이다.

난 그 중에서도 MVVM이라는 패턴을 배우고 적용했다.

MVVM 패턴이란?

아키텍처 패턴 (Architectural Pattern) 중의 하나인데, 아키텍처 패턴이란 소프트웨어의 전체적인 구조와 조직을 다루는 패턴으로 대규모 앱의 모듈화, 유지보수성 향상, 테스트 용이성 등을 고려하여 시스템 전체의 구조를 설계하는 데 사용된다.

아키텍처 패턴을 쓰면 소프트웨어 시스템을 더 효과적으로 구성하고 관리할 수 있다. MVC (Model-View-Controller), MVVM (Model-View-ViewModel), Clean Architecture 등의 종류가 있다.



데이터통신 시리즈 에서 다뤘던 작업을 MVVM 패턴으로 리펙토링 해보자!
그런데 dto, mapper까지 한 번에 적용하면 너무 어려우니까... 오늘은 repository와 viewmodel만 다뤄볼 것이다.

step1. Repository 구성

Repository 란 데이터 저장소라는 뜻으로 DataSource에 접근하는 역할을 한다. 즉, 기존에 main_screen에서 곧 바로 데이터를 불러오는 함수를 작성했었는데, UI와 로직 분리를 위하여 함수를 repository로 분리시키는 것이다. 앞으로 데이터는 이 repository가 담게 된다.

class StoreRepository {
  Future<List<Store>> fetch() async {
    final List<Store> stores = [];
    var response = await http.get(Uri.parse('http://www.example.com));
    final jsonResult = jsonDecode(response.body);
    final jsonStores = jsonResult['stores'];

    jsonStores.forEach((e) {
      stores.add(Store.fromJson(e));
    });

    return stores.where((e)
    => e.remainStat == 'plenty' ||
        e.remainStat == 'some' ||
        e.remainStat == 'few'
    ).toList();
  }
}
  • 주목해야할 부분은 이전과 달리 fetch() 함수에 return이 생긴다는 것이다.
  • 레퍼지토리로 이동한 fetch 함수는 더이상 void가 될 수 없다. 이제 main_screen에서 fetch() 함수를 호출해야 하기 때문에 반드시 이 함수는 반환값이 있어야 한다.
  • 그냥 폴더 분리만 하면 되는게 아니었다 ㅜ 이걸 이해 못해서 시간이 한참 걸렸다 ㅠ_ㅠ



step2. View Model 구성

view_model 파일은 한 뎁스 안에 위치해 있어 위의 MVVM 예시 사진에는 나와있지 않은데 실제 위치는 ui 폴더 안이다. view_model은 페이지별로 존재해야 하기 때문인데 이번엔 main_screen에 대한 view_model 하나만 구성했다.

class StoreModel with ChangeNotifier {
  var isLoading = false;
  List<Store> stores = [];
  final _storeRepository = StoreRepository();
 
  StoreModel() {
    fetch2();
  }
  //initState에서 처음 로딩될 때 fetch를 실행시켜야 하는데
  //view엔 기능을 담을 수 없다보니 여기서 함수를 실행해서 최초 앱이 실행될때 데이터가 나오게 함

Future fetch2() async {
    isLoading = true;
    notifyListeners();

    stores = await _storeRepository.fetch();
  isLoading = false;
  notifyListeners(); 

1) view_model의 탄생

  • view_model 폴더가 생겼으니!! 앞으로 UI를 담고 있는 main_screen은 view_model을 거쳐야만 repository가 담고 있는 기능을 받을 수 있다. (main_screen 직접 접근할 수 없게 private으로 숨기기까지 했다)
  • 즉 repository -> view_model -> view (main_screen)이 되는 것이다.
  • 따라서 StoreRepository의 인스턴스를 생성했으며 StoreRepository의 fetch함수를 fetch2 함수를 통해 불러오는 것을 확인할 수 있다.
  • 추가로 isLoading 이라는 함수도 UI와 분리를 위해 view_model로 이동하였다.

2) provider의 사용

  • setState를 사용하지 않기 위해 ChangeNotifier를 상속 받았고, 변경점마다 notifyListeners를 실행하여 변경점이 생긴걸 통지하게 했다.
  • 함수가 끝난 후에 통지가 되야 하니 await으로 기다리게 했다.
  • 통지는 ChangeNotifier가 위젯트리 꼭대기에서 받을 것이다. (꼭대기 = main/아래 코드 참고!) 통지를 받아서 변경이 필요한 곳에 알려줄 수 있게 된다.
void main() {
  runApp(
      ChangeNotifierProvider.value(value: StoreModel(), 
          child: MyApp(),
      ),
  );
}


step3. main_screen 간소화

이제 우리의 main_screen은 오직 UI만 담고 있고, 기능은 모두 참조해서 가져다 쓰기만 하면 된다. 즉 메인이 아주 아주 간소해진 다는 것이다.

class MainPage extends StatelessWidget{
  
  @override
  Widget build(BuildContext context) {
    final storeModel = Provider.of<StoreModel>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(
            '마스크 재고 있는 곳 : ${storeModel.stores.length}곳'),
        actions: <Widget>[
          IconButton(
              onPressed: () {
                storeModel.fetch2();
              },
              icon: const Icon(Icons.refresh)),
        ],
      ),
      body: storeModel.isLoading
          ? const Center(child: CircularProgressIndicator())
          : ListView(
        children: storeModel.stores
           .map((e) => ListTile(
          title: Text(e.name ?? ''),
          subtitle: Text(e.addr ?? ''),
          trailing: RemainStatWidget(e), //e: Store 객체
        )) .toList(),
      ),
    );
  }
}

1) stateful -> stateless 로 변경

  • 더이상 기능, 변수가 없기 때문에 stateful이 아닌 stateless로 변경된다.
  • 따라서 Inintstate도 빠지게 된다.
  • 기능, 변수를 담은 코드가 싹 다 빠졌다.

2) 이제 레퍼지토리가 아닌 뷰모델에 접근한다.

  • 이제는 StoreModel에 접근하여 UI에 데이터를 뿌려준다.

3) provider 사용으로 setState가 모두 빠진다.

  • setState로 전체를 빌드하는 것이 아닌, 변경이 된 부분만 빌드 된다.
  • final storeModel = Provider.of(context);
    이 코드를 통해 제일 꼭대기에서 제공하는 storeModel의 인스턴스를 쓸 수 있게된다.

0개의 댓글