🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

ConsumerWidget과 Consumer 사용법 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 30. · 12 Views

ConsumerWidget과 Consumer 사용법 완벽 가이드

Flutter에서 Riverpod을 사용할 때 필수적인 ConsumerWidget과 Consumer 위젯의 사용법을 알아봅니다. 상태 관리의 핵심인 WidgetRef 활용법부터 성능 최적화까지 실무 예제와 함께 설명합니다.


목차

  1. ConsumerWidget이란?
  2. WidgetRef_사용하기
  3. Consumer_위젯으로_부분_리빌드
  4. ConsumerStatefulWidget
  5. StatefulWidget에서_Riverpod_사용
  6. 성능_최적화_팁

1. ConsumerWidget이란?

어느 날 김개발 씨가 Flutter 프로젝트에서 상태 관리를 구현하다가 고민에 빠졌습니다. "StatelessWidget에서 Provider 값을 어떻게 읽어오지?" 검색을 해보니 ConsumerWidget이라는 것이 눈에 들어왔습니다.

과연 이것은 무엇일까요?

ConsumerWidget은 Riverpod에서 Provider의 상태를 읽을 수 있도록 해주는 특별한 위젯입니다. 마치 카페에서 주문을 받는 바리스타처럼, Provider가 제공하는 데이터를 위젯에게 전달해주는 역할을 합니다.

StatelessWidget 대신 ConsumerWidget을 상속받으면 Provider의 값을 손쉽게 사용할 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 간단한 카운터 Provider 정의
final counterProvider = StateProvider<int>((ref) => 0);

// ConsumerWidget을 상속받아 위젯 생성
class CounterScreen extends ConsumerWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch로 Provider 값을 구독
    final count = ref.watch(counterProvider);

    return Text('현재 카운트: $count');
  }
}

김개발 씨는 입사 2개월 차 주니어 개발자입니다. 오늘은 처음으로 Riverpod을 사용해 상태 관리를 구현해야 하는 날입니다.

기존에 알고 있던 StatelessWidget으로 화면을 만들었는데, Provider의 값을 어떻게 가져와야 할지 막막했습니다. 선배 개발자 박시니어 씨가 다가와 화면을 살펴봅니다.

"김개발 씨, Riverpod을 사용하려면 StatelessWidget 대신 ConsumerWidget을 써야 해요." 그렇다면 ConsumerWidget이란 정확히 무엇일까요? 쉽게 비유하자면, ConsumerWidget은 마치 카페의 바리스타와 같습니다.

손님이 음료를 주문하면 바리스타가 음료를 만들어 전달해주듯이, ConsumerWidget은 Provider가 가진 데이터를 위젯에게 전달해주는 역할을 합니다. 바리스타 없이는 손님이 직접 커피 머신을 다뤄야 하듯이, ConsumerWidget 없이는 Provider 데이터에 접근하기 어렵습니다.

ConsumerWidget이 없던 시절에는 어땠을까요? 개발자들은 Provider 패턴이나 InheritedWidget을 사용해 직접 상태를 전달해야 했습니다.

코드가 복잡해지고 보일러플레이트가 늘어났습니다. 더 큰 문제는 위젯 트리가 깊어질수록 상태 전달이 점점 어려워진다는 점이었습니다.

바로 이런 문제를 해결하기 위해 Riverpod과 ConsumerWidget이 등장했습니다. ConsumerWidget을 사용하면 어디서든 Provider 값에 쉽게 접근할 수 있습니다.

또한 Provider 값이 변경되면 자동으로 위젯이 리빌드됩니다. 무엇보다 코드가 간결해지고 가독성이 좋아진다는 큰 장점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 counterProvider를 정의합니다.

StateProvider는 간단한 상태를 관리할 때 사용하며, 초기값으로 0을 설정했습니다. 다음으로 ConsumerWidget을 상속받는 CounterScreen 클래스를 만듭니다.

