주요 콘텐츠로 건너뛰기

코드 생성(Code generation)에 대한 정보

코드 생성은 도구를 사용하여 코드를 생성하는 것을 말합니다. Dart에서는 애플리케이션을 'Compile'하는 데 추가 단계가 필요하다는 단점이 있습니다. 이 문제는 Dart 팀이 이 문제를 해결하려는 잠재적인 해결방안을 연구하고 있기 때문에 곧 해결될 수도 있습니다.

Riverpod 환경(Context)에서의 코드 생성은 "provider"를 선언하는 문법을 약간 변경하는 것을 의미합니다:

final fetchUserProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
});

코드 생성을 사용하면 다음과 같이 작성할 수 있습니다:


Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
}

Riverpod 사용 시 코드 생성은 완전히 선택 사항입니다. 코드 생성을 하지 않고도 Riverpod을 사용할 수 있습니다. 동시에 Riverpod은 코드 생성을 수용하며 사용을 권장합니다.

Riverpod의 코드 생성을 사용하려면, 시작하기 페이지를 참조하십시오. 문서의 사이드바에서 코드 생성(Code Generation)을 활성화해야 합니다.

코드 생성을 사용해야 하나요?

코드 생성은 Riverpod에서 선택 사항입니다. 이를 염두에 두고, 코드 생성을 사용해야 할지 말지 궁금할 수 있습니다.

답변은 대부분의 경우 예 입니다.
코드 생성을 사용하는 것이 Riverpod을 사용하는 권장 방법입니다. 이는 보다 미래 지향적인 접근방식(future-proof approach)이며, Riverpod의 잠재력을 최대한 활용할 수 있게 해줍니다.
동시에 많은 애플리케이션에서 이미 Freezedjson_serializable와 같은 패키지를 사용하여 코드 생성을 사용하고 있습니다. 이 경우, 프로젝트가 이미 코드 생성을 위해 설정되어 있으며, Riverpod을 사용하는 것은 간단합니다.

현재 코드 생성은 build_runner를 많은 사람이 싫어하기 때문에 선택 사항입니다. 하지만, Static Metaprogramming이 Dart에서 사용 가능해지면, build_runner는 더 이상 문제가 되지 않을 것입니다. 그 때부터는 Riverpod에 코드 생성 문법만 유일한 문법이 될 것입니다.

build_runner를 사용하는 것이 거부감이 든다면, 코드 생성을 사용하지 않는 것을 고려할 수 있습니다. 하지만, 이 경우에는 일부 기능을 사용할 수 없으며, 미래에 코드 생성으로 마이그레이션해야 합니다. 이 때 Riverpod은 마이그레이션을 가능한 한 원활하게 진행할 수 있도록 마이그레이션 도구를 제공할 것입니다.

코드 생성을 사용하면 어떤 이점이 있나요?

아마 궁금하실 겁니다: "Riverpod에서 코드 생성은 선택 사항이면, 왜 사용해야 하지?"

패키지는 언제나 그렇듯: 사용자의 삶을 더 쉽게 만들기 위한 것입니다. 이는 다음 내용이 포함되지많 여기에만 국한되지는 않습니다:

  • 더 나은 문법, 더 읽기 쉽고 유연하며, 학습 곡선이 낮습니다.
    • provider의 타입을 직접 지정할 필요가 없습니다. 로직을 작성하고, Riverpod이 가장 적합한 provider를 선택합니다.
    • 문법이 "더러운 전역 변수(dirty global variable)"를 정의하는 것처럼 보이지 않습니다. 대신, 사용자 정의 함수/클래스를 정의합니다.
    • provider에 매개변수를 전달하는 것이 더 이상 제한되지 않습니다. .family를 사용하여 단일 위치 매개변수(single positional parameter)를 전달하는 것이 아니라, 모든 매개변수를 전달할 수 있습니다. 이는 명명된 매개변수(named parameters), 선택적(optional) 매개변수, 기본값(default values)도 포함합니다.
  • Riverpod에서 작성한 코드는 상태를 가지는 핫 리로드(stateful hot-reload)를 지원합니다.
  • 더 나은 디버깅을 위해 추가 메타데이터를 생성하고 디버거가 수집합니다.
  • 일부 Riverpod 기능은 코드 생성을 통해서만 사용할 수 있습니다.

