subindev 개발 블로그

[⚠️ERROR] MVVM 패턴 - ViewModel 인스턴스 다중 생성으로 인한 에러 본문

.NET/Xamarin

[⚠️ERROR] MVVM 패턴 - ViewModel 인스턴스 다중 생성으로 인한 에러

subindev 2025. 12. 11. 21:08

📝 1. 상황 설명

현재 Xamarin Forms 기반의 Calendar Todo 앱을 개발하고 있다. 상황을 아주 단순하게 정리하면 다음과 같다.

 

  • Page1 - Todo 리스트 메인 페이지
    • 여기서는 Update, Delete, Load 모두 정상적으로 화면에 반영된다.
  • Page2 (Modal) - Todo등록 페이지
    • 여기서 **추가(Insert)**를 하면 Page1 - Page2(모달) - Page1
    • 서버(DB)에는 정상적으로 저장되는데
    • Page1 화면에는 새 Todo가 바로 보이지 않는다.
  • 공통점/차이점
    • 둘 다 SimplePageViewModel 를 사용한다고 생각했음.
    • 하지만 실제로는 서로 다른 ViewModel 인스턴스를 쓰고 있어서 문제가 발생했다.

 

2. 구조 이해: Page / ViewModel 관계 🧱

 

2-1. Page1 – SimplePage.xaml

<ContentPage
    x:Class="MOBILE_TEST.Views.SimplePage"
    ...
    x:Name="simpleCalendarPage">

    <ContentPage.BindingContext>
        <!-- 여기서 ViewModel을 new 해서 사용 -->
        <local:SimplePageViewModel />
    </ContentPage.BindingContext>

    <!-- Calendar -->
    <plugin:Calendar
        Events="{Binding Events}"
        ...>

        <!-- EventTemplate 내부 -->
        <plugin:Calendar.EventTemplate>
            <DataTemplate>
                <Grid>
                    <SwipeView>
                        <Grid>
                            <Grid.GestureRecognizers>
                                <!-- 체크 토글 -->
                                <TapGestureRecognizer
                                    Command="{Binding BindingContext.IsDoneCommand, Source={x:Reference simpleCalendarPage}}"
                                    CommandParameter="{Binding .}" />
                            </Grid.GestureRecognizers>

                            <!-- 스와이프 삭제 -->
                            <SwipeView.RightItems>
                                <SwipeItems Mode="Reveal">
                                    <SwipeItem
                                        Command="{Binding BindingContext.DeleteCommand, Source={x:Reference simpleCalendarPage}}"
                                        CommandParameter="{Binding .}" />
                                </SwipeItems>
                            </SwipeView.RightItems>
                        </Grid>
                    </SwipeView>
                </Grid>
            </DataTemplate>
        </plugin:Calendar.EventTemplate>
    </plugin:Calendar>
</ContentPage>

  • BindingContext 에서 new SimplePageViewModel() 이 실행된다.
  • 따라서 Page1이 사용하는 ViewModel 인스턴스를 A라고 부르자.
  • Calendar에 바인딩된 Events, IsDoneCommand, DeleteCommand 는 모두 A 인스턴스를 보고 있다.

 

3. Page1 내부에서만 동작할 때는 왜 정상일까? ✅

LoadTodosFromServer, UpdateTodoIsDone, DeleteTodo 는 전부 ViewModel A에서 실행된다.

private async Task LoadTodosFromServer()
{
    var userId = Session.CurrentUser.QM05IPID;
    var list = await _service.GetTodoList(userId);

    if (list == null || list.Count == 0)
        return;

    var todoList = list.Select(s => new TodoModel
    {
        ID = s.ID,
        Content = s.TODO_CONTENT,
        Description = s.DESCRIPTION,
        Startdate = s.START_DATE,
        Category = s.CATEGORY,
        Priority = s.PRIORITY,
        IsDone = s.ISDONE.ToString(),
        CreatedAt = s.CREATEDAT,
        WriterId = s.WRITER_ID
    }).ToList();

    foreach (var todo in todoList)
    {
        if (DateTime.TryParse(todo.Startdate, out var date))
        {
            if (!Events.ContainsKey(date))
                Events[date] = new ObservableCollection<TodoModel>();

            ((ObservableCollection<TodoModel>)Events[date]).Add(todo);
        }
    }

    // 필요하다면 여기서 OnPropertyChanged(nameof(Events)) 호출
}

  • Events 는 ViewModel A 의 프로퍼티.
  • Calendar 역시 ViewModel A의 Events를 바인딩하고 있다.
  • 따라서
    • Load 시 → A.Events 수정 → Calendar 반영
    • Delete/Update 시 → A.Events 수정 → Calendar 반영

이 구간은 설계 상 문제가 없다.


 

4. Modal을 거쳐서 삽입할 때만 화면에 안 보이는 이유 ⚠️

문제는 **추가(Add)**를 할 때 발생한다.

4-1. AddTodoModalPage 초기 코드 (문제 버전)

public partial class AddTodoModalPage : ContentPage
{
    private readonly SimplePageViewModel _svm;

    public AddTodoModalPage(DateTime selectedDate)
    {
        InitializeComponent();

        // 문제 포인트: 여기서 App.SimpleVM 같은 다른 인스턴스를 사용
        _svm = App.SimpleVM;

        datePickerStart.Date = selectedDate;
        ...
    }