일반 StatelessWidget과 다른 점은 build 메서드에 WidgetRef ref 매개변수가 추가된다는 것입니다. build 메서드 내부에서 **ref.watch(counterProvider)**를 호출하면 Provider의 현재 값을 가져올 수 있습니다.

watch를 사용했기 때문에 값이 변경될 때마다 위젯이 자동으로 리빌드됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다. 장바구니에 담긴 상품 개수를 화면에 표시해야 할 때, ConsumerWidget을 사용하면 장바구니 Provider의 값을 쉽게 읽어와 화면에 보여줄 수 있습니다.

상품을 추가하거나 삭제하면 자동으로 화면이 업데이트됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 StatelessWidget을 상속받고 ref를 사용하려고 시도하는 것입니다. 이렇게 하면 ref를 찾을 수 없다는 컴파일 에러가 발생합니다.

반드시 ConsumerWidget을 상속받아야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, StatelessWidget 대신 ConsumerWidget을 쓰면 되는군요!" ConsumerWidget을 제대로 이해하면 Riverpod 상태 관리의 첫 단추를 올바르게 끼울 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - ConsumerWidget은 StatelessWidget의 Riverpod 버전이라고 생각하면 쉽습니다

  • build 메서드의 시그니처가 다르다는 점을 기억하세요: Widget build(BuildContext context, WidgetRef ref)
  • Provider 값이 변경될 때마다 전체 build가 다시 호출됩니다

2. WidgetRef 사용하기

김개발 씨가 ConsumerWidget을 사용해 화면을 만들었습니다. 그런데 build 메서드에 있는 WidgetRef ref가 무엇인지 정확히 이해가 되지 않았습니다.

"이 ref로 뭘 할 수 있는 거지?" 궁금증이 생겼습니다.

WidgetRef는 Provider와 위젯을 연결해주는 핵심 객체입니다. 마치 리모컨처럼, WidgetRef를 통해 Provider의 값을 읽고, 변경을 감지하고, 상태를 업데이트할 수 있습니다.

ref.watch, ref.read, ref.listen 등 다양한 메서드를 제공하여 상황에 맞게 Provider를 활용할 수 있습니다.

다음 코드를 살펴봅시다.

class CartScreen extends ConsumerWidget {
  const CartScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch: 값을 구독하고 변경 시 리빌드
    final cartItems = ref.watch(cartProvider);

    // listen: 값 변경 시 콜백 실행 (side effect용)
    ref.listen(cartProvider, (previous, next) {
      if (next.isEmpty) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('장바구니가 비었습니다')),
        );
      }
    });

    return ElevatedButton(
      // read: 일회성 읽기 (콜백 내에서 사용)
      onPressed: () => ref.read(cartProvider.notifier).clear(),
      child: Text('전체 삭제 (${cartItems.length}개)'),
    );
  }
}

김개발 씨는 ConsumerWidget을 만들었지만, build 메서드에 낯선 매개변수가 하나 있었습니다. 바로 WidgetRef ref입니다.

이게 대체 무엇이고, 어떻게 사용해야 하는 걸까요? 박시니어 씨에게 질문했습니다.

"선배님, 이 ref가 정확히 뭔가요? watch랑 read랑 뭐가 다른 건지 헷갈려요." WidgetRef란 정확히 무엇일까요?

쉽게 비유하자면, WidgetRef는 마치 TV 리모컨과 같습니다. 리모컨 하나로 채널을 바꾸고, 볼륨을 조절하고, 전원을 끌 수 있듯이, WidgetRef 하나로 Provider의 값을 읽고, 변경을 감지하고, 상태를 업데이트할 수 있습니다.

리모컨 없이는 TV 앞까지 가서 버튼을 눌러야 하듯이, WidgetRef 없이는 Provider에 접근하기 어렵습니다. WidgetRef가 제공하는 핵심 메서드는 세 가지입니다.

첫 번째는 ref.watch입니다. 이것은 Provider의 값을 구독하고, 값이 변경될 때마다 위젯을 리빌드합니다.

