개발/Android

[Flutter] Riverpod 상태관리 - watch vs read 차이

y_lime 2025. 4. 2. 13:52

Riverpod watch vs read 차이와 UI 빌드 영향 분석

🔍 watch vs read 차이

watch (ref.watch)

  • ref.watch(provider)를 사용하면 해당 provider의 값이 변경될 때마다 UI가 자동으로 리빌드됨.
  • 주로 UI에 실시간으로 반영해야 하는 상태 값을 구독할 때 사용.

read (ref.read)

  • ref.read(provider)는 현재 상태 값만 한 번 읽고 끝.
  • 이후 provider 값이 변경되더라도 UI가 리빌드되지 않음.
  • 주로 이벤트 기반 로직 (ex. 버튼 클릭 시 상태 변경)에서 사용.

사용 방식 리빌드 여부 설명

ref.watch(provider) ✅ 리빌드됨 provider의 값이 변경될 때마다 UI가 다시 빌드됨. (자동 감지)
ref.read(provider) ❌ 리빌드 안됨 한 번만 읽고 이후 변경이 있어도 UI에 영향을 주지 않음.
ref.watch(provider.notifier) ❌ 리빌드 안됨 notifier 객체 자체는 변하지 않음. (상태 변경 감지 X)
ref.read(provider.notifier) ❌ 리빌드 안됨 notifier의 메서드를 실행하지만, UI 리빌드는 일어나지 않음.

🛠 예제 코드 (UI 빌드 확인용)

아래 코드를 실행하면 watch와 read의 차이를 명확하게 확인할 수 있습니다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// 상태 관리 (카운터)
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    state++;
  }

  void decrement() {
    state--;
  }
}

final counterProvider = StateNotifierProvider<CounterNotifier, int>(
  (ref) => CounterNotifier(),
);

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("🖥️ CounterScreen 전체가 빌드됨!");

    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Consumer(
              builder: (context, ref, child) {
                final count = ref.watch(counterProvider);
                print("🔄 Counter 값만 빌드됨! (Text 위젯)");
                return Text(
                  'Count: $count',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
            const SizedBox(height: 20),
            Consumer(
              builder: (context, ref, child) {
                print("🔄 버튼 UI가 빌드됨!");
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    IconButton(
                      onPressed: () {
                        print("➖ Decrement 버튼 클릭!");
                        ref.read(counterProvider.notifier).decrement(); // ✅ read 사용 (UI 리빌드 방지)
                      },
                      icon: const Icon(Icons.remove),
                    ),
                    IconButton(
                      onPressed: () {
                        print("➕ Increment 버튼 클릭!");
                        ref.watch(counterProvider.notifier).increment(); // ❌ 잘못된 watch 사용
                      },
                      icon: const Icon(Icons.add),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

실행 결과 및 분석

터미널 출력 결과를 보면 어떤 부분이 다시 빌드되는지 확인할 수 있습니다.

🖥️ CounterScreen 전체가 빌드됨!
🔄 Counter 값만 빌드됨! (Text 위젯)
🔄 버튼 UI가 빌드됨!

➕ Increment 버튼 클릭!
🔄 Counter 값만 빌드됨! (Text 위젯)

➖ Decrement 버튼 클릭!
🔄 Counter 값만 빌드됨! (Text 위젯)

👉 버튼 UI(Row)는 처음 한 번만 빌드되고 이후에는 변경되지 않음!

👉 오직 Text 위젯만 리빌드됨! (watch(counterProvider) 때문)


ref.watch(counterProvider.notifier)를 사용하면 안 되는 이유

  • counterProvider.notifier 자체는 변하지 않으므로 watch를 해도 UI가 리빌드되지 않음.
Consumer(
              builder: (context, ref, child) {
                final count = ref.watch(counterProvider.notifier);
                print("🔄 Counter 값만 빌드됨! (Text 위젯)");
                return Text(
                  'Count: $count',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
            const SizedBox(height: 20),
            Consumer(
              builder: (context, ref, child) {
                print("🔄 버튼 UI가 빌드됨!");
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    IconButton(
                      onPressed: () {
                        print("➖ Decrement 버튼 클릭!");
                        ref.read(counterProvider.notifier).decrement(); // ✅ read 사용 (UI 리빌드 방지)
                      },
                      icon: const Icon(Icons.remove),
                    ),
                    IconButton(
                      onPressed: () {
                        print("➕ Increment 버튼 클릭!");
                        ref.watch(counterProvider.notifier).increment(); // ❌ 잘못된 watch 사용
                      },
                      icon: const Icon(Icons.add),
                    ),
                  ],
                );
              },
            ),

위와 같은 코드로 실행을 하면, 아래와 같이 버튼 클릭한 것만 인식을 하고, 그 후 다시 빌드하지 않아 UI 변경은 없다.

CounterScreen 전체가 빌드됨!
🔄 Counter 값만 빌드됨! (Text 위젯)
🔄 버튼 UI가 빌드됨!

➕ Increment 버튼 클릭!

➖ Decrement 버튼 클릭!

때문에 Text("Count : $count)를 실제 화면에서 보면 아래와 같은 화면이 나타난다.

 

  • watch할 때는 counterProvider 자체를 watch해야 한다 !

올바른 코드 수정 방법

 Consumer(
              builder: (context, ref, child) {
                final count = ref.watch(counterProvider);  // ⭕ 올바른 사용
                return Text(
                  'Count: $count',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
.
.
.


IconButton(
  onPressed: () {
    print("➕ Increment 버튼 클릭!");
    ref.read(counterProvider.notifier).increment(); // ⭕ 올바른 사용
  },
  icon: const Icon(Icons.add),
),