문법

provider 정의하기:

코드 생성을 사용하여 provider를 정의할 때 아래 사항을 알아 두면 도움이 됩니다:

  • provider는 어노테이션 function 또는 어노테이션 class로 정의할 수 있습니다. 둘은 거의 동일하지만 class 기반 provider는 외부 객체가 provider의 상태를 수정할 수 있는 공용 메서드(Public Method)를 포함할 수 있다는 장점이 있습니다(부수적인 효과). 함수형 provider는 빌드 메서드만으로 클래스 기반 provider를 작성하기 위한 달콤한 문법(Sugar syntax)이며, 따라서 UI에서 수정할 수 없습니다.
  • 모든 Dart async primitives(Future, FutureOr, Stream)가 지원됩니다.
  • 함수가 async로 표시되면, provider는 오류(errors)/로딩(loading) 상태를 자동으로 처리하고 AsyncValue를 노출합니다.
Functional
(공개(Public) 메소드로
부가작업(Side-Effects)을 처리할 수 없습니다)
Class-Based
(공개(Public) 메소드로
부가작업(Side-Effects)을 처리할 수 있습니다)
Sync

String example(ExampleRef ref) {
return 'foo';
}

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
Async - Future

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
Async - Stream

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}

자동폐기(autoDispose) 활성화/비활성화:

코드 생성을 사용할때, provider는 기본적으로 autoDispose가 활성화됩니다. 코드 생성을 사용할 때 공급자는 기본적으로 자동 폐기됩니다. 즉, 연결(ref.watch/ref.listen)된 리스너가 없을 때 자동으로 폐기됩니다. 이 기본 설정은 Riverpod의 철학에 더 잘 부합합니다. 코드 생성하지 않는 버전을 사용하면, package:provider에서 마이그레이션하는 사용자를 위해 자동폐기(autoDispose) 기능이 기본적으로 꺼져 있었습니다.

자동폐기(autoDispose)를 비활성화하려면, autoDispose: false를 어노테이션에 전달하면 됩니다.

// AutoDispose provider (keepAlive is false by default)

String example1(Example1Ref ref) => 'foo';

// Non autoDispose provider
(keepAlive: true)
String example2(Example2Ref ref) => 'foo';

provider에 매개변수 전달하기 (family):

코드 생성을 사용할 때, provider에 매개변수를 전달하기위해 더 이상 family 수정자(modifier)를 사용할 필요가 없습니다. 대신 provider의 메인 함수(main function)는 명명된 매개변수(named parameters), 선택적 매개변수(optional parameters), 기본값(default values)을 포함하여 모든 매개변수를 받을 수 있습니다. 그러나 이 매개변수들은 여전히 일관된 ==를 가져야 한다는 점에 유의하세요. 즉, 값이 캐시되거나 매개변수가 ==를 재정의해야 한다는 의미입니다.

FunctionalClass-Based

String example(
ExampleRef ref,
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

class Example extends _$Example {

String build(
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

// Add methods to mutate the state
}

코드 생성을 사용하지 않는 방식(non-code-generation variant)에서 마이그레이션하기:

코드 생성 방식을 사용하지 않는 경우에는 provider의 타입을 직접 결정해야 합니다. 다음은 코드 생성 방식으로 전환하기 위한 해당 옵션입니다:

Provider
Before
final exampleProvider = Provider.autoDispose<String>(
(ref) {
return 'foo';
},
);
After

String example(ExampleRef ref) {
return 'foo';
}
NotifierProvider
Before
final exampleProvider = NotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeNotifier<String> {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
FutureProvider
Before
final exampleProvider =
FutureProvider.autoDispose<String>((ref) async {
return Future.value('foo');
});
After

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}
StreamProvider
Before
final exampleProvider =
StreamProvider.autoDispose<String>((ref) async* {
yield 'foo';
});
After

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}
AsyncNotifierProvider
Before
final exampleProvider =
AsyncNotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeAsyncNotifier<String> {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
StreamNotifierProvider
Before
final exampleProvider =
StreamNotifierProvider.autoDispose<ExampleNotifier, String>(() {
return ExampleNotifier();
});

class ExampleNotifier extends AutoDisposeStreamNotifier<String> {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}