build 메서드 내에서 UI에 표시할 데이터를 가져올 때 사용합니다. 마치 TV에서 특정 채널을 시청하는 것처럼, 해당 Provider의 변화를 계속 지켜보는 것입니다.

두 번째는 ref.read입니다. 이것은 Provider의 현재 값을 일회성으로 읽어옵니다.

버튼 클릭 같은 콜백 함수 내에서 사용합니다. 채널을 잠깐 확인만 하고 다른 채널로 돌아가는 것과 비슷합니다.

값이 변경되어도 리빌드가 일어나지 않습니다. 세 번째는 ref.listen입니다.

이것은 Provider 값의 변경을 감지하여 특정 동작을 실행합니다. 스낵바 표시나 네비게이션 같은 사이드 이펙트에 사용합니다.

특정 채널이 방송 시작할 때 알림을 받는 것과 같습니다. 위의 코드를 자세히 살펴보겠습니다.

먼저 **ref.watch(cartProvider)**로 장바구니 아이템 목록을 가져옵니다. 이 값은 장바구니가 변경될 때마다 자동으로 업데이트됩니다.

다음으로 ref.listen을 사용해 장바구니가 비었을 때 스낵바를 표시합니다. 마지막으로 버튼의 onPressed에서는 ref.read를 사용합니다.

왜 onPressed에서는 read를 사용할까요? 콜백 함수는 버튼을 누를 때만 실행됩니다.

따라서 매번 리빌드마다 값을 구독할 필요가 없습니다. 오히려 watch를 사용하면 불필요한 리빌드가 발생할 수 있습니다.

이것이 바로 Riverpod 공식 문서에서 강조하는 중요한 규칙입니다. 초보 개발자들이 흔히 하는 실수가 있습니다.

build 메서드에서 ref.read를 사용하거나, 콜백에서 ref.watch를 사용하는 것입니다. 이렇게 하면 상태 변화가 제대로 반영되지 않거나, 불필요한 리빌드가 발생합니다.

"build에서는 watch, 콜백에서는 read"라는 원칙을 기억하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 환하게 웃었습니다. "아, watch는 구독, read는 일회성 읽기, listen은 감지군요!

리모컨 비유가 정말 이해하기 쉬웠어요." WidgetRef를 제대로 이해하면 상황에 맞는 올바른 메서드를 선택할 수 있습니다.

실전 팁

💡 - build 메서드 안에서는 ref.watch를 사용하세요

  • onPressed, onTap 같은 콜백에서는 ref.read를 사용하세요
  • 스낵바, 다이얼로그, 네비게이션 같은 사이드 이펙트는 ref.listen을 사용하세요

3. Consumer 위젯으로 부분 리빌드

김개발 씨가 만든 화면이 너무 느려졌습니다. 프로파일러로 확인해보니, 상태가 바뀔 때마다 화면 전체가 리빌드되고 있었습니다.

"이 작은 텍스트 하나 바꾸려고 전체를 다시 그린다고?" 박시니어 씨는 웃으며 Consumer 위젯을 알려주었습니다.

Consumer는 위젯 트리의 특정 부분만 리빌드할 수 있게 해주는 위젯입니다. 마치 방 전체 조명 대신 스탠드 조명만 켜는 것처럼, 필요한 부분만 업데이트하여 성능을 크게 향상시킵니다.

ConsumerWidget으로 전체를 감싸는 대신, Consumer로 필요한 부분만 감싸면 됩니다.

다음 코드를 살펴봅시다.

class ProductDetailScreen extends StatelessWidget {
  const ProductDetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('상품 상세')),
      body: Column(
        children: [
          // 이 부분은 리빌드되지 않음
          const ProductImage(),
          const ProductDescription(),

          // Consumer로 감싼 부분만 리빌드됨
          Consumer(
            builder: (context, ref, child) {
              final cartCount = ref.watch(cartCountProvider);
              return Text('장바구니: $cartCount개');
            },
          ),
        ],
      ),
    );
  }
}

