🤖

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

⚠️

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

이미지 로딩 중...

ProviderScope로 의존성 주입 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 11. · 11 Views

ProviderScope로 의존성 주입 완벽 가이드

Riverpod의 ProviderScope를 활용한 의존성 주입 패턴을 실무 중심으로 알아봅니다. ProviderContainer 직접 생성부터 Flutter 외부 사용까지, 초급 개발자도 쉽게 이해할 수 있도록 실전 예제와 함께 설명합니다.


목차

  1. ProviderScope 필수 설정
  2. ProviderContainer 직접 생성
  3. 중첩 ProviderScope
  4. UncontrolledProviderScope
  5. Flutter 외부에서 사용
  6. main() 초기화 패턴

1. ProviderScope 필수 설정

어느 날 김개발 씨가 Flutter 프로젝트에 Riverpod을 도입하려고 합니다. 공식 문서를 보니 main 함수에서 ProviderScope로 앱을 감싸라고 되어 있습니다.

"이게 왜 필요한 거지?" 김개발 씨는 궁금해졌습니다.

ProviderScope는 Riverpod이 작동하기 위한 필수 설정입니다. 마치 식당에서 주문을 받고 음식을 관리하는 매니저와 같습니다.

이것을 제대로 설정하면 앱 전체에서 상태 관리를 사용할 수 있게 됩니다.

다음 코드를 살펴봅시다.

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

