주요 콘텐츠로 건너뛰기

자주 묻는 질문

커뮤니티에서 자주 나오는 질문을 모았습니다.

ref.refresh와 ref.invalidate는 어떻게 다른가요?​

ref에는 provider를 강제로 다시 계산하게 하는 메서드가 두 개 있어서, 둘이 어떻게 다른지 궁금했을 수도 있습니다.

생각보다 간단합니다. ref.refresh는 invalidate + read의 문법 설탕(syntax sugar)일 뿐입니다.

T refresh<T>(provider) {
invalidate(provider);
return read(provider);
}

다시 계산한 뒤의 새 값이 필요 없다면 invalidate를 사용하면 됩니다.
새 값이 필요하다면 refresh를 사용하세요.

정보

이 규칙은 린트 규칙으로 자동으로 강제됩니다.
반환값을 사용하지 않고 ref.refresh를 호출하면 경고가 표시됩니다.

동작상의 가장 큰 차이는, 무효화한 직후에 provider를 읽으면 provider가 즉시 다시 계산된다는 점입니다.
반면 invalidate만 호출하고 곧바로 읽지 않으면 업데이트는 나중에 일어납니다.

이 "나중"은 보통 다음 프레임이 시작될 때입니다. 다만 현재 아무도 구독하지 않는 provider를 무효화하면, 다시 구독될 때까지 업데이트되지 않습니다.

Ref와 WidgetRef에는 왜 공통 인터페이스가 없나요?​

Riverpod은 Ref와 WidgetRef를 의도적으로 분리해 두었습니다.
둘 중 어느 쪽에 조건부로 의존하는 코드를 작성하지 않도록 하기 위해서입니다.

한 가지 문제는 Ref와 WidgetRef가 비슷해 보여도 미묘한 차이가 있다는 점입니다.
둘 모두에 의존하는 코드는 발견하기 어려운 방식으로 불안정해집니다.

또한 WidgetRef에 의존하는 것은 BuildContext에 의존하는 것과 같습니다. 사실상 로직을 UI 계층에 두는 셈이므로 권장하지 않습니다.


이런 코드는 항상 Ref를 사용하도록 리팩터링해야 합니다.

보통은 로직을 Notifier로 옮기고(Provider 참고), 그 로직을 해당 Notifier의 메서드로 만드는 것이 해결책입니다.

이렇게 하면 위젯에서 이 로직을 호출하고 싶을 때 다음과 같이 작성할 수 있습니다.

ref.read(yourNotifierProvider.notifier).yourMethod();

yourMethod는 Notifier의 Ref를 사용해 다른 provider와 상호작용합니다.

왜 일반 StatelessWidget 대신 ConsumerWidget을 상속해야 하나요?​

InheritedWidget API의 아쉬운 한계 때문입니다.

몇 가지 문제가 있습니다.

  • InheritedWidget으로는 "변경 시" 리스너를 구현할 수 없습니다. 즉, ref.listen 같은 기능을 BuildContext로는 사용할 수 없습니다.

    가장 비슷한 것은 State.didChangeDependencies지만, 신뢰할 수 없습니다. 한 가지 문제는 의존성이 바뀌지 않았는데도 이 생명주기 메서드가 호출될 수 있다는 점입니다. 특히 위젯 트리에서 GlobalKey를 사용할 때 그렇습니다(일부 Flutter 위젯은 이미 내부적으로 사용합니다).

  • InheritedWidget을 구독한 위젯은 구독을 절대 멈추지 않습니다. "theme"이나 "media query" 같은 순수 메타데이터라면 대개 문제가 없습니다.

    하지만 비즈니스 로직에서는 문제가 됩니다. 페이지네이션 API를 provider로 표현한다고 해 봅시다. 페이지 오프셋이 바뀌면, 위젯이 이전에 보이던 페이지를 계속 구독하기를 원하지 않을 것입니다.

  • InheritedWidget은 위젯이 언제 구독을 멈추는지 추적할 방법이 없습니다. Riverpod은 provider가 구독되고 있는지 여부를 추적하는 데 의존하는 경우가 있습니다.

이 기능은 자동 폐기 메커니즘과 provider에 인자를 전달하는 기능 모두에 꼭 필요합니다.
이런 기능이 Riverpod을 강력하게 만들어 줍니다.