김개발 씨는 쇼핑몰 앱의 상품 상세 화면을 만들었습니다. 장바구니에 상품을 추가하면 화면 하단의 카운트가 업데이트되어야 합니다.

그런데 이상합니다. 카운트가 바뀔 때마다 화면 전체가 깜빡이는 것 같았습니다.

Flutter DevTools로 확인해보니, 장바구니 숫자 하나 바꾸려고 상품 이미지, 설명, 리뷰까지 전부 리빌드되고 있었습니다. 비효율의 극치였습니다.

박시니어 씨가 다가왔습니다. "ConsumerWidget을 쓰면 전체 build 메서드가 다시 실행돼요.

부분만 업데이트하고 싶으면 Consumer 위젯을 써야 해요." Consumer 위젯이란 무엇일까요? 쉽게 비유하자면, Consumer는 마치 스탠드 조명과 같습니다.

방 전체 조명을 켜면 모든 곳이 밝아지지만 전기도 많이 씁니다. 반면 스탠드 조명은 책상 위만 비추므로 효율적입니다.

Consumer도 마찬가지로, 전체 화면 대신 필요한 부분만 리빌드합니다. Consumer를 사용하지 않으면 어떤 문제가 생길까요?

Provider 값이 변경될 때마다 ConsumerWidget의 build 메서드 전체가 실행됩니다. 이미지 로딩, 복잡한 레이아웃 계산, 애니메이션 등 비용이 큰 작업이 불필요하게 반복됩니다.

사용자는 버벅거림을 느끼고, 배터리도 빨리 닳습니다. Consumer를 사용하면 이 문제를 해결할 수 있습니다.

위의 코드를 살펴보면, ProductDetailScreen은 일반 StatelessWidget입니다. ConsumerWidget이 아닙니다.

따라서 이 화면 자체는 Provider 변경과 무관하게 리빌드되지 않습니다. 장바구니 카운트를 표시하는 부분만 Consumer로 감쌌습니다.

Consumer의 builder 함수는 익숙한 구조입니다. context, ref, child 세 개의 매개변수를 받습니다.

이 builder 안에서 ref.watch를 사용하면 해당 Provider가 변경될 때 builder 부분만 다시 실행됩니다. 실제로 얼마나 차이가 날까요?

상품 이미지가 고해상도이고, 설명에 복잡한 위젯이 있다고 가정해봅시다. ConsumerWidget을 사용하면 장바구니에 상품을 추가할 때마다 이 모든 것이 다시 그려집니다.

반면 Consumer를 사용하면 작은 텍스트 하나만 업데이트됩니다. 체감 성능 차이는 상당합니다.

Consumer에는 child 매개변수도 있습니다. builder 안에서 변경되지 않는 위젯이 있다면 child로 분리할 수 있습니다.

child는 리빌드되지 않고 재사용됩니다. 이렇게 하면 성능을 더욱 최적화할 수 있습니다.

주의할 점도 있습니다. Consumer를 너무 남발하면 코드가 복잡해집니다.

모든 곳에 Consumer를 쓰기보다는, 정말 성능이 중요한 부분에만 선택적으로 사용하는 것이 좋습니다. 대부분의 경우 ConsumerWidget으로 충분합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. Consumer를 적용한 후 프로파일러를 다시 확인했습니다.

장바구니 숫자가 바뀔 때 정말 그 부분만 리빌드되었습니다. "와, 이렇게 간단한 방법이 있었군요!" Consumer를 제대로 이해하면 성능과 코드 가독성 사이의 균형을 잡을 수 있습니다.

실전 팁

💡 - 전체 화면이 리빌드되어도 괜찮다면 ConsumerWidget을 사용하세요

  • 특정 부분만 자주 업데이트된다면 Consumer로 감싸세요
  • Consumer의 child 매개변수를 활용하면 성능을 더 최적화할 수 있습니다