void main() {
  // ProviderScope로 앱 전체를 감싸줍니다
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

김개발 씨는 입사 2개월 차 주니어 개발자입니다. 오늘 처음으로 Riverpod을 프로젝트에 도입하는 임무를 받았습니다.

설렘 반 걱정 반으로 공식 문서를 펼쳐봅니다. 첫 페이지부터 눈에 띄는 것이 있습니다.

"반드시 ProviderScope로 앱을 감싸야 합니다." 김개발 씨는 고개를 갸우뚱합니다. "왜 굳이 감싸야 하는 거지?" 선배 개발자 박시니어 씨가 옆에서 슬쩍 말을 건넵니다.

"아, 그거 안 하면 Provider 하나도 못 써요. 에러 바로 납니다." 그렇다면 ProviderScope란 정확히 무엇일까요?

쉽게 비유하자면, ProviderScope는 마치 식당의 매니저와 같습니다. 손님이 주문을 하면 매니저가 주방에 전달하고, 음식이 나오면 테이블로 가져다줍니다.

이처럼 ProviderScope도 앱 전체에서 Provider들을 관리하고 연결해주는 역할을 담당합니다. ProviderScope가 없던 시절에는 어땠을까요?

초기 상태 관리 패턴에서는 개발자들이 직접 의존성을 주입하고 관리해야 했습니다. 각 위젯마다 필요한 데이터를 일일이 전달하는 것이 일반적이었습니다.

더 큰 문제는 깊은 위젯 트리에서 데이터를 전달할 때였습니다. 5단계, 10단계 아래로 데이터를 내려보내려면 중간의 모든 위젯이 그 데이터를 받아서 다시 전달해야 했습니다.

바로 이런 문제를 해결하기 위해 ProviderScope가 등장했습니다. ProviderScope를 사용하면 앱 전체에서 Provider에 접근이 가능해집니다.

또한 위젯 트리의 어느 위치에서든 필요한 데이터를 바로 가져올 수 있습니다. 무엇보다 코드가 깔끔해지고 유지보수가 쉬워진다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 main 함수에서 runApp을 호출할 때 ProviderScope로 감싸는 것을 알 수 있습니다.

이 부분이 핵심입니다. 다음으로 child 파라미터에 MyApp을 전달합니다.

이렇게 하면 MyApp 아래의 모든 위젯에서 Provider를 사용할 수 있게 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다. 사용자 로그인 정보, 장바구니 데이터, 상품 목록 등 다양한 상태를 관리해야 합니다.

ProviderScope를 main 함수에 한 번만 설정해두면, 홈 화면에서든 상세 화면에서든 장바구니 화면에서든 어디서든 필요한 데이터에 접근할 수 있습니다. 많은 기업에서 이런 패턴을 적극적으로 사용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 ProviderScope를 여러 개 중첩해서 사용하는 것입니다.

특별한 이유가 없다면 main 함수에 한 번만 설정하면 충분합니다. 불필요하게 중첩하면 오히려 복잡도만 증가합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 필수인 거군요!" ProviderScope를 제대로 설정하면 Riverpod의 모든 기능을 자유롭게 사용할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - ProviderScope는 반드시 main 함수에서 설정하세요

  • 특별한 이유가 없다면 한 번만 설정하면 충분합니다
  • ProviderScope 없이 Provider를 사용하면 런타임 에러가 발생합니다

2. ProviderContainer 직접 생성

김개발 씨가 코드를 작성하던 중 궁금증이 생겼습니다. "Flutter 위젯이 아닌 곳에서도 Provider를 쓸 수 있을까?" 예를 들어 순수 Dart 클래스나 테스트 코드에서 말입니다.

ProviderContainer는 Flutter 없이 Provider를 사용할 수 있게 해주는 도구입니다. 마치 포장 가능한 도시락 세트처럼, 필요한 곳 어디든 가져갈 수 있습니다.

테스트나 백그라운드 작업에서 특히 유용합니다.

다음 코드를 살펴봅시다.

import 'package:riverpod/riverpod.dart';

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

void main() {
  // ProviderContainer를 직접 생성합니다
  final container = ProviderContainer();

  // Provider 값 읽기
  final counter = container.read(counterProvider);
  print('초기값: $counter'); // 출력: 0

  // Provider 값 변경
  container.read(counterProvider.notifier).state = 42;
  print('변경된 값: ${container.read(counterProvider)}'); // 출력: 42

  // 사용 후 반드시 dispose 호출
  container.dispose();
}

김개발 씨는 요즘 유닛 테스트 작성에 빠져 있습니다. 테스트 코드를 작성하다 보니 문득 의문이 생겼습니다.

"Provider를 테스트하려면 어떻게 해야 하지? Flutter 위젯 없이도 가능할까?" 점심시간에 박시니어 씨에게 물어봤습니다.

"선배님, 테스트에서 Provider 어떻게 써요?" 박시니어 씨가 웃으며 대답합니다. "ProviderContainer 쓰면 돼요.

그거 진짜 편해요." 그렇다면 ProviderContainer란 정확히 무엇일까요? 쉽게 비유하자면, ProviderContainer는 마치 포장 가능한 도시락 세트와 같습니다.

식당에서 먹는 음식이 ProviderScope라면, 도시락으로 포장해서 어디든 들고 갈 수 있는 것이 ProviderContainer입니다. 이처럼 ProviderContainer도 Flutter 위젯 트리와 독립적으로 Provider를 사용할 수 있게 해줍니다.

ProviderContainer가 없던 시절에는 어땠을까요? Provider를 테스트하려면 반드시 Flutter 위젯을 만들어야 했습니다.

testWidgets를 사용하고, ProviderScope로 감싸고, 위젯을 빌드하는 복잡한 과정이 필요했습니다. 더 큰 문제는 백그라운드 작업이나 서버 사이드 코드에서는 Provider를 전혀 사용할 수 없었다는 점입니다.

바로 이런 문제를 해결하기 위해 ProviderContainer가 등장했습니다. ProviderContainer를 사용하면 Flutter 없이도 Provider에 접근이 가능해집니다.

또한 테스트 코드를 간단하게 작성할 수 있습니다. 무엇보다 백그라운드 작업이나 CLI 도구에서도 Riverpod을 활용할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ProviderContainer를 생성하는 것을 알 수 있습니다.

이 부분이 핵심입니다. 다음으로 container.read를 사용해서 Provider의 값을 읽습니다.

notifier를 통해 값을 변경하는 것도 가능합니다. 마지막으로 반드시 dispose를 호출해야 메모리 누수를 방지할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 백그라운드에서 주기적으로 데이터를 동기화하는 작업을 개발한다고 가정해봅시다.

Flutter 위젯과는 별개로 동작하는 Dart 클래스에서 ProviderContainer를 생성하여 필요한 데이터 소스나 리포지토리에 접근할 수 있습니다. 실제로 많은 기업에서 알림 처리, 데이터 동기화, 배치 작업 등에 이 패턴을 활용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 dispose를 호출하지 않는 것입니다.

ProviderContainer를 생성한 후 사용이 끝나면 반드시 dispose를 호출해야 합니다. 그렇지 않으면 메모리 누수가 발생할 수 있습니다.

따라서 try-finally 블록이나 using 패턴을 활용하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "와, 테스트가 진짜 간단해지네요!" ProviderContainer를 제대로 이해하면 Flutter 위젯 외부에서도 Riverpod의 강력한 기능을 활용할 수 있습니다.

여러분도 오늘 배운 내용을 테스트 코드에 적용해 보세요.

실전 팁

💡 - ProviderContainer 생성 후 반드시 dispose를 호출하세요

  • 테스트에서 간단하게 Provider를 초기화할 수 있습니다
  • Flutter 위젯 없이도 Provider를 사용할 수 있습니다

3. 중첩 ProviderScope

김개발 씨가 복잡한 앱 구조를 설계하다가 막혔습니다. "서로 다른 환경에서 다른 Provider 값을 사용하고 싶은데, 어떻게 하지?" 예를 들어 개발 환경과 운영 환경에서 다른 API 주소를 사용하고 싶습니다.

중첩 ProviderScope는 Provider의 값을 부분적으로 재정의할 수 있게 해줍니다. 마치 건물마다 다른 인터넷 설정을 사용하는 것처럼, 위젯 트리의 특정 부분에서만 다른 값을 사용할 수 있습니다.

다음 코드를 살펴봅시다.

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

// API URL Provider
final apiUrlProvider = Provider<String>((ref) => 'https://api.prod.com');

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        home: Scaffold(
          body: Column(
            children: [
              // 기본값 사용
              Consumer(
                builder: (context, ref, child) {
                  final url = ref.watch(apiUrlProvider);
                  return Text('운영: $url');
                },
              ),
              // 개발 환경용으로 재정의
              ProviderScope(
                overrides: [
                  apiUrlProvider.overrideWithValue('https://api.dev.com'),
                ],
                child: Consumer(
                  builder: (context, ref, child) {
                    final url = ref.watch(apiUrlProvider);
                    return Text('개발: $url');
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}

김개발 씨는 이제 중급 개발자가 되었습니다. 오늘은 앱에 개발자 모드 기능을 추가하는 작업을 맡았습니다.

개발자 모드에서는 실제 서버가 아닌 테스트 서버에 접속해야 합니다. "이거 어떻게 구현하지?" 김개발 씨는 고민에 빠졌습니다.

API URL을 하드코딩할 수도 없고, 매번 Provider를 새로 만들 수도 없습니다. 바로 그때 박시니어 씨가 지나가다 한마디 던집니다.

"중첩 ProviderScope 써보셨어요?" 그렇다면 중첩 ProviderScope란 정확히 무엇일까요? 쉽게 비유하자면, 중첩 ProviderScope는 마치 건물마다 다른 인터넷 설정을 사용하는 것과 같습니다.

전체 아파트 단지는 기본 인터넷을 사용하지만, 특정 동에서는 다른 회사의 인터넷을 쓸 수 있습니다. 이처럼 중첩 ProviderScope도 위젯 트리의 특정 부분에서만 다른 Provider 값을 제공할 수 있게 해줍니다.

중첩 ProviderScope가 없던 시절에는 어땠을까요? 환경마다 다른 설정을 사용하려면 Provider를 여러 개 만들어야 했습니다.

prodApiUrlProvider, devApiUrlProvider처럼 말입니다. 코드가 중복되고, 실수하기도 쉬웠습니다.

더 큰 문제는 런타임에 동적으로 환경을 전환하기가 매우 어려웠다는 점입니다. 바로 이런 문제를 해결하기 위해 중첩 ProviderScope가 등장했습니다.

중첩 ProviderScope를 사용하면 동일한 Provider를 다른 값으로 재정의할 수 있습니다. 또한 위젯 트리의 일부분에만 영향을 줍니다.

무엇보다 런타임에 동적으로 환경을 전환할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 최상위에 ProviderScope가 있고, 그 안에 기본 Consumer가 있습니다. 여기서는 운영 환경의 API URL이 출력됩니다.

다음으로 또 다른 ProviderScope가 중첩되어 있는데, overrides 파라미터를 통해 apiUrlProvider를 재정의합니다. 이 ProviderScope 안쪽에서는 개발 환경의 API URL이 사용됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 A/B 테스트를 구현한다고 가정해봅시다.

사용자의 일부에게는 새로운 UI를, 나머지에게는 기존 UI를 보여주고 싶습니다. 중첩 ProviderScope를 활용하면 특정 사용자 그룹에 대해서만 테마 Provider나 기능 플래그 Provider를 재정의할 수 있습니다.

실제로 많은 기업에서 실험 기능을 안전하게 배포하기 위해 이 패턴을 사용합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 중첩 ProviderScope를 남용하는 것입니다. 너무 깊게 중첩하면 코드를 이해하기 어려워지고, 어느 위치에서 어떤 값이 사용되는지 추적하기 힘들어집니다.

따라서 꼭 필요한 경우에만 사용하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언을 들은 김개발 씨는 바로 코드를 수정했습니다. "이제 개발 모드 구현이 정말 쉬워졌어요!" 중첩 ProviderScope를 제대로 이해하면 환경별 설정이나 A/B 테스트 같은 복잡한 요구사항도 우아하게 해결할 수 있습니다.

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

실전 팁

💡 - overrides를 사용하여 특정 Provider만 재정의할 수 있습니다

  • 중첩이 너무 깊어지지 않도록 주의하세요
  • 테스트 환경이나 개발자 모드에 특히 유용합니다

4. UncontrolledProviderScope

김개발 씨가 고급 기능을 탐험하던 중 UncontrolledProviderScope라는 이름을 발견했습니다. "이건 또 뭐지?

일반 ProviderScope와 뭐가 다른 거지?" 궁금증이 생겼습니다.

UncontrolledProviderScope는 외부에서 생성한 ProviderContainer를 위젯 트리에 주입할 수 있게 해줍니다. 마치 이미 만들어진 도시락을 식당 테이블에 가져다 놓는 것처럼, 기존 컨테이너를 위젯에서 사용할 수 있습니다.

다음 코드를 살펴봅시다.

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

final messageProvider = Provider<String>((ref) => '기본 메시지');

void main() {
  // ProviderContainer를 직접 생성
  final container = ProviderContainer(
    overrides: [
      messageProvider.overrideWithValue('외부에서 만든 메시지'),
    ],
  );

  runApp(
    // 외부 컨테이너를 주입
    UncontrolledProviderScope(
      container: container,
      child: MaterialApp(
        home: Scaffold(
          body: Consumer(
            builder: (context, ref, child) {
              final message = ref.watch(messageProvider);
              return Center(child: Text(message));
            },
          ),
        ),
      ),
    ),
  );

  // 주의: 앱이 종료될 때 수동으로 dispose 호출 필요
}

김개발 씨는 이제 시니어 개발자가 되었습니다. 최근 복잡한 요구사항이 들어왔습니다.

앱 시작 전에 Provider를 미리 초기화하고, 특정 데이터를 미리 로드해야 합니다. "일반 ProviderScope로는 안 되겠는걸?" 김개발 씨는 고민에 빠졌습니다.

점심시간에 박시니어 씨를 만났습니다. 이제는 둘 다 시니어가 되어 동료가 되었습니다.

"박 선배, 이런 케이스는 어떻게 처리하세요?" 박시니어 씨가 빙그레 웃으며 대답합니다. "UncontrolledProviderScope 한번 써보셨어요?" 그렇다면 UncontrolledProviderScope란 정확히 무엇일까요?

쉽게 비유하자면, UncontrolledProviderScope는 마치 이미 만들어진 도시락을 식당 테이블에 가져다 놓는 것과 같습니다. 일반 ProviderScope는 식당에서 음식을 주문하고 만들어서 제공하는 것이라면, UncontrolledProviderScope는 외부에서 준비한 음식을 그대로 가져다 쓰는 것입니다.

이처럼 UncontrolledProviderScope도 외부에서 생성한 ProviderContainer를 위젯 트리에 연결해주는 역할을 합니다. UncontrolledProviderScope가 없던 시절에는 어땠을까요?

앱 시작 전에 Provider를 초기화하거나 특정 값을 미리 설정하기가 매우 어려웠습니다. ProviderScope는 내부적으로 컨테이너를 생성하기 때문에, 개발자가 직접 제어할 수 없었습니다.

더 큰 문제는 테스트에서 특정 상태로 초기화된 컨테이너를 주입하기가 복잡했다는 점입니다. 바로 이런 문제를 해결하기 위해 UncontrolledProviderScope가 등장했습니다.

UncontrolledProviderScope를 사용하면 ProviderContainer를 직접 제어할 수 있습니다. 또한 앱 시작 전에 초기화 작업을 수행할 수 있습니다.

무엇보다 테스트에서 원하는 상태로 미리 설정된 컨테이너를 주입할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 main 함수에서 ProviderContainer를 직접 생성합니다. 이때 overrides를 통해 원하는 값으로 초기화할 수 있습니다.

다음으로 UncontrolledProviderScope의 container 파라미터에 생성한 컨테이너를 전달합니다. 이렇게 하면 위젯 트리에서 외부 컨테이너의 Provider들을 사용할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 앱 시작 시 사용자 인증 정보를 확인하고, 캐시된 데이터를 로드하는 복잡한 초기화 작업이 필요하다고 가정해봅시다.

main 함수에서 ProviderContainer를 생성하고 필요한 모든 초기화를 완료한 후, UncontrolledProviderScope로 위젯 트리에 연결할 수 있습니다. 실제로 많은 기업에서 복잡한 부트스트랩 로직을 구현할 때 이 패턴을 활용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 컨테이너의 생명주기를 잊어버리는 것입니다.

UncontrolledProviderScope는 이름 그대로 제어되지 않는 ProviderScope입니다. 즉, 개발자가 직접 컨테이너의 dispose를 호출해야 합니다.

앱이 종료될 때 반드시 정리 작업을 해야 메모리 누수를 방지할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언을 따라 구현한 김개발 씨는 만족스러운 표정을 지었습니다. "이제 초기화 로직을 깔끔하게 분리할 수 있겠어요!" UncontrolledProviderScope를 제대로 이해하면 고급 초기화 패턴이나 복잡한 테스트 시나리오도 우아하게 처리할 수 있습니다.

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

실전 팁

💡 - 컨테이너의 dispose를 직접 호출해야 합니다

  • 앱 시작 전 복잡한 초기화가 필요할 때 유용합니다
  • 대부분의 경우 일반 ProviderScope로 충분하므로 꼭 필요할 때만 사용하세요

5. Flutter 외부에서 사용

김개발 씨가 새로운 프로젝트를 시작했습니다. 이번에는 Flutter 앱뿐만 아니라 CLI 도구도 함께 개발해야 합니다.

"Flutter 없이도 Riverpod을 쓸 수 있을까?" 김개발 씨는 궁금해졌습니다.

Riverpod은 Flutter에 의존하지 않는 순수 Dart 패키지로도 제공됩니다. 마치 휴대용 배터리처럼, Flutter 위젯 없이도 어디서든 사용할 수 있습니다.

CLI 도구나 서버 코드에서도 동일한 Provider 패턴을 활용할 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:riverpod/riverpod.dart';

// 데이터 소스 Provider
final dataSourceProvider = Provider<DataSource>((ref) {
  return DataSource();
});

// 리포지토리 Provider
final repositoryProvider = Provider<Repository>((ref) {
  final dataSource = ref.watch(dataSourceProvider);
  return Repository(dataSource);
});

class DataSource {
  String fetchData() => '서버에서 가져온 데이터';
}

class Repository {
  final DataSource dataSource;
  Repository(this.dataSource);

  String getData() => dataSource.fetchData();
}

void main() {
  // Flutter 없이 ProviderContainer 사용
  final container = ProviderContainer();

  final repository = container.read(repositoryProvider);
  print(repository.getData()); // 출력: 서버에서 가져온 데이터

  container.dispose();
}

김개발 씨는 이제 풀스택 개발자가 되었습니다. 최근 회사에서 새로운 프로젝트를 시작했는데, Flutter 앱과 함께 데이터 마이그레이션을 위한 CLI 도구도 만들어야 합니다.

"앱과 CLI 도구에서 같은 비즈니스 로직을 공유할 수 있으면 좋을 텐데." 김개발 씨는 생각에 잠겼습니다. 회의 시간에 박시니어 씨가 제안합니다.

"우리 이미 Riverpod 쓰고 있잖아요. 그거 Flutter 없이도 되거든요." 김개발 씨의 눈이 반짝였습니다.

"정말요?" 그렇다면 Flutter 외부에서 Riverpod 사용이란 정확히 무엇일까요? 쉽게 비유하자면, Riverpod은 마치 휴대용 배터리와 같습니다.

스마트폰에서만 쓰는 것이 아니라, 태블릿에서도, 노트북에서도 사용할 수 있습니다. 이처럼 Riverpod도 Flutter 위젯에 종속되지 않고 어디서든 사용할 수 있는 독립적인 상태 관리 라이브러리입니다.

Flutter 외부에서 사용할 수 없던 시절에는 어땠을까요? 상태 관리 라이브러리들은 대부분 Flutter에 강하게 결합되어 있었습니다.

Provider나 BLoC 같은 라이브러리는 Flutter 위젯 없이는 사용할 수 없었습니다. 더 큰 문제는 Flutter 앱과 CLI 도구에서 서로 다른 패턴을 사용해야 했다는 점입니다.

코드 중복이 발생하고, 일관성을 유지하기 어려웠습니다. 바로 이런 문제를 해결하기 위해 Riverpod은 순수 Dart 패키지로 설계되었습니다.

Riverpod을 Flutter 외부에서 사용하면 Flutter 앱과 동일한 패턴을 CLI나 서버에서도 활용할 수 있습니다. 또한 비즈니스 로직을 플랫폼 독립적으로 작성할 수 있습니다.

무엇보다 테스트가 훨씬 쉬워진다는 큰 이점이 있습니다. Flutter 위젯을 만들 필요 없이 순수 Dart 테스트로 작성할 수 있기 때문입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 riverpod 패키지를 import하는 것을 주목하세요.

flutter_riverpod이 아닌 riverpod입니다. 다음으로 일반적인 Provider들을 정의합니다.

Flutter 코드와 전혀 다르지 않습니다. main 함수에서 ProviderContainer를 생성하고 Provider를 읽어서 사용합니다.

마지막으로 dispose를 호출하여 정리합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 데이터베이스 마이그레이션 도구를 만든다고 가정해봅시다. Flutter 앱에서 사용하는 리포지토리 패턴과 동일한 구조를 CLI 도구에서도 사용할 수 있습니다.

dataSourceProvider, repositoryProvider 등을 그대로 재사용하면서, CLI 환경에 맞게 일부 구현만 교체할 수 있습니다. 실제로 많은 기업에서 관리 도구나 배치 작업을 개발할 때 이 패턴을 활용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 flutter_riverpod과 riverpod을 혼동하는 것입니다.

Flutter 위젯에서는 flutter_riverpod을 사용해야 ConsumerWidget이나 Consumer 같은 위젯을 쓸 수 있습니다. 반면 순수 Dart 코드에서는 riverpod 패키지만 있으면 충분합니다.

패키지를 올바르게 선택하는 것이 중요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 제안을 받아들인 김개발 씨는 CLI 도구를 빠르게 개발했습니다. "와, 앱과 똑같은 패턴을 쓰니까 정말 빠르네요!" Riverpod을 Flutter 외부에서 활용하는 방법을 이해하면 플랫폼을 넘나드는 일관된 코드베이스를 구축할 수 있습니다.

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

실전 팁

💡 - riverpod 패키지를 사용하면 Flutter 없이도 Riverpod을 쓸 수 있습니다

  • CLI 도구나 백엔드 코드에서도 동일한 Provider 패턴을 활용하세요
  • 비즈니스 로직을 플랫폼 독립적으로 작성할 수 있습니다

6. main() 초기화 패턴

김개발 씨가 대규모 프로젝트를 맡았습니다. 앱 시작 시 로깅 시스템 초기화, 데이터베이스 연결, 사용자 설정 로드 등 해야 할 일이 산더미입니다.

"이걸 어떻게 깔끔하게 정리하지?" 김개발 씨는 고민에 빠졌습니다.

main 함수 초기화 패턴은 앱 시작 전에 필요한 모든 설정을 체계적으로 처리하는 방법입니다. 마치 공연 전 무대 준비와 같습니다.

ProviderContainer를 활용하여 필요한 모든 초기화를 완료한 후 앱을 시작할 수 있습니다.

다음 코드를 살펴봅시다.

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

// 초기화가 필요한 Provider들
final databaseProvider = Provider<Database>((ref) => Database());
final loggerProvider = Provider<Logger>((ref) => Logger());

class Database {
  Future<void> initialize() async {
    await Future.delayed(Duration(seconds: 1));
    print('데이터베이스 초기화 완료');
  }
}

class Logger {
  void setup() => print('로거 설정 완료');
}

Future<void> main() async {
  // Flutter 바인딩 초기화
  WidgetsFlutterBinding.ensureInitialized();

  // ProviderContainer 생성 및 초기화
  final container = ProviderContainer();

  // 필요한 서비스들을 미리 초기화
  final database = container.read(databaseProvider);
  await database.initialize();

  final logger = container.read(loggerProvider);
  logger.setup();

  // 모든 초기화 완료 후 앱 시작
  runApp(
    UncontrolledProviderScope(
      container: container,
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Scaffold(body: Center(child: Text('앱 실행!'))));
  }
}

김개발 씨는 이제 리드 개발자가 되었습니다. 새로운 대형 프로젝트를 시작하면서 앱 아키텍처를 설계하는 중입니다.

"앱이 시작되기 전에 해야 할 일이 너무 많아." 데이터베이스 초기화, API 클라이언트 설정, 로깅 시스템 준비, 사용자 인증 상태 확인 등 리스트가 끝이 없습니다. 팀 미팅에서 박시니어 씨가 말합니다.

"체계적인 초기화 패턴이 필요할 것 같은데요?" 김개발 씨가 고개를 끄덕입니다. "맞아요.

지금 main 함수가 너무 복잡해지고 있어요." 그렇다면 main 함수 초기화 패턴이란 정확히 무엇일까요? 쉽게 비유하자면, main 함수 초기화 패턴은 마치 공연 전 무대 준비와 같습니다.

관객이 입장하기 전에 조명을 점검하고, 음향을 테스트하고, 소품을 배치합니다. 모든 준비가 완료되어야만 공연이 시작됩니다.

이처럼 앱도 모든 필수 서비스가 준비된 후에 UI를 렌더링해야 합니다. 체계적인 초기화 패턴이 없던 시절에는 어땠을까요?

개발자들은 initState나 build 메서드에서 초기화 작업을 수행했습니다. 문제는 이때 이미 위젯 트리가 생성되고 있다는 점입니다.

초기화가 완료되지 않은 상태에서 위젯이 Provider를 읽으려고 하면 에러가 발생했습니다. 더 큰 문제는 초기화 로직이 여러 곳에 흩어져서 관리가 어려웠다는 점입니다.

바로 이런 문제를 해결하기 위해 main 함수 초기화 패턴이 등장했습니다. 이 패턴을 사용하면 앱 시작 전에 모든 초기화를 완료할 수 있습니다.

또한 초기화 로직이 한곳에 모여 유지보수가 쉬워집니다. 무엇보다 위젯이 렌더링될 때는 이미 모든 서비스가 준비된 상태라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 main 함수를 async로 만듭니다.

이는 비동기 초기화 작업을 수행하기 위함입니다. 다음으로 WidgetsFlutterBinding.ensureInitialized를 호출합니다.

이는 runApp 전에 Flutter 엔진을 초기화하는 필수 단계입니다. ProviderContainer를 생성하고 필요한 Provider들을 읽어서 초기화합니다.

모든 준비가 끝나면 UncontrolledProviderScope로 앱을 시작합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 금융 앱을 개발한다고 가정해봅시다. 앱이 시작되기 전에 보안 키를 로드하고, 인증서를 검증하고, 로컬 데이터베이스를 초기화하고, 사용자 세션을 복원해야 합니다.

main 함수에서 이 모든 작업을 순차적으로 수행한 후, 준비가 완료되면 로그인 화면이나 홈 화면을 보여줍니다. 실제로 많은 금융 앱에서 이런 패턴을 사용하여 안전하고 안정적인 앱 시작을 보장합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 초기화 시간이 너무 오래 걸리게 만드는 것입니다.

사용자는 앱을 실행한 후 빈 화면만 보게 됩니다. 따라서 필수적인 초기화만 main 함수에서 수행하고, 나머지는 지연 로딩을 활용하는 것이 좋습니다.

스플래시 화면을 보여주면서 초기화를 진행하는 것도 좋은 방법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

체계적인 초기화 패턴을 도입한 후 팀원들의 반응이 좋았습니다. "이제 앱 시작 로직이 정말 명확해졌어요!" main 함수 초기화 패턴을 제대로 이해하면 복잡한 부트스트랩 로직도 깔끔하게 관리할 수 있습니다.

여러분도 오늘 배운 내용을 프로젝트에 적용하여 안정적인 앱 시작을 구현해 보세요.

실전 팁

💡 - WidgetsFlutterBinding.ensureInitialized는 반드시 호출하세요

  • 필수 초기화만 main 함수에서 수행하고 나머지는 지연 로딩하세요
  • 스플래시 화면을 활용하면 사용자 경험이 개선됩니다

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

#Flutter#Riverpod#ProviderScope#DependencyInjection#StateManagement#Flutter,Riverpod

댓글 (0)

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

함께 보면 좋은 카드 뉴스