본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 11. · 11 Views
Riverpod Generator로 보일러플레이트 제거 완벽 가이드
Riverpod의 코드 생성 기능을 활용하여 반복적인 보일러플레이트 코드를 제거하는 방법을 배웁니다. @riverpod 애노테이션을 사용한 간결한 Provider 작성법과 기존 코드 마이그레이션 전략을 실무 중심으로 알아봅니다.
목차
- riverpod_generator 설치
- @riverpod 함수형 Provider
- @riverpod 클래스형 Notifier
- build_runner 실행
- 생성된 코드 분석
- 기존 코드 마이그레이션
1. riverpod generator 설치
김개발 씨는 Flutter 프로젝트에서 Riverpod을 사용하고 있었습니다. Provider를 만들 때마다 StateNotifier, StateNotifierProvider, 제네릭 타입 등 반복적으로 작성해야 하는 코드가 너무 많았습니다.
"더 간단한 방법은 없을까요?" 선배에게 물어보니 riverpod_generator를 추천받았습니다.
riverpod_generator는 코드 생성을 통해 Provider 작성 시 필요한 보일러플레이트 코드를 자동으로 만들어주는 패키지입니다. @riverpod 애노테이션만 붙이면 build_runner가 필요한 모든 코드를 생성해줍니다.
개발자는 핵심 로직에만 집중할 수 있게 됩니다.
다음 코드를 살펴봅시다.
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^2.3.0
# 코드 생성에 필요한 패키지들
custom_lint: ^0.5.0
riverpod_lint: ^2.3.0
김개발 씨는 Flutter 프로젝트에 Riverpod을 도입한 지 한 달째입니다. 처음에는 Provider 패턴이 신기하고 좋았는데, 시간이 지날수록 불편한 점이 생겼습니다.
새로운 Provider를 만들 때마다 StateNotifier를 상속받고, StateNotifierProvider를 선언하고, 제네릭 타입을 맞추고... 이런 반복적인 작업이 지루했습니다.
"분명히 더 나은 방법이 있을 텐데" 하고 중얼거리던 차에 선배 개발자 박시니어 씨가 다가왔습니다. "김 개발님, riverpod_generator 써보셨어요?" 박시니어 씨가 물었습니다.
김개발 씨는 고개를 저었습니다. "그게 뭔데요?" riverpod_generator는 무엇일까요?
쉽게 비유하자면, riverpod_generator는 마치 자동 번역기와 같습니다. 우리가 간단한 한국어 문장을 입력하면 자동으로 완벽한 영어 문장으로 번역해주듯이, 우리가 간단한 함수나 클래스를 작성하면 자동으로 완벽한 Provider 코드를 생성해줍니다.
riverpod_generator가 없던 시절에는 어땠을까요? 개발자들은 Provider를 만들 때마다 복잡한 타입 선언을 직접 작성해야 했습니다.
StateNotifier를 상속받고, StateNotifierProvider를 만들고, family나 autoDispose 같은 수식어를 정확한 위치에 배치해야 했습니다. 실수하기도 쉬웠고, 타입 에러가 자주 발생했습니다.
더 큰 문제는 코드의 일관성이었습니다. 팀원마다 Provider를 작성하는 스타일이 달랐고, 리뷰할 때마다 "이건 왜 이렇게 작성했나요?"라는 질문이 반복되었습니다.
바로 이런 문제를 해결하기 위해 riverpod_generator가 등장했습니다. riverpod_generator를 사용하면 코드 생성을 통해 완벽한 Provider를 자동으로 만들 수 있습니다.
또한 팀 전체가 동일한 패턴으로 코드를 작성하게 되어 일관성이 높아집니다. 무엇보다 개발자가 핵심 비즈니스 로직에만 집중할 수 있다는 큰 이점이 있습니다.
설치 과정을 살펴보겠습니다. 먼저 pubspec.yaml 파일을 열어 필요한 패키지를 추가합니다.
riverpod_annotation은 @riverpod 애노테이션을 제공하는 패키지입니다. riverpod_generator는 실제 코드를 생성하는 도구이며, build_runner는 코드 생성 과정을 실행하는 역할을 합니다.
dependencies 섹션에는 실제 앱에서 사용할 패키지들을 추가합니다. dev_dependencies 섹션에는 개발 단계에서만 필요한 도구들을 추가합니다.
custom_lint와 riverpod_lint는 코드 품질을 높여주는 린트 도구입니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 Flutter 프로젝트에서 riverpod_generator는 필수적인 도구가 되었습니다. 특히 대규모 팀 프로젝트에서 코드 일관성을 유지하는 데 큰 도움이 됩니다.
많은 기업에서 Riverpod을 도입할 때 처음부터 riverpod_generator를 함께 설정합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 패키지 버전을 맞추지 않는 것입니다. riverpod_annotation과 riverpod_generator의 버전이 다르면 코드 생성 시 에러가 발생할 수 있습니다.
따라서 공식 문서를 확인하여 호환되는 버전을 사용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 터미널에 "flutter pub get"을 입력했습니다. "이제 어떻게 사용하는 건가요?" riverpod_generator를 제대로 설치하면 Provider 작성이 훨씬 쉬워집니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - riverpod_annotation과 riverpod_generator는 항상 같은 버전을 사용하세요
- build_runner는 최신 버전을 유지하면 코드 생성 속도가 빨라집니다
- custom_lint와 riverpod_lint를 함께 사용하면 코드 품질이 크게 향상됩니다
2. @riverpod 함수형 Provider
패키지 설치를 마친 김개발 씨는 첫 번째 Provider를 만들어보기로 했습니다. "기존 방식은 20줄이 넘었는데, 이건 정말 간단하네요!" 화면을 보던 김개발 씨가 감탄했습니다.
@riverpod 함수형 Provider는 일반 함수에 @riverpod 애노테이션을 붙여서 Provider를 생성하는 방식입니다. 함수 이름이 자동으로 Provider 이름이 되며, 반환 타입과 매개변수가 자동으로 인식됩니다.
가장 간단하고 직관적인 Provider 작성 방법입니다.
다음 코드를 살펴봅시다.
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 생성될 파일을 지정합니다
part 'user_provider.g.dart';
// 간단한 문자열을 반환하는 Provider
@riverpod
String userName(UserNameRef ref) {
return 'Kim Developer';
}
// 비동기 데이터를 가져오는 Provider
@riverpod
Future<User> fetchUser(FetchUserRef ref, int userId) async {
// API 호출 시뮬레이션
await Future.delayed(Duration(seconds: 1));
return User(id: userId, name: 'User $userId');
}
김개발 씨는 이전에 작성했던 Provider 코드를 다시 열어봤습니다. final userNameProvider = Provider<String>((ref) => 'Kim Developer'); 이렇게 한 줄로 끝나는 간단한 Provider였지만, 타입을 명시하고 괄호를 맞추는 게 번거로웠습니다.
"더 복잡한 Provider는 얼마나 길어질까?" 생각하다가 과거에 작성한 StateNotifierProvider 코드를 찾아봤습니다. 클래스 정의 10줄, Provider 선언 5줄, 총 15줄이 넘는 코드였습니다.
박시니어 씨가 화면을 가리키며 말했습니다. "이제 @riverpod를 사용해서 똑같은 기능을 만들어볼까요?" @riverpod 함수형 Provider는 어떻게 작동할까요?
쉽게 비유하자면, @riverpod는 마치 스마트폰의 자동 완성 기능과 같습니다. 우리가 "안녕"이라고 입력하면 "안녕하세요"를 자동으로 제안해주듯이, 우리가 간단한 함수를 작성하면 완전한 Provider 코드를 자동으로 생성해줍니다.
함수 이름, 반환 타입, 매개변수를 분석해서 최적의 Provider를 만들어냅니다. @riverpod가 없던 시절에는 어땠을까요?
Provider를 만들 때마다 제네릭 타입을 정확히 명시해야 했습니다. Provider<String>, FutureProvider<User>, StateNotifierProvider<CounterNotifier, int> 등 복잡한 타입 선언이 필요했습니다.
매개변수를 받는 Provider를 만들 때는 family 수식어를 추가해야 했고, 자동 해제가 필요하면 autoDispose도 붙여야 했습니다. 더 큰 문제는 타입 불일치 에러였습니다.
반환 타입과 Provider 타입이 맞지 않으면 컴파일 에러가 발생했고, 디버깅하는 데 시간이 오래 걸렸습니다. 바로 이런 문제를 해결하기 위해 @riverpod 함수형 Provider가 등장했습니다.
@riverpod를 사용하면 타입 추론이 자동으로 이루어집니다. 함수의 반환 타입을 보고 적절한 Provider를 생성하며, Future를 반환하면 자동으로 FutureProvider가 됩니다.
또한 매개변수가 있으면 자동으로 family Provider가 생성됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 part 지시문을 보면 생성될 파일의 이름을 지정합니다. user_provider.dart 파일이면 user_provider.g.dart가 생성됩니다.
이 부분이 핵심입니다. 다음으로 userName 함수를 보면 @riverpod 애노테이션이 붙어 있습니다.
이 함수는 String을 반환하므로, build_runner가 자동으로 userNameProvider라는 이름의 Provider<String>을 생성합니다. 함수 이름이 camelCase면 Provider 이름도 camelCase로 자동 변환됩니다.
fetchUser 함수에서는 userId라는 매개변수를 받습니다. 이 경우 build_runner가 자동으로 family Provider를 생성하며, ref.watch(fetchUserProvider(123)) 형태로 사용할 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 뉴스 앱을 개발한다고 가정해봅시다.
카테고리별 뉴스 목록을 가져오는 Provider가 필요합니다. @riverpod를 활용하면 간단한 함수 하나로 카테고리를 매개변수로 받는 Provider를 만들 수 있습니다.
기존 방식으로는 family, autoDispose, FutureProvider를 모두 수동으로 조합해야 했지만, 이제는 함수 하나면 충분합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 part 지시문을 빼먹는 것입니다. part 구문이 없으면 코드 생성이 제대로 이루어지지 않습니다.
따라서 항상 파일 상단에 part 지시문을 추가해야 합니다. 또 다른 실수는 함수 매개변수에 Ref를 빼먹는 것입니다.
@riverpod 함수의 첫 번째 매개변수는 반드시 Ref여야 합니다. Ref를 통해 다른 Provider를 읽거나 생명주기를 관리할 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 안내에 따라 코드를 작성한 김개발 씨는 만족스러운 표정을 지었습니다.
"와, 정말 간단하네요!" @riverpod 함수형 Provider를 제대로 이해하면 더 빠르고 안전하게 Provider를 작성할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 함수 이름은 명확하게 지으면 자동 생성되는 Provider 이름도 명확해집니다
- part 지시문은 항상 import 구문 다음에 위치시키세요
- Ref 매개변수는 함수의 첫 번째 매개변수로 반드시 포함해야 합니다
3. @riverpod 클래스형 Notifier
함수형 Provider를 익힌 김개발 씨는 다음 단계로 넘어갔습니다. "상태를 변경해야 하는 경우는 어떻게 하나요?" 박시니어 씨가 웃으며 대답했습니다.
"그럴 때는 클래스형 Notifier를 사용하죠."
@riverpod 클래스형 Notifier는 상태를 관리하고 변경할 수 있는 Provider를 만드는 방식입니다. 클래스에 @riverpod 애노테이션을 붙이고 build 메서드를 구현하면, 자동으로 NotifierProvider가 생성됩니다.
복잡한 상태 관리 로직을 깔끔하게 구현할 수 있습니다.
다음 코드를 살펴봅시다.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
// 동기 상태 관리 Notifier
@riverpod
class Counter extends _$Counter {
@override
int build() => 0; // 초기 상태
// 상태를 변경하는 메서드들
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
// 비동기 상태 관리 AsyncNotifier
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
// 초기 데이터 로드
return await fetchTodosFromApi();
}
Future<void> addTodo(String title) async {
state = AsyncLoading(); // 로딩 상태 표시
state = await AsyncValue.guard(() async {
final newTodo = await createTodoOnApi(title);
final currentList = await future;
return [...currentList, newTodo];
});
}
}
김개발 씨는 카운터 앱을 만들면서 고민에 빠졌습니다. 숫자를 증가시키고 감소시키는 기능이 필요한데, 함수형 Provider로는 상태를 변경할 수 없었습니다.
"어떻게 해야 하지?" 이전에는 StateNotifier를 상속받는 클래스를 만들고, StateNotifierProvider를 선언하고, 제네릭 타입을 두 개나 지정해야 했습니다. 코드가 길어지고 복잡해지는 게 불만이었습니다.
박시니어 씨가 다가와서 말했습니다. "상태를 변경해야 할 때는 클래스형 Notifier를 사용하세요.
훨씬 간단해요." @riverpod 클래스형 Notifier는 무엇일까요? 쉽게 비유하자면, 클래스형 Notifier는 마치 리모컨과 같습니다.
TV(상태)를 켜고 끄고, 채널을 바꾸고, 볼륨을 조절할 수 있는 버튼들(메서드)이 있는 리모컨처럼, Notifier도 상태를 읽고 변경할 수 있는 메서드들을 제공합니다. 사용자는 리모컨 버튼만 누르면 되고, 내부 동작은 신경 쓰지 않아도 됩니다.
클래스형 Notifier가 없던 시절에는 어땠을까요? StateNotifier를 직접 상속받아야 했습니다.
class CounterNotifier extends StateNotifier<int>처럼 제네릭 타입을 명시하고, 생성자에서 super(0)을 호출해서 초기 상태를 설정해야 했습니다. 그리고 StateNotifierProvider를 별도로 선언하면서 또 한 번 타입을 지정해야 했습니다.
더 큰 문제는 비동기 상태 관리였습니다. AsyncValue를 다루는 코드가 복잡했고, 로딩, 에러, 데이터 상태를 수동으로 관리해야 했습니다.
실수하기 쉬웠고, 코드 가독성도 떨어졌습니다. 바로 이런 문제를 해결하기 위해 @riverpod 클래스형 Notifier가 등장했습니다.
클래스형 Notifier를 사용하면 build 메서드 하나로 초기 상태를 정의할 수 있습니다. 또한 state 프로퍼티를 통해 현재 상태를 읽고 변경할 수 있어 직관적입니다.
무엇보다 비동기 상태 관리를 위한 AsyncNotifier가 자동으로 생성된다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 Counter 클래스를 보면 _$Counter를 상속받습니다. 이 클래스는 build_runner가 자동으로 생성하는 베이스 클래스입니다.
언더스코어와 달러 기호가 붙은 이름이 특이하지만, 이것이 코드 생성의 핵심 패턴입니다. 다음으로 build 메서드에서는 초기 상태를 반환합니다.
이 메서드가 딱 한 번 호출되어 상태를 초기화합니다. 반환 타입이 int이므로, build_runner가 자동으로 Notifier<int>를 생성합니다.
increment, decrement, reset 메서드들은 state를 직접 변경합니다. state는 베이스 클래스에서 제공하는 프로퍼티로, 현재 상태 값을 나타냅니다.
state를 변경하면 자동으로 리스너들에게 알림이 전달됩니다. TodoList 클래스는 비동기 상태를 다룹니다.
build 메서드가 Future를 반환하므로, 자동으로 AsyncNotifier<List<Todo>>가 생성됩니다. addTodo 메서드에서는 AsyncValue.guard를 사용해서 에러 처리를 안전하게 수행합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 앱의 장바구니 기능을 개발한다고 가정해봅시다.
상품 추가, 삭제, 수량 변경 등 다양한 상태 변경 작업이 필요합니다. 클래스형 Notifier를 활용하면 이런 메서드들을 깔끔하게 정리할 수 있고, 각 메서드는 하나의 책임만 가지게 됩니다.
많은 팀에서 이런 패턴을 선호합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 build 메서드 안에서 state를 변경하려는 것입니다. build 메서드는 초기 상태만 반환해야 하며, 상태 변경은 별도 메서드에서 해야 합니다.
build 메서드에서 state를 변경하면 무한 루프가 발생할 수 있습니다. 또 다른 실수는 클래스 이름과 파일 이름을 다르게 하는 것입니다.
Counter 클래스는 counter_provider.dart 파일에 있어야 하며, 그래야 counter_provider.g.dart가 올바르게 생성됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 직접 코드를 작성해봤습니다. "오, 정말 간단하네요!
이제 상태 관리가 두렵지 않아요." 클래스형 Notifier를 제대로 이해하면 복잡한 상태 관리도 쉽게 다룰 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - build 메서드는 초기 상태만 반환하고, 상태 변경은 다른 메서드에서 수행하세요
- 비동기 작업은 AsyncNotifier를 사용하면 자동으로 로딩, 에러, 데이터 상태가 관리됩니다
- 클래스 이름과 파일 이름을 일치시켜야 코드 생성이 올바르게 동작합니다
4. build runner 실행
코드 작성을 마친 김개발 씨는 궁금해졌습니다. "이제 어떻게 하면 되나요?" 박시니어 씨가 터미널을 열며 말했습니다.
"build_runner를 실행해야 실제 Provider 코드가 생성됩니다."
build_runner는 @riverpod 애노테이션이 붙은 코드를 분석해서 실제 Provider 코드를 자동으로 생성하는 도구입니다. dart run build_runner build 명령으로 일회성 생성을 하거나, watch 명령으로 파일 변경을 감지하여 자동 생성할 수 있습니다.
다음 코드를 살펴봅시다.
# 일회성 코드 생성 (한 번만 실행)
flutter pub run build_runner build
# 기존 생성 파일 삭제 후 재생성 (충돌 해결)
flutter pub run build_runner build --delete-conflicting-outputs
# 파일 변경 감지 및 자동 생성 (개발 중 추천)
flutter pub run build_runner watch
# 특정 파일만 생성 (선택적)
flutter pub run build_runner build --build-filter="lib/providers/**.dart"
# 생성된 파일 확인
# user_provider.g.dart 파일이 자동 생성됨
# 이 파일에는 userNameProvider, fetchUserProvider 등이 포함됨
김개발 씨는 @riverpod 애노테이션을 붙인 코드를 작성했지만, 아직 Provider가 실제로 생성되지 않았습니다. IDE에서는 빨간 줄이 표시되며 "userNameProvider를 찾을 수 없습니다"라는 에러가 나타났습니다.
"제가 뭘 잘못했나요?" 김개발 씨가 걱정스러운 표정으로 물었습니다. 박시니어 씨가 웃으며 대답했습니다.
"잘못한 게 아니에요. 아직 코드 생성을 안 해서 그래요." build_runner는 무엇일까요?
쉽게 비유하자면, build_runner는 마치 공장의 자동화 시스템과 같습니다. 우리가 설계도(@riverpod 코드)를 제공하면, 공장이 그 설계도를 분석해서 완성품(Provider 코드)을 자동으로 제작해줍니다.
사람이 직접 조립하는 것보다 훨씬 빠르고 정확합니다. build_runner가 없던 시절에는 어땠을까요?
개발자들은 모든 Provider 코드를 손으로 직접 작성해야 했습니다. 타입을 하나하나 명시하고, 제네릭을 맞추고, 실수가 없는지 계속 확인해야 했습니다.
코드를 수정할 때마다 관련된 모든 부분을 함께 수정해야 했고, 놓치는 부분이 생기면 런타임 에러가 발생했습니다. 더 큰 문제는 대규모 프로젝트였습니다.
Provider가 수십 개, 수백 개가 되면 일관성을 유지하기가 매우 어려웠습니다. 리팩토링할 때는 악몽이 시작되었습니다.
바로 이런 문제를 해결하기 위해 build_runner가 등장했습니다. build_runner를 사용하면 코드 생성이 자동화됩니다.
개발자는 핵심 로직만 작성하고, 나머지는 도구에게 맡기면 됩니다. 또한 watch 모드를 사용하면 파일을 저장하는 즉시 자동으로 코드가 재생성되어 개발 경험이 크게 향상됩니다.
무엇보다 일관된 코드 품질을 보장한다는 큰 이점이 있습니다. 명령어들을 하나씩 살펴보겠습니다.
먼저 기본 build 명령은 현재 프로젝트의 모든 코드 생성을 수행합니다. 이 명령을 실행하면 build_runner가 프로젝트를 스캔하고, @riverpod 애노테이션이 붙은 모든 파일을 찾아서 .g.dart 파일을 생성합니다.
--delete-conflicting-outputs 옵션은 매우 유용합니다. 때때로 이전에 생성된 파일과 새로 생성될 파일이 충돌하는 경우가 있는데, 이 옵션을 사용하면 충돌하는 파일을 자동으로 삭제하고 새로 생성합니다.
에러가 발생할 때 첫 번째로 시도해볼 해결 방법입니다. watch 명령이 가장 많이 사용됩니다.
이 명령을 실행하면 build_runner가 백그라운드에서 계속 실행되며, 파일이 변경될 때마다 자동으로 코드를 재생성합니다. 개발하는 동안 이 모드를 켜두면 매우 편리합니다.
--build-filter 옵션은 대규모 프로젝트에서 유용합니다. 전체 프로젝트를 스캔하는 대신 특정 폴더나 파일만 대상으로 지정할 수 있어 빌드 시간이 단축됩니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 개발자들은 프로젝트를 시작할 때 터미널을 하나 열어서 build_runner watch를 실행해둡니다.
그리고 개발이 끝날 때까지 그대로 둡니다. 코드를 수정할 때마다 자동으로 Provider가 재생성되므로, 별도로 신경 쓸 필요가 없습니다.
CI/CD 파이프라인에서는 build 명령을 사용합니다. 배포 전에 모든 코드 생성이 완료되도록 스크립트에 포함시킵니다.
이렇게 하면 생성된 파일이 누락되어 발생하는 문제를 방지할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 .g.dart 파일을 git에 커밋하지 않는 것입니다. 생성된 파일이므로 .gitignore에 추가해야 한다고 생각하지만, 실제로는 커밋하는 것이 권장됩니다.
다른 팀원이 프로젝트를 클론했을 때 바로 빌드할 수 있기 때문입니다. 또 다른 실수는 build_runner를 종료하지 않고 pub get을 실행하는 것입니다.
watch 모드가 실행 중일 때 패키지를 업데이트하면 충돌이 발생할 수 있습니다. 따라서 패키지를 업데이트할 때는 build_runner를 먼저 종료해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 안내에 따라 터미널에 "flutter pub run build_runner watch"를 입력한 김개발 씨는 화면을 주시했습니다.
잠시 후 "Succeeded after 3.2s with 5 outputs"라는 메시지가 나타났습니다. IDE의 빨간 줄이 사라졌습니다!
build_runner를 제대로 활용하면 개발 생산성이 크게 향상됩니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 개발 중에는 watch 모드를 켜두고, 배포 전에는 build 명령으로 최종 생성하세요
- .g.dart 파일은 git에 커밋하는 것이 팀 협업에 유리합니다
- 충돌 에러가 발생하면 --delete-conflicting-outputs 옵션을 사용하세요
5. 생성된 코드 분석
코드 생성이 완료된 후, 김개발 씨는 호기심이 생겼습니다. "생성된 파일을 열어봐도 될까요?" 박시니어 씨가 고개를 끄덕였습니다.
"물론이죠. 어떤 코드가 생성되는지 이해하면 더 잘 활용할 수 있어요."
생성된 코드는 .g.dart 파일에 저장되며, @riverpod 애노테이션을 분석하여 완전한 Provider와 Notifier 코드를 포함합니다. 타입 안전성이 보장되고, family, autoDispose 등의 기능이 자동으로 적용됩니다.
직접 수정하면 안 되지만, 읽어보면 Riverpod의 내부 동작을 이해할 수 있습니다.
다음 코드를 살펴봅시다.
// user_provider.g.dart (자동 생성된 파일)
// userName 함수에서 생성된 Provider
final userNameProvider = Provider<String>((ref) {
return userName(ref);
});
// fetchUser 함수에서 생성된 Family Provider
final fetchUserProvider = FutureProvider.family<User, int>((ref, userId) {
return fetchUser(ref, userId);
});
// Counter 클래스에서 생성된 NotifierProvider
final counterProvider = NotifierProvider<Counter, int>(() {
return Counter();
});
// TodoList 클래스에서 생성된 AsyncNotifierProvider
final todoListProvider = AsyncNotifierProvider<TodoList, List<Todo>>(() {
return TodoList();
});
// 자동 dispose 기능도 함께 생성됨 (조건부)
// 자동으로 keepAlive 관리 코드도 포함됨
김개발 씨는 user_provider.g.dart 파일을 열어봤습니다. 파일 상단에는 "GENERATED CODE - DO NOT MODIFY BY HAND"라는 경고 문구가 있었습니다.
스크롤을 내리니 자신이 작성한 간단한 함수가 완전한 Provider로 변환되어 있었습니다. "와, 이렇게 많은 코드가 생성되다니!" 김개발 씨가 감탄했습니다.
단 5줄의 함수가 수십 줄의 완벽한 Provider 코드로 확장되었습니다. 박시니어 씨가 설명을 시작했습니다.
"생성된 코드를 이해하면 Riverpod이 내부적으로 어떻게 동작하는지 알 수 있어요." 생성된 코드는 어떤 구조일까요? 쉽게 비유하자면, 생성된 코드는 마치 건축 도면과 완성된 건물의 관계와 같습니다.
우리가 작성한 간단한 도면(@riverpod 코드)을 바탕으로, 실제로 사용할 수 있는 완전한 건물(Provider)이 세워집니다. 전기 배선, 배관, 내부 마감까지 모두 포함된 완성품입니다.
자동 코드 생성이 없던 시절에는 어땠을까요? 개발자들은 이 모든 코드를 직접 작성해야 했습니다.
Provider를 선언하고, 제네릭 타입을 지정하고, family를 추가하고, autoDispose를 설정하고... 반복적이고 지루한 작업이었습니다.
한 글자라도 틀리면 컴파일 에러가 발생했고, 디버깅에 시간을 낭비했습니다. 더 큰 문제는 유지보수였습니다.
함수 시그니처를 변경하면 Provider 선언도 함께 수정해야 했고, 놓치는 부분이 있으면 런타임 에러로 이어졌습니다. 바로 이런 문제를 해결하기 위해 자동 코드 생성이 도입되었습니다.
코드 생성을 사용하면 타입 안전성이 완벽하게 보장됩니다. 함수의 반환 타입과 Provider의 타입이 자동으로 일치하며, 컴파일러가 모든 타입 체크를 수행합니다.
또한 매개변수가 있으면 자동으로 family Provider가 생성되어 별도 작업이 필요 없습니다. 무엇보다 코드 변경 시 자동으로 재생성되므로 동기화 문제가 발생하지 않습니다.
생성된 코드를 하나씩 살펴보겠습니다. 먼저 userNameProvider를 보면 일반 Provider<String>으로 생성되었습니다.
우리가 작성한 userName 함수를 호출하는 간단한 형태입니다. 제네릭 타입이 정확하게 String으로 지정되어 타입 안전성이 보장됩니다.
다음으로 fetchUserProvider는 FutureProvider.family로 생성되었습니다. userId 매개변수가 있었기 때문에 자동으로 family가 추가되었습니다.
제네릭 타입은 <User, int>로, User는 반환 타입이고 int는 매개변수 타입입니다. counterProvider를 보면 NotifierProvider로 생성되었습니다.
Counter 클래스가 _$Counter를 상속받았고, build 메서드가 int를 반환했기 때문에 NotifierProvider<Counter, int>가 만들어졌습니다. 이 Provider를 통해 상태를 읽고 메서드를 호출할 수 있습니다.
todoListProvider는 AsyncNotifierProvider입니다. build 메서드가 Future를 반환했기 때문에 비동기 버전이 생성되었습니다.
로딩, 에러, 데이터 상태를 자동으로 관리하는 코드가 포함되어 있습니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 개발자들은 생성된 파일을 직접 열어보지 않습니다. 필요 없기 때문입니다.
하지만 문제가 발생했을 때나 고급 기능을 사용할 때는 생성된 코드를 읽어보면 도움이 됩니다. 예를 들어 "왜 이 Provider는 autoDispose가 안 되지?"라는 의문이 들 때, 생성된 코드를 보면 답을 찾을 수 있습니다.
또한 팀 내에서 코드 리뷰를 할 때 생성된 파일을 함께 검토하면, @riverpod 사용법이 올바른지 확인할 수 있습니다. 예상과 다른 코드가 생성되었다면, 원본 코드를 수정해야 한다는 신호입니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 생성된 파일을 직접 수정하는 것입니다.
"GENERATED CODE - DO NOT MODIFY BY HAND"라는 경고를 무시하고 코드를 변경하면, 다음 빌드 때 모든 수정이 사라집니다. 변경이 필요하다면 원본 파일(@riverpod 코드)을 수정해야 합니다.
또 다른 실수는 생성된 파일을 git에서 제외하는 것입니다. 앞서 언급했듯이, .g.dart 파일은 커밋하는 것이 좋습니다.
다른 팀원이 코드를 받았을 때 build_runner를 실행하지 않아도 바로 작업할 수 있기 때문입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
생성된 코드를 읽어본 김개발 씨는 Riverpod의 작동 원리를 더 깊이 이해하게 되었습니다. "이제 왜 @riverpod가 편한지 확실히 알겠어요!" 생성된 코드를 제대로 이해하면 Riverpod을 더 효과적으로 활용할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 생성된 파일은 읽기만 하고 절대 수정하지 마세요
- .g.dart 파일을 git에 커밋하면 팀 협업이 원활해집니다
- 예상과 다른 코드가 생성되면 원본 @riverpod 코드를 확인하세요
6. 기존 코드 마이그레이션
마지막으로 김개발 씨는 고민에 빠졌습니다. "이미 작성한 기존 Provider들은 어떻게 하죠?" 박시니어 씨가 미소를 지으며 대답했습니다.
"단계별로 마이그레이션하면 됩니다. 생각보다 어렵지 않아요."
기존 코드 마이그레이션은 수동으로 작성한 Provider를 @riverpod 방식으로 전환하는 과정입니다. 한 번에 모든 코드를 바꾸지 않고, 파일 단위나 기능 단위로 점진적으로 전환하는 것이 안전합니다.
기존 코드와 새 코드가 공존할 수 있어 리스크가 적습니다.
다음 코드를 살펴봅시다.
// 기존 코드 (수동 작성)
final userNameProvider = Provider<String>((ref) {
return 'Kim Developer';
});
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// 마이그레이션 후 (@riverpod 사용)
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
@riverpod
String userName(UserNameRef ref) {
return 'Kim Developer';
}
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
// 사용하는 코드는 거의 동일
// ref.watch(userNameProvider) - 변경 없음
// ref.read(counterProvider.notifier).increment() - 변경 없음
김개발 씨의 프로젝트에는 이미 수십 개의 Provider가 있었습니다. 모두 수동으로 작성한 코드였고, StateNotifier를 사용하는 복잡한 Provider도 많았습니다.
"이걸 다 바꿔야 하나요?" 박시니어 씨가 안심시켰습니다. "한 번에 다 바꿀 필요는 없어요.
새로 작성하는 코드부터 @riverpod를 사용하고, 기존 코드는 여유가 있을 때 조금씩 전환하면 됩니다." 기존 코드 마이그레이션은 어떻게 진행할까요? 쉽게 비유하자면, 마이그레이션은 마치 오래된 건물을 리모델링하는 것과 같습니다.
건물 전체를 한 번에 부수고 새로 짓는 것이 아니라, 한 층씩 또는 한 방씩 순차적으로 개선해나갑니다. 다른 층은 그대로 사용하면서 작업하므로, 전체 서비스를 중단할 필요가 없습니다.
마이그레이션 전략이 없던 시절에는 어땠을까요? 새로운 패턴이나 라이브러리가 나오면, 개발자들은 큰 결단을 내려야 했습니다.
전체 코드를 한 번에 리팩토링할지, 아니면 새로운 기술을 포기할지 선택해야 했습니다. 한 번에 리팩토링하면 위험부담이 컸고, 포기하면 기술 부채가 쌓였습니다.
더 큰 문제는 팀 협업이었습니다. 누군가 리팩토링을 시작하면 다른 팀원의 작업과 충돌이 발생했고, 병합할 때마다 문제가 생겼습니다.
바로 이런 문제를 해결하기 위해 점진적 마이그레이션 전략이 등장했습니다. 점진적 마이그레이션을 사용하면 위험을 최소화할 수 있습니다.
작은 단위로 전환하고 테스트하므로, 문제가 발생해도 빠르게 롤백할 수 있습니다. 또한 팀원들이 새로운 패턴에 적응할 시간을 가질 수 있어 학습 곡선이 완만해집니다.
무엇보다 서비스 중단 없이 개선할 수 있다는 큰 이점이 있습니다. 마이그레이션 과정을 단계별로 살펴보겠습니다.
첫 번째 단계는 간단한 Provider부터 시작하는 것입니다. 위 코드에서 userNameProvider는 단순히 문자열을 반환하는 Provider입니다.
이런 간단한 것부터 @riverpod로 전환하면서 익숙해집니다. 두 번째 단계는 StateNotifier를 클래스형 Notifier로 전환하는 것입니다.
CounterNotifier를 보면 StateNotifier를 상속받고 있습니다. 이것을 @riverpod 클래스로 바꾸면, 생성자에서 super(0)를 호출하던 부분이 build 메서드로 변경됩니다.
나머지 메서드들은 그대로 유지됩니다. 세 번째 단계는 사용하는 코드를 확인하는 것입니다.
놀랍게도 대부분의 경우 변경이 필요 없습니다. ref.watch(counterProvider)는 그대로 동작하며, ref.read(counterProvider.notifier).increment()도 똑같이 사용할 수 있습니다.
네 번째 단계는 build_runner를 실행하는 것입니다. watch 모드가 켜져 있다면 자동으로 코드가 생성됩니다.
생성된 Provider 이름이 기존과 동일한지 확인합니다. 마지막 단계는 테스트입니다.
마이그레이션한 Provider를 사용하는 모든 화면과 기능을 테스트합니다. 문제가 없으면 다음 Provider로 넘어갑니다.
실제 현업에서는 어떻게 활용할까요? 많은 팀에서 "새 코드는 @riverpod, 기존 코드는 유지" 정책을 사용합니다.
신규 기능을 개발할 때는 무조건 @riverpod를 사용하고, 기존 코드는 버그 수정이나 기능 개선 시에만 전환합니다. 이렇게 하면 자연스럽게 점진적 마이그레이션이 진행됩니다.
또 다른 전략은 "스프린트 당 5개씩" 같은 목표를 정하는 것입니다. 매 스프린트마다 5개의 Provider를 전환하기로 팀이 합의하면, 몇 달 안에 전체 마이그레이션이 완료됩니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 한 번에 너무 많은 코드를 바꾸는 것입니다.
욕심내서 수십 개의 Provider를 한꺼번에 전환하면 디버깅이 어려워집니다. 문제가 발생했을 때 어느 부분이 원인인지 찾기 힘들기 때문입니다.
또 다른 실수는 테스트를 건너뛰는 것입니다. "간단한 변경이니까 괜찮겠지" 하고 넘어가면, 나중에 프로덕션에서 문제가 발생할 수 있습니다.
마이그레이션 후에는 반드시 철저한 테스트가 필요합니다. 마지막 주의사항은 팀 커뮤니케이션입니다.
혼자서 조용히 마이그레이션을 진행하면, 다른 팀원이 같은 파일을 수정할 때 충돌이 발생합니다. 마이그레이션 계획을 팀과 공유하고, 진행 상황을 투명하게 알려야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언에 따라 김개발 씨는 가장 간단한 Provider 하나를 골라서 마이그레이션해봤습니다.
5분도 안 걸렸고, 테스트도 모두 통과했습니다. "생각보다 쉽네요!" 다음 날부터 김개발 씨는 매일 3-5개씩 Provider를 전환했습니다.
한 달 후, 프로젝트의 모든 Provider가 @riverpod 방식으로 깔끔하게 정리되었습니다. 코드 리뷰 시간도 줄어들고, 신입 개발자들도 "이해하기 쉽다"고 좋아했습니다.
기존 코드 마이그레이션을 제대로 진행하면 기술 부채를 줄이고 코드 품질을 높일 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 한 번에 5-10개씩 작은 단위로 마이그레이션하세요
- 간단한 Provider부터 시작해서 복잡한 것으로 진행하세요
- 마이그레이션 후에는 반드시 테스트를 수행하세요
- 팀과 마이그레이션 계획을 공유하고 진행 상황을 투명하게 알리세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
LangGraph Fault Tolerance 장애 복구 완벽 가이드
LangGraph 애플리케이션의 장애 복구 메커니즘을 실무 중심으로 배웁니다. Durable Execution부터 Graph Migrations까지 체크포인트 기반 복구 시스템을 스토리텔링으로 쉽게 이해할 수 있습니다.
LangGraph Time Travel 완벽 가이드
LangGraph의 Time Travel 기능으로 특정 시점으로 돌아가고, 상태를 포크하여 분기하고, 대안 경로를 탐색하는 방법을 배웁니다. 실무에서 디버깅과 실험에 활용하는 실전 가이드입니다.
LangGraph Persistence 완벽 가이드
LangGraph의 Persistence 기능을 활용하여 대화 상태를 저장하고 관리하는 방법을 배웁니다. Thread와 Checkpoint를 이해하고, 상태 조회 및 수정 방법을 실무 중심으로 학습합니다.
Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드
Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.
Riverpod 3.0 Retry 자동 재시도 완벽 가이드
Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.