4. ConsumerStatefulWidget

김개발 씨가 이번에는 애니메이션이 있는 화면을 만들어야 합니다. AnimationController를 사용하려면 StatefulWidget이 필요한데, Riverpod도 써야 합니다.

"StatefulWidget에서는 ref를 어떻게 쓰지?" 고민이 시작되었습니다.

ConsumerStatefulWidget은 StatefulWidget의 Riverpod 버전입니다. 마치 스위스 아미 나이프처럼, 상태 유지 기능과 Provider 접근 기능을 모두 갖추고 있습니다.

AnimationController, TextEditingController 등 생명주기 관리가 필요한 객체와 Riverpod을 함께 사용할 때 필요합니다.

다음 코드를 살펴봅시다.

class AnimatedCounterScreen extends ConsumerStatefulWidget {
  const AnimatedCounterScreen({super.key});

  @override
  ConsumerState<AnimatedCounterScreen> createState() =>
      _AnimatedCounterScreenState();
}

class _AnimatedCounterScreenState
    extends ConsumerState<AnimatedCounterScreen>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  Widget build(BuildContext context) {
    // ConsumerState에서는 ref를 바로 사용 가능
    final count = ref.watch(counterProvider);

    return ScaleTransition(
      scale: _controller,
      child: Text('$count'),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

김개발 씨는 카운터 값이 변경될 때마다 숫자가 튀어오르는 애니메이션을 구현하고 싶었습니다. AnimationController를 사용해야 하는데, 이것은 StatefulWidget에서만 사용할 수 있습니다.

그런데 카운터 값은 Provider에서 가져와야 합니다. "ConsumerWidget은 StatelessWidget 기반이잖아요.

StatefulWidget 버전은 없나요?" 박시니어 씨에게 물었습니다. "당연히 있지.

ConsumerStatefulWidget을 쓰면 돼." ConsumerStatefulWidget이란 무엇일까요? 쉽게 비유하자면, ConsumerStatefulWidget은 마치 스위스 아미 나이프와 같습니다.

일반 칼로는 할 수 없는 다양한 기능을 하나의 도구로 해결할 수 있듯이, ConsumerStatefulWidget은 상태 유지와 Provider 접근을 모두 가능하게 합니다. StatefulWidget에서는 initState, dispose 같은 생명주기 메서드를 사용할 수 있습니다.

AnimationController, TextEditingController, ScrollController 등은 이런 생명주기 관리가 필수입니다. ConsumerStatefulWidget은 이런 기능을 유지하면서 Riverpod Provider에도 접근할 수 있게 해줍니다.

위의 코드 구조를 살펴보겠습니다. 먼저 ConsumerStatefulWidget을 상속받는 클래스를 만듭니다.

createState 메서드의 반환 타입은 ConsumerState입니다. 일반 State가 아닙니다.

State 클래스는 ConsumerState를 상속받습니다. ConsumerState의 핵심은 ref를 클래스 속성으로 바로 사용할 수 있다는 점입니다.

ConsumerWidget에서는 build 메서드의 매개변수로 ref를 받았지만, ConsumerState에서는 this.ref로 어디서든 접근할 수 있습니다. 이것이 왜 중요할까요?

initState나 다른 생명주기 메서드에서도 ref를 사용할 수 있기 때문입니다. 예를 들어, 화면이 처음 로드될 때 Provider의 값을 읽어서 초기 설정을 해야 한다면, initState에서 ref.read를 사용할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 쇼핑몰 앱의 상품 검색 화면을 생각해봅시다.

검색어 입력을 위한 TextEditingController가 필요합니다. 동시에 검색 결과는 Provider에서 가져옵니다.

이런 경우 ConsumerStatefulWidget이 딱 맞습니다. 주의할 점이 있습니다.

ref.watch는 build 메서드에서만 사용해야 합니다. initState에서 ref.watch를 사용하면 에러가 발생합니다.

initState에서는 ref.read를 사용하고, 지속적인 구독이 필요하면 ref.listen을 사용하세요. 또한, ConsumerStatefulWidget을 과도하게 사용하는 것은 좋지 않습니다.

사실 AnimationController 같은 것도 Provider로 관리할 수 있습니다. Riverpod의 철학은 가능한 한 많은 상태를 Provider로 관리하는 것입니다.

ConsumerStatefulWidget은 정말 필요한 경우에만 사용하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.

ConsumerStatefulWidget을 사용해 애니메이션을 완성했습니다. 카운터 값이 변경될 때마다 숫자가 부드럽게 튀어오릅니다.

"StatefulWidget이 필요할 때도 Riverpod을 쓸 수 있구나!" ConsumerStatefulWidget을 제대로 이해하면 복잡한 UI 요구사항도 Riverpod과 함께 구현할 수 있습니다.

실전 팁

💡 - AnimationController, TextEditingController 등 dispose가 필요한 객체와 함께 사용하세요

  • ConsumerState에서는 this.ref로 어디서든 ref에 접근할 수 있습니다
  • 가능하다면 상태를 Provider로 옮기고 ConsumerWidget을 사용하는 것이 더 좋습니다

5. StatefulWidget에서 Riverpod 사용

김개발 씨의 프로젝트에는 이미 수백 개의 StatefulWidget이 있었습니다. 전부 ConsumerStatefulWidget으로 바꾸기에는 시간이 너무 오래 걸립니다.

"기존 StatefulWidget을 유지하면서 Riverpod을 쓸 수는 없을까요?" 현실적인 고민이 시작되었습니다.

기존 StatefulWidget 내부에서도 Consumer 위젯을 사용하면 Riverpod을 활용할 수 있습니다. 마치 기존 집에 새 가전제품을 설치하는 것처럼, 전체 구조를 바꾸지 않고도 필요한 기능을 추가할 수 있습니다.

레거시 코드를 점진적으로 마이그레이션할 때 유용한 방법입니다.

다음 코드를 살펴봅시다.

// 기존 StatefulWidget 유지
class LegacySearchScreen extends StatefulWidget {
  const LegacySearchScreen({super.key});

  @override
  State<LegacySearchScreen> createState() => _LegacySearchScreenState();
}

class _LegacySearchScreenState extends State<LegacySearchScreen> {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: _controller),

        // Consumer로 Riverpod 사용
        Consumer(
          builder: (context, ref, child) {
            final results = ref.watch(searchResultsProvider);
            return ListView.builder(
              shrinkWrap: true,
              itemCount: results.length,
              itemBuilder: (context, index) =>
                  ListTile(title: Text(results[index])),
            );
          },
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

김개발 씨가 입사한 회사에는 3년 된 Flutter 프로젝트가 있었습니다. 수백 개의 StatefulWidget이 있는데, 새로 도입하기로 한 Riverpod을 적용해야 합니다.

모든 위젯을 ConsumerStatefulWidget으로 바꾸려면 몇 달이 걸릴 것 같았습니다. "선배님, 더 현실적인 방법은 없을까요?" 박시니어 씨에게 물었습니다.

"물론 있지. 기존 StatefulWidget 안에서 Consumer를 쓰면 돼.

전체를 바꾸지 않아도 Riverpod을 사용할 수 있어." 이 방법이 왜 유용할까요? 쉽게 비유하자면, 기존 집에 스마트 스피커를 설치하는 것과 같습니다.

집 전체를 스마트 홈으로 리모델링하지 않아도, 스피커 하나만 설치하면 일부 스마트 기능을 사용할 수 있습니다. Consumer도 마찬가지로, 기존 위젯 구조를 유지하면서 필요한 부분에만 Riverpod을 적용할 수 있습니다.

위의 코드를 살펴보겠습니다. LegacySearchScreen은 완전히 일반적인 StatefulWidget입니다.

ConsumerStatefulWidget이 아닙니다. TextEditingController를 관리하는 기존 로직을 그대로 유지합니다.

검색 결과를 보여주는 부분만 Consumer로 감쌌습니다. Consumer의 builder 안에서 ref.watch를 사용해 검색 결과 Provider를 구독합니다.

검색 결과가 변경되면 ListView 부분만 리빌드됩니다. 이 접근 방식의 장점은 무엇일까요?

첫째, 점진적 마이그레이션이 가능합니다. 한 번에 모든 코드를 바꾸지 않고, 필요한 부분부터 조금씩 Riverpod을 적용할 수 있습니다.

둘째, 위험 최소화입니다. 기존에 잘 동작하는 코드를 건드리지 않으므로 버그 발생 위험이 줄어듭니다.

셋째, 학습 곡선 완화입니다. 팀원들이 Riverpod에 익숙해지는 동안 기존 방식도 유지할 수 있습니다.

실제 마이그레이션 전략을 세워봅시다. 1단계: 새로 만드는 위젯은 ConsumerWidget이나 ConsumerStatefulWidget으로 작성합니다.

2단계: 기존 위젯 중 수정이 필요한 부분에는 Consumer를 사용합니다. 3단계: 시간이 허락할 때 점진적으로 ConsumerStatefulWidget으로 전환합니다.

주의할 점도 있습니다. Consumer를 너무 깊이 중첩하면 코드가 복잡해집니다.

또한 StatefulWidget의 State에서 직접 ref에 접근할 수 없으므로, 콜백 함수를 통해 값을 전달해야 하는 경우가 생깁니다. 장기적으로는 ConsumerStatefulWidget으로 전환하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 이 방법을 알게 된 후 마이그레이션 계획을 다시 세웠습니다.

급한 기능부터 Consumer를 적용하고, 나머지는 차근차근 전환하기로 했습니다. "한 번에 다 바꾸지 않아도 되니까 부담이 훨씬 덜하네요!" 기존 StatefulWidget에서 Consumer를 활용하면 현실적인 마이그레이션 전략을 세울 수 있습니다.

실전 팁

💡 - 새 코드는 ConsumerWidget/ConsumerStatefulWidget으로, 기존 코드는 Consumer로 시작하세요

  • Consumer를 너무 많이 중첩하면 오히려 코드가 복잡해집니다
  • 장기적으로는 ConsumerStatefulWidget으로 통일하는 것이 유지보수에 좋습니다

6. 성능 최적화 팁

김개발 씨의 앱이 점점 커지면서 성능 문제가 나타나기 시작했습니다. Provider를 여러 개 구독하는 화면에서 버벅거림이 느껴졌습니다.

"Riverpod을 제대로 쓰고 있는 건 맞는데, 왜 느리지?" 최적화가 필요한 시점이 왔습니다.

Riverpod 성능 최적화의 핵심은 불필요한 리빌드를 줄이는 것입니다. 마치 물을 아껴 쓰듯이, 리빌드도 필요한 곳에만 발생하도록 해야 합니다.

select를 활용한 부분 구독, Consumer를 통한 범위 제한, 그리고 적절한 Provider 분리가 핵심 전략입니다.

다음 코드를 살펴봅시다.

class OptimizedProductScreen extends ConsumerWidget {
  const OptimizedProductScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        // select로 필요한 값만 구독
        Consumer(
          builder: (context, ref, child) {
            // 상품 목록 중 길이만 구독 (목록 내용 변경시 리빌드 안됨)
            final count = ref.watch(
              productsProvider.select((products) => products.length),
            );
            return Text('총 $count개 상품');
          },
        ),

        // 개별 상품은 별도 Consumer로 분리
        Expanded(
          child: ListView.builder(
            itemCount: ref.watch(productsProvider).length,
            itemBuilder: (context, index) => ProductTile(index: index),
          ),
        ),
      ],
    );
  }
}

