코드 생성에 대하여
코드 생성이란 도구를 사용해 코드를 대신 만들어 내는 방식을 말합니다. Dart에서는 애플리케이션을 "컴파일"하기 위해 추가 단계가 필요하다는 단점이 있습니다. 다만 Dart 팀이 이 문제의 해결책을 연구하고 있으므로, 가까운 미래에 해결될 수도 있습니다.
Riverpod에서 코드 생성이란 "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(Ref ref, {required int userId}) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
}
Riverpod에서 코드 생성은 전적으로 선택 사항입니다. 코드 생성 없이도 Riverpod을 얼마든지 사용할 수 있습니다. 그러면서도 Riverpod은 코드 생성을 적극 지원하며, 사용을 권장합니다.
Riverpod의 코드 생성기를 설치하고 사용하는 방법은 시작하기 페이지를 참고하세요. 문서 사이드바에서 코드 생성을 활성화하는 것도 잊지 마세요.
코드 생성을 사용해야 할까요?
Riverpod에서 코드 생성은 선택 사항입니다. 그렇다면 코드 생성을 써야 할지 말아야 할지 궁금할 수 있습니다.
답은 이미 다른 용도로 코드 생성을 사용하고 있다면 쓰세요입니다. (Freezed, json_serializable 등) Dart 팀이 "macros"라는 기능을 개발하던 시기에는 코드 생성이 Riverpod을 사용하는 권장 방식이었습니다. 하지만 안타깝게도 macros는 취소되었습니다.
코드 생성은 여러 이점이 있지만, 현재로서는 여전히 꽤 느립니다. Dart 팀이 코드 생성 성능을 개선하고 있지만, 언제 적용될지, 얼마나 나아질지는 아직 알 수 없습니다. 따라서 프로젝트에서 아직 코드 생성을 사용하지 않고 있다면, Riverpod만을 위해 코드 생성을 도입할 가치는 크지 않을 것입니다.
한편 많은 애플리케이션이 이미 Freezed나 json_serializable 같은 패키지로 코드 생성을 사용하고 있습니다. 이런 경우라면 프로젝트에 코드 생성 환경이 이미 갖춰져 있을 가능성이 높으므로, Riverpod에서도 쉽게 사용할 수 있습니다.
코드 생성을 사용하면 어떤 이점이 있나요?
"Riverpod에서 코드 생성이 선택 사항이라면, 굳이 왜 써야 하지?"라는 의문이 들 수 있습니다.
여느 패키지와 마찬가지로, 개발을 더 편하게 하기 위해서입니다. 예를 들면 다음과 같은 이점이 있습니다.
- 더 읽기 쉽고 유연하며, 배우기도 쉬운 문법
- provider의 종류를 고민할 필요가 없습니다. 로직만 작성하면 Riverpod이 가장 적합한 provider를 골라 줍니다.
- 더 이상 "지저분한 전역 변수"를 정의하는 것처럼 보이지 않습니다. 대신 사용자 정의 함수/클래스를 정의합니다.
- provider에 매개변수를 제한 없이 전달할 수 있습니다. Family를 사용해 위치 매개변수 하나만 전달하는 대신, 어떤 매개변수든 전달할 수 있습니다. 이름 있는 매개변수, 선택적 매개변수는 물론 기본값까지 사용할 수 있습니다.
- Riverpod으로 작성한 코드의 상태를 유지하는 hot-reload(stateful hot-reload)
- 디버거가 활용하는 추가 메타데이터를 생성하므로 디버깅이 더 쉬워집니다.
문법
provider 정의하기:
코드 생성으로 provider를 정의할 때는 다음 사항을 기억해 두면 좋습니다.
- provider는 어노테이션을 붙인 함수나
어노테이션을 붙인 클래스로 정의할 수 있습니다. 둘은 거의 같지만,
클래스 기반 provider는 외부 객체가 provider의 상태를 변경(부수 효과)할 수 있도록
public 메서드를 둘 수 있다는 장점이 있습니다. 함수형 provider는
build메서드만 있는 클래스 기반 provider를 간단히 쓰기 위한 문법적 설탕이므로, UI에서 상태를 변경할 수 없습니다. - Dart의 모든 비동기 기본 타입(Future, FutureOr, Stream)을 지원합니다.
- 함수에 async를 붙이면 provider가 오류/로딩 상태를 자동으로 처리하고 AsyncValue를 노출합니다.
| 함수형 (public 메서드로 부수 효과를 수행할 수 없음) | 클래스 기반 (public 메서드로 부수 효과를 수행할 수 있음) | |
|---|---|---|
| 동기 | | |
| 비동기 - Future | | |
| 비동기 - Stream | | |
autoDispose 활성화/비활성화:
코드 생성을 사용하면 provider는 기본적으로 autoDispose입니다. 즉, 연결된 리스너(ref.watch/ref.listen)가
하나도 남지 않으면 스스로 폐기됩니다.
이 기본 설정이 Riverpod의 철학에 더 잘 맞습니다. 처음에 코드 생성을 사용하지 않는 방식에서는
package:provider에서 마이그레이션하는 사용자를 배려해 autoDispose가 기본적으로 꺼져 있었습니다.
autoDispose를 끄고 싶다면 어노테이션에 keepAlive: true를 전달하면 됩니다.
// AutoDispose provider (keepAlive is false by default)
String example1(Ref ref) => 'foo';
// Non autoDispose provider
(keepAlive: true)
String example2(Ref ref) => 'foo';
provider에 매개변수 전달하기(family):
코드 생성을 사용하면 provider에 매개변수를 전달하기 위해 더 이상 family 수식자에 의존할 필요가 없습니다.
대신 provider의 메인 함수가 이름 있는 매개변수, 선택적 매개변수, 기본값을 포함해 원하는 만큼 매개변수를 받을 수 있습니다.
다만 이 매개변수들은 여전히 일관된 ==를 가져야 한다는 점에 유의하세요.
즉, 값을 캐시하거나 매개변수가 ==를 오버라이드해야 합니다.
| 함수형 | 클래스 기반 |
|---|---|
| |
코드 생성을 사용하지 않는 방식에서 마이그레이션하기:
코드 생성을 사용하지 않는 방식에서는 provider의 종류를 직접 정해야 합니다. 코드 생성 방식으로 옮길 때 각 provider에 대응하는 형태는 다음과 같습니다.
| Provider | |
| 이전 | |
| 이후 | |
| NotifierProvider | |
| 이전 | |
| 이후 | |
| FutureProvider | |
| 이전 | |
| 이후 | |
| StreamProvider | |
| 이전 | |
| 이후 | |
| AsyncNotifierProvider | |
| 이전 | |
| 이후 | |
| StreamNotifierProvider | |
| 이전 | |
| 이후 | |