본문 바로가기

MAUI MVVM Command가 두 번 실행되는 문제 해결, 이벤트 버블링부터 GestureRecognizer까지 구조적 분석

@veedeeo 2025. 12. 11. 16:31

1. 서론 — MAUI에서 Command가 두 번 실행되는 문제, 단순한 버그가 아니다

MAUI에서 Button, ImageButton, CollectionView의 ItemTapped, 또는 TapGestureRecognizer를 사용할 때 다음과 같은 현상이 보고된다.

  • Command가 한 번 눌렀는데 두 번 실행된다
  • TapGestureRecognizer가 연속해서 두 번 호출된다
  • Button Command와 TapGestureRecognizer가 동시에 호출된다
  • ItemSelected와 TapGestureRecognizer가 중복 호출된다
  • Navigation도 두 번 실행돼 화면이 두 번 push되는 현상이 나타난다

이 문제는 단순한 실수가 아니라 MAUI 이벤트 구조의 특성 때문이다.

MAUI는 이벤트가 Native → Handler → VirtualView → BindingContext(Command)로 전달되며, 특히 GestureRecognizer의 버블링(Bubbling), 터치 중복 처리, ItemTapped와 TapGestureRecognizer 간 충돌이 주요 원인이 된다.

이 글에서는 Command가 두 번 실행되는 구조적 원인을 깊이 있게 분석하고, 실전 프로젝트에서 문제를 예방하고 해결하는 전략을 정리한다.

 

MVVM Command Double-Execution Diagram


2. Command가 두 번 실행되는 핵심 원인 분석

원인 1: Button 클릭 + TapGestureRecognizer가 동시에 작동

예를 들어 다음과 같은 구조:

 
<StackLayout>
    <StackLayout.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding TapCommand}" />
    </StackLayout.GestureRecognizers>

    <Button Text="OK" Command="{Binding TapCommand}" />
</StackLayout>
 
 

Button을 눌렀을 때:

  • Button.Command 실행
  • 부모 StackLayout의 TapGestureRecognizer 실행

따라서 Command가 2번 호출된다.


원인 2: CollectionView의 SelectionChanged와 TapGestureRecognizer 중복 호출

많이 발생하는 패턴:

 
<CollectionView ItemsSource="{Binding Items}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding ItemClickCommand}" />
                </Grid.GestureRecognizers>
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
 
 

문제는 CollectionView가 기본적으로 다음 이벤트를 가지고 있다는 점이다.

  • SelectionChanged
  • ItemTapped(플랫폼별)
  • TapGestureRecognizer

즉, 하나의 터치가 다음 같은 흐름으로 전달된다.

  1. 셀 선택 (SelectionChanged)
  2. 셀 탭 이벤트 (ItemTapped)
  3. GestureRecognizer 호출

그 결과 Command가 2~3번씩 실행될 수 있다.


원인 3: TapGestureRecognizer가 "Pressed"와 "Released"를 모두 감지하는 구조

MAUI의 TapGestureRecognizer는 단순한 “탭”이 아니라
Native Touch 이벤트 두 개를 감지한다.

  • Touch Down
  • Touch Up

일부 플랫폼에서는 이게 두 번 전달되며,
MAUI가 이를 완전히 통합하지 못해 Command가 중복 호출되기도 한다.


원인 4: Button과 GestureRecognizer의 터치 버블링(Bubbling)

MAUI 이벤트는 다음 방향으로 전달된다.

자식 → 부모 → Shell (Root)

즉, Button을 눌러도 부모 레이아웃의 GestureRecognizer가 호출되는 구조다.

이벤트를 “막지” 않으면 Command는 여러 번 동작하게 된다.


원인 5: CommandParameter null 비교나 Task 실행 지연으로 인한 중복 처리

Command 내부 로직이 다음과 같을 경우:

if (!_isRunning)
{
    _isRunning = true;
    await Task.Delay(10);
    _isRunning = false;
}
 
 

UI 스레드 이벤트가 두 번 발생할 경우
딜레이 때문에 두 번 진입하는 상황도 발생한다.


3. 실전에서 실제로 발생하는 중복 호출 문제 사례

❌ 사례 1 — CollectionView 셀 클릭 시 Navigation이 두 번 실행됨