// 개별 아이템을 위한 별도 위젯
class ProductTile extends ConsumerWidget {
  final int index;
  const ProductTile({super.key, required this.index});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 해당 인덱스의 상품만 구독
    final product = ref.watch(
      productsProvider.select((products) => products[index]),
    );
    return ListTile(title: Text(product.name));
  }
}

김개발 씨의 쇼핑몰 앱에는 수백 개의 상품이 표시됩니다. 상품 하나의 가격이 바뀔 때마다 전체 목록이 리빌드되어 스크롤이 버벅거렸습니다.

박시니어 씨가 코드를 검토했습니다. "김개발 씨, 여기 최적화할 부분이 많네요.

하나씩 알려줄게요." 첫 번째 팁: select를 활용하세요 ref.watch로 Provider를 구독하면 Provider의 어떤 값이든 변경되면 리빌드됩니다. 하지만 select를 사용하면 특정 값만 구독할 수 있습니다.

마치 뷔페에서 전체 음식 대신 필요한 것만 가져오는 것과 같습니다. 예를 들어, 상품 목록의 개수만 표시하는 위젯이 있다고 합시다.

상품의 이름이나 가격이 바뀌어도 개수는 그대로입니다. 이럴 때 select를 사용하면 개수가 변경될 때만 리빌드됩니다.