    private async void OnSaveClicked(object sender, EventArgs e)
    {
        TodoModel todo = new TodoModel
        {
            Content = entryTask.Text,
            WriterId = Session.CurrentUser.QM05IPID,
            Description = editorDescription.Text,
            Startdate = datePickerStart.Date.ToString("yyyy/MM/dd"),
            Category = labelCategory.Text,
            Priority = _priority,
            IsDone = ...
        };

        try
        {
            _svm.AddCommand.Execute(todo);  // 여기서 사용하는 _svm은 ViewModel B 인스턴스
            await Navigation.PopModalAsync();
        }
        catch (Exception ex)
        {
            ...
        }
    }
}

여기서 중요한 사실:

  • Page1: XAML에서 만든 SimplePageViewModel → 인스턴스 A
  • Page2: App.SimpleVM (또는 다른 static property에서 만들어 둔) → 인스턴스 B

그 결과:

  • 모달에서 AddTodo 를 호출하면 B.Events가 수정된다.
  • 하지만 Calendar는 A.Events만 보고 있다.
  • 그래서:
    • DB Insert는 성공 ✅
    • 화면에는 새 일정이 안 보이는 것처럼 보임

 

5. 근본 원인: “같은 타입(ViewModel)”이 아니라 “같은 인스턴스”가 중요하다 💡

  • SimplePageViewModel 라는 타입은 같다.
  • 하지만 new를 두 번 하면 완전히 다른 객체(인스턴스)가 생긴다.
  • UI 바인딩은 “어떤 타입이냐”가 아니라 **“어떤 객체를 BindingContext로 쓰고 있냐”**에 의해 결정된다.

구분 ViewModel 인스턴스 동작

Page1 화면 A Calendar/Events 바인딩
Page2 모달 B AddTodo 실행 시 B.Events 변경
결과 A != B 화면에 반영 X

 

결론 : OnPropertyChanged, ObservableCollection 문제가 아니라

완전히 다른 ViewModel 인스턴스를 건드리고 있었기 때문에 생긴 버그다.


6. 해결 방법: Modal 생성 시 ViewModel 인스턴스를 같이 넘기기 🛠

해결 전략은 간단하다.

Page1에서 사용 중인 ViewModel 인스턴스(A)를 Modal(Page2)에 그대로 넘겨서, 같은 인스턴스를 공유하도록 만들기

 

6-1. Modal 생성자 수정 – ViewModel 주입

public partial class AddTodoModalPage : ContentPage
{
    private readonly SimplePageViewModel _svm;

    // 🔑 SimplePageViewModel 인스턴스를 외부에서 받도록 변경
    public AddTodoModalPage(DateTime selectedDate, SimplePageViewModel svm)
    {
        InitializeComponent();

        _svm = svm;   // 여기서 App.SimpleVM 을 만들지 않고, 넘겨받은 인스턴스를 사용

        datePickerStart.Date = selectedDate;
        labelStartDate.Text = selectedDate.ToString("yy년 M월 dd일 (ddd)",
            new CultureInfo("ko-KR"));

        ...
    }
}

 

6-2. SimplePageViewModel.OpenAddModal 수정 – this 전달

public Command OpenAddModalCommand { get; }

public SimplePageViewModel() : base()
{
    ...
    OpenAddModalCommand = new Command<DateTime>(OpenAddModal);
    ...
}

private async void OpenAddModal(DateTime _selectedDate)
{
    try
    {
        // 🔑 현재 ViewModel 인스턴스(this)를 Modal 로 함께 전달
        await Application.Current.MainPage.Navigation.PushModalAsync(
            new AddTodoModalPage(_selectedDate, this));
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.Message);
        await Application.Current.MainPage.DisplayAlert("에러발생", ex.Message, "OK");
    }
}

  • 이제 Modal은 Page1에서 사용 중인 같은 ViewModel 인스턴스 A를 참조한다.
  • 따라서 _svm.AddCommand.Execute(todo) 를 호출하면:
    • A.Events 에 Todo가 추가되고,
    • Calendar도 A.Events를 보고 있기 때문에 화면에 바로 반영된다.

 

6-3. OnSaveClicked는 그대로 사용 가능

private async void OnSaveClicked(object sender, EventArgs e)
{
    if (string.IsNullOrWhiteSpace(entryTask.Text))
    {
        await DisplayAlert("알림", "내용을 입력해주세요.", "확인");
        return;
    }

    TodoModel todo = new TodoModel
    {
        Content = entryTask.Text,
        WriterId = Session.CurrentUser.QM05IPID,
        Description = editorDescription.Text,
        Startdate = datePickerStart.Date.ToString("yyyy/MM/dd"),
        Category = labelCategory.Text,
        Priority = _priority,
        IsDone = _status == "NotStarted" ? "0" :
                 _status == "InProgress" ? "1" : "2"
    };

    try
    {
        await _svm.AddTodo(todo);      // Task 사용 시
        await Navigation.PopModalAsync();
    }
    catch (Exception ex)
    {
        Debug.WriteLine("Add 실패: " + ex.Message);
        await DisplayAlert("오류", "등록 중 문제가 발생했습니다.", "확인");
    }
}

이제 _svm 은 Page1과 같은 인스턴스이기 때문에, Add 후에 곧바로 Calendar에 반영된다.

 


Spring에서는 간편하게 의존성 주입을 받아서 사용하고 싱글톤 패턴으로 유지되어서 인스턴스를 생각할 필요가 없었다.
그것에 익숙해져서 인스턴스 관리를 소홀하게 하였고 이런 실수를 하게 되었다.
앞으로도 코드를 짤 때 조금더 구조적인것을 고민하면서 짜야겠다!