원인: SelectionChanged + TapGestureRecognizer 충돌

❌ 사례 2 — Button을 눌렀는데 상위 StackLayout TapGestureRecognizer도 작동

원인: 터치 버블링

❌ 사례 3 — 동일 Command가 Pressed/Released에 의해 2회 호출

원인: GestureRecognizer 처리 방식

❌ 사례 4 — Custom Control 내부에서 Button과 TapRecognizer를 함께 사용

원인: NativeHandler 중복 전달

 

MVVM Command Double-Execution Diagram


4. MAUI에서 Command 중복 실행을 방지하는 정석 해결 전략

여기서는 실무에서 가장 안정적으로 검증된 방법 5가지를 제시한다.


✔ 해결 전략 1: Button 내부에는 GestureRecognizer를 절대 섞지 않는다

가장 중요한 원칙:

Button 클릭은 Button만 담당하게 하라.

GestureRecognizer는

  • 이미지
  • 카드
  • 레이아웃 전체
    같은 “광역 클릭 UI”에만 사용해야 한다.

✔ 해결 전략 2: CollectionView에서는 TapGestureRecognizer 대신 SelectionChanged를 사용

 
<CollectionView
    SelectionMode="Single"
    SelectionChanged="OnItemSelected">
 
 

TapGestureRecognizer를 쓰면:

  • Pressed
  • Released
  • Tapped
  • Native Tap

등이 모두 섞여 중복 실행 위험이 높아진다.

SelectionChanged가 가장 안정적인 방식이다.


✔ 해결 전략 3: GestureRecognizer에서 터치 이벤트 버블링을 막기

MAUI는 이벤트 취소(Handled)를 기본 제공하지 않지만
이 패턴으로 간접적으로 해결할 수 있다.

 
bool _tapped = false;

void OnTapped(object sender, TappedEventArgs e)
{
    if (_tapped) return;

    _tapped = true;
    Task.Delay(100).ContinueWith(_ => _tapped = false);

    // Command 실행
}
 
 

이 방식으로 중복 탭을 100ms 안에 차단할 수 있다.


✔ 해결 전략 4: Command 중복 실행 방지용 “Busy 플래그 패턴”

ViewModel:

 
private bool _isBusy;

public ICommand MyCommand => new Command(async () =>
{
    if (_isBusy) return;
    _isBusy = true;

    try
    {
        await DoWork();
    }
    finally
    {
        _isBusy = false;
    }
});
 
 

이 패턴은 MAUI에서 매우 널리 사용되는 정석이다.


✔ 해결 전략 5: Navigation 중복 Push 방지

 
if (Shell.Current.Navigation.NavigationStack.LastOrDefault()?.GetType() == typeof(DetailPage))
    return;

await Shell.Current.GoToAsync(nameof(DetailPage));
 
 

Navigation 중복 발생을 완벽히 방지한다.


5. 문제 해결 후의 아키텍처 설계 원칙

Command 중복 실행을 막기 위해서는 다음 구조적 설계가 필요하다.

  • Button 내부에는 GestureRecognizer를 사용하지 않는다
  • CollectionView는 SelectionChanged 중심 구조로 재설계한다
  • GestureRecognizer는 레이아웃 수준에서만 사용한다
  • Busy 플래그로 Command 재진입을 방지한다
  • Navigation 흐름에서 중복 Push를 차단한다

이 규칙만 지켜도 중복 호출 문제의 90% 이상은 해결된다.


마무리 — 이벤트 흐름을 이해하면 Command 구조가 완전히 안정된다

MAUI에서 Command가 두 번 실행되는 문제는
단순 이벤트 버그가 아니라:

  • Native Touch 이벤트
  • GestureRecognizer 처리 방식
  • 이벤트 버블링
  • SelectionChanged와 Tap 충돌
  • Navigation 중복 처리

이 복합적으로 얽혀 있는 구조적인 문제다.

이 글에서 소개한 해결 전략을 적용하면
중복 Command 실행 문제를 근본적으로 해결할 수 있고
UI 안정성이 크게 향상된다.

veedeeo
@veedeeo :: .net MAUI·Netlify·SEO·웹 최적화 기술 블로그

.net MAUI·Netlify·SEO·웹 최적화 기술 블로그

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차