두 번째 팁: 리스트 아이템을 별도 위젯으로 분리하세요 ListView.builder의 itemBuilder 안에서 직접 ref.watch를 사용하면, 어떤 아이템이 변경되어도 모든 아이템이 리빌드될 수 있습니다. 각 아이템을 별도의 ConsumerWidget으로 분리하면 해당 아이템만 리빌드됩니다.

위의 코드에서 ProductTile은 별도의 ConsumerWidget입니다. select를 사용해 자신의 인덱스에 해당하는 상품만 구독합니다.

다른 상품이 변경되어도 이 위젯은 리빌드되지 않습니다. 세 번째 팁: Provider를 적절히 분리하세요 하나의 거대한 Provider보다 여러 개의 작은 Provider가 성능에 유리합니다.

예를 들어, 사용자 정보와 장바구니를 하나의 Provider로 관리하면, 장바구니가 변경될 때 사용자 정보를 표시하는 위젯도 리빌드됩니다. 분리하면 이런 문제를 피할 수 있습니다.

네 번째 팁: const 생성자를 활용하세요 위젯에 const 생성자를 사용하면 Flutter가 위젯을 캐싱할 수 있습니다. Consumer의 child 매개변수와 함께 사용하면 리빌드되지 않는 부분을 효율적으로 재사용할 수 있습니다.