먼 미래에는 이 문제들이 해결될지도 모릅니다. 그렇게 되면 Riverpod은 Ref 대신 BuildContext를 사용하도록 바뀔 것입니다. 그러면 ConsumerWidget 대신 StatelessWidget을 사용할 수 있게 됩니다.
하지만 그건 나중 이야기입니다!

hooks_riverpod은 왜 flutter_hooks를 export하지 않나요?​

올바른 버전 관리 관행을 따르기 위해서입니다.

hooks_riverpod은 flutter_hooks 없이 사용할 수 없지만, 두 패키지의 버전은 서로 독립적으로 관리됩니다. 한쪽에만 호환성을 깨는 변경이 생길 수도 있습니다.

모든 provider를 한 번에 초기화하는 방법이 있나요?​

아니요, 모든 provider를 한 번에 초기화하는 방법은 없습니다.

안티 패턴으로 여겨지기 때문에 의도적으로 제공하지 않습니다. 모든 provider를 한 번에 초기화하면 초기화할 생각이 없던 provider까지 초기화되는 경우가 많습니다.

이 질문은 사용자가 로그아웃할 때 애플리케이션 상태를 초기화하고 싶은 경우에 자주 나옵니다.
그게 목적이라면, 사용자 상태에 의존하는 모든 것이 "user" provider를 ref.watch하도록 만드세요.

그러면 사용자가 로그아웃할 때 이 provider에 의존하는 provider는 모두 자동으로 초기화되고, 나머지는 그대로 유지됩니다.

위젯이 폐기된 후 "Using "ref" when a widget is about to or has been unmounted is unsafe." 오류가 발생합니다. 무엇이 문제인가요?​

같은 문제에 대한 예전 오류 메시지인 "Bad state: No ProviderScope found"가 표시될 수도 있습니다.

이 오류는 더 이상 마운트되어 있지 않은 위젯에서 ref를 사용하려고 할 때 발생합니다. 보통 await 이후에 일어납니다.

ElevatedButton(
onPressed: () async {
await future;
ref.read(...); // "Using "ref" when a widget is about to or has been unmounted is unsafe."가 발생할 수 있습니다
}
)

해결 방법은 BuildContext와 마찬가지로 ref를 사용하기 전에 mounted를 확인하는 것입니다.

ElevatedButton(
onPressed: () async {
await future;
if (!context.mounted) return;
ref.read(...); // 더 이상 예외가 발생하지 않습니다
}
)

알려진 주의 사항: 위젯이 제거되기 한 프레임 전에 provider가 다시 계산될(그리고 예외를 던질) 수 있습니다​

즉시 재계산 방식의 알려진 한계로, 아직 깔끔한 해결책이 없습니다.

provider의 의존성이 바뀌면, 리스너가 남아 있는 모든 의존 provider가 즉시 다시 계산됩니다. 구독 중인 위젯이 다시 빌드되거나 제거되는 다음 프레임보다 먼저입니다.

따라서 같은 변경이 리스너를 제거하는 원인이기도 하다면(로그아웃하거나 필터로 목록이 줄어드는 경우 등), 의존 provider는 위젯이 언마운트되기 전에 새 값으로 한 번 더 다시 계산됩니다. 그 계산이 이전 값을 전제로 했다면 예외가 발생합니다.

final userNameProvider = Provider((ref) {
final user = ref.watch(userProvider);
return user!.name; // 로그아웃 시 화면이 제거되기 한 프레임 전에 예외가 발생합니다
});
주의

현재로서는 이 마지막 재계산을 건너뛸 방법이 없습니다. 방법이 생기기 전까지는 계산이 모든 입력을 처리하도록 만드는 것이 권장되는 우회 방법입니다. 일시적인 상태에서는 예외를 던지는 대신 null(또는 센티널 값)을 반환하세요.

final userNameProvider = Provider((ref) {
final user = ref.watch(userProvider);
if (user == null) return null; // 잠깐 동안의 유효하지 않은 구간을 처리합니다
return user.name;
});

코드가 다소 번잡해지지만 예외를 피할 수 있습니다. throw는 일시적인 상태가 아니라 정말로 불가능한 상태에만 사용하세요.