Flutter MVVM 패턴으로 코드 구조 최적화하기

Flutter 앱 개발에서 코드의 가독성과 유지보수성은 매우 중요한 요소입니다. MVVM 패턴은 이러한 문제를 해결하는 방법으로, Model-View-ViewModel의 약자입니다. 이번 글에서는 앱 개발 시 MVVM 패턴을 적용하여 코드의 구조를 어떻게 개선하고 Model, View, ViewModel로 코드를 분리함으로써 얻을 수 있는 이점과 구체적인 구현 방법을 정리하였습니다.

MVVM이란?

MVVM은 Model-View-ViewModel의 약자로, 소프트웨어 개발에서 사용되는 아키텍처 패턴입니다. 이 패턴은 주로 사용자 인터페이스(UI) 기반 응용 프로그램 개발에 적용되며, UI와 비즈니스 로직을 분리하여 코드의 관리와 유지보수를 용이하게 만드는 데 목적이 있습니다. MVVM 패턴은 애플리케이션을 세 가지 주요 구성 요소로 나눕니다.

MVVM-Model-View-ViewModel

Model

애플리케이션의 데이터와 비즈니스 로직을 담당합니다. 모델은 데이터의 저장, 검색, 수정 등을 처리하며, 데이터베이스, 웹 서비스 호출 등 백엔드와의 통신을 담당할 수도 있습니다.

View

사용자에게 보이는 UI 부분입니다. 뷰는 사용자가 볼 수 있는 요소(버튼, 텍스트 박스, 레이아웃 등)를 표시하고, 사용자의 입력을 받습니다. 뷰는 ViewModel을 통해 사용자 인터페이스의 상태를 업데이트하며, 사용자의 액션에 반응합니다.

ViewModel

뷰와 모델 사이의 연결 고리 역할을 합니다. ViewModel은 뷰가 필요로 하는 데이터를 모델로부터 가져와서, 뷰가 사용하기 쉬운 형태로 가공합니다. 또한, 사용자의 액션에 대한 로직을 처리하며, 데이터 바인딩을 통해 UI의 자동 업데이트를 지원합니다. ViewModel은 모델에 대한 참조는 가지고 있지만, 뷰에 대한 직접적인 참조를 가지지 않아 뷰와의 의존성이 낮습니다.

장점

  • 유지보수성 향상: UI 코드와 비즈니스 로직이 분리되어 있어, 변경 사항이 한 부분에만 국한되어 다른 부분에 영향을 미치지 않습니다.
  • 테스트 용이성: 비즈니스 로직이 뷰로부터 분리되어 있어, UI 없이도 로직의 테스트가 가능합니다.
  • 재사용성 및 확장성: UI 컴포넌트와 비즈니스 로직이 잘 분리되어 있어, 재사용성과 확장성이 좋습니다.
  • 데이터 바인딩: ViewModel과 뷰 사이의 데이터 바인딩을 통해, UI 요소와 데이터의 자동 동기화를 지원합니다. 이는 코드의 양을 줄이고, 더 깔끔한 코드 구조를 가능하게 합니다.

구조화

이전에 작성했던 HTTP 사용 예제를 이용해서 MVVM 패턴을 적용한 프로젝트 구조를 만들어 보겠습니다. 코드를 이해하기 위해서 먼저 참고부탁드립니다.

models, view_models, views, services 폴더로 나뉘며, 각각의 역할에 맞게 코드를 분리합니다.

flutter-mvvm-폴더구조

Model (models/post.dart)

모델은 데이터의 구조를 정의합니다. 이 경우 Post 클래스는 게시물 데이터를 나타냅니다. fromJson 팩토리 메서드를 사용하여 JSON으로부터 Post 객체를 생성할 수 있습니다.

// Post 데이터 모델 정의
class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  // JSON 객체로부터 Post 인스턴스를 생성하는 팩토리 메서드
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }
}

Service (services/http_service.dart)

서비스는 네트워크 요청을 처리합니다. HttpService 클래스는 API로부터 게시물 데이터를 비동기적으로 가져오는 메서드 getPosts를 정의합니다.

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/post.dart';

// HTTP 요청을 관리하는 서비스 클래스
class HttpService {
  final String postsURL = "https://jsonplaceholder.typicode.com/posts";

  // 게시물 데이터를 가져오는 메서드
  Future<List<Post>> getPosts() async {
    final response = await http.get(Uri.parse(postsURL));
    if (response.statusCode == 200) {
      final List<dynamic> postJson = json.decode(response.body);
      return postJson.map((json) => Post.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
}

ViewModel (view_models/post_view_model.dart)

ViewModel은 ChangeNotifier를 상속받아, 데이터 변경을 알릴 수 있습니다. 이는 View가 데이터 변경을 감지하고 UI를 업데이트할 수 있게 합니다.

import 'package:flutter/foundation.dart';
import '../models/post.dart';
import '../services/http_service.dart';

// 게시물 데이터를 관리하는 ViewModel
class PostViewModel extends ChangeNotifier {
  final HttpService _httpService = HttpService();
  List<Post> _posts = [];

  List<Post> get posts => _posts;

  // 게시물 데이터를 비동기적으로 가져오는 메서드
  Future<void> fetchPosts() async {
    _posts = await _httpService.getPosts();
    notifyListeners(); // 데이터 변경 알림
  }
}

View (views/screens/home_page.dart)

View는 UI를 구성합니다. AnimatedBuilder를 사용하여 ViewModel의 상태 변화에 따라 UI를 업데이트합니다.

import 'package:flutter/material.dart';
import '../../view_models/post_view_model.dart';
import '../widgets/post_list_tile.dart';

// 앱의 홈 화면을 구성하는 StatefulWidget
class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final PostViewModel _viewModel = PostViewModel();

  @override
  void initState() {
    super.initState();
    _viewModel.fetchPosts(); // ViewModel을 통해 데이터 로드
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("HTTP Demo without Provider")),
      body: AnimatedBuilder(
        animation: _viewModel,
        builder: (context, _) {
          return ListView.builder(
            itemCount: _viewModel.posts.length,
            itemBuilder: (context, index) {
              return PostListTile(post: _viewModel.posts[index]); // 게시물 데이터를 표시하는 위젯
            },
          );
        },
      ),
    );
  }
}

재사용 가능한 커스텀 위젯을 widgets폴더에 만듭니다.

import 'package:flutter/material.dart';
import '../../models/post.dart';

class PostListTile extends StatelessWidget {
  final Post post;

  const PostListTile({Key? key, required this.post}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(post.title),
      subtitle: Text("ID: ${post.id}"),
    );
  }
}

Main Entry (main.dart)

앱의 진입점입니다. MaterialApp을 통해 앱이 시작됩니다.

import 'package:flutter/material.dart';
import 'views/screens/home_page.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MVVM Demo',
      home: HomePage(), // 홈 페이지 설정
    );
  }
}

마무리

MVVM 패턴은 특히 WPF(Windows Presentation Foundation), Silverlight, 그리고 최근에는 웹 애플리케이션 및 모바일 애플리케이션 개발에도 널리 사용되고 있습니다. Flutter에서도 MVVM 패턴을 적용하여 크로스 플랫폼 애플리케이션 개발의 이점을 최대화하면서 코드의 가독성과 재사용성을 보장할 수 있습니다. 이는 단순히 생산성을 높이는 것을 넘어서, 애플리케이션의 지속 가능한 성장과 확장을 위한 견고한 기반을 마련합니다. 읽어주셔서 감사합니다.

전체 코드는 링크에서 확인할 수 있습니다.

Leave a Comment