다섯 번째 팁: ref.read를 적절히 사용하세요 값을 한 번만 읽으면 되는 상황에서 ref.watch를 사용하면 불필요한 구독이 생깁니다. 초기화나 이벤트 핸들러에서는 ref.read를 사용하세요.

하지만 build 메서드에서는 여전히 ref.watch를 사용해야 합니다. 성능 측정도 중요합니다.

Flutter DevTools의 Performance 탭을 활용하세요. 어떤 위젯이 얼마나 자주 리빌드되는지 확인할 수 있습니다.

추측보다는 측정을 기반으로 최적화하는 것이 효과적입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

이 모든 팁을 적용한 후 앱이 훨씬 부드러워졌습니다. "select 하나만 써도 이렇게 달라지다니!" DevTools로 확인해보니 불필요한 리빌드가 90% 이상 줄어들었습니다.

성능 최적화를 제대로 이해하면 수천 개의 아이템도 부드럽게 처리할 수 있습니다.

실전 팁

💡 - select를 사용해 꼭 필요한 값만 구독하세요

  • 리스트의 각 아이템은 별도 위젯으로 분리하고 select를 활용하세요
  • Flutter DevTools로 리빌드 횟수를 측정하고 병목 지점을 찾으세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Flutter#Riverpod#ConsumerWidget#Consumer#StateManagement#Flutter,State Management

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스