자취얌! 프로젝트에서 Provider의 의존성 주입에 관련한 코드 리팩토링 중 BottomSheet가 진입 이전 UI의 viewModel을 참조하지 못하는 상황이 발생했습니다. 오늘은 이러한 에러의 원인과 해결 과정을 공유하려고 합니다.
현재 진행하고 있는 자취얌! 프로젝트에서는 Provider를 통해 상태관리를 하고있습니다.
기존의 개발되던 코드는 항상 앱의 최상단에 대부분의 Provider를 주입하고 있었고,
저는 두가지 근거로 인하여 코드 리팩토링을 결정하였습니다.
- 최상단에 주입된 많은 Provider들은 메모리를 낭비하게 된다.
프로젝트 UX 설계에 의하여 데이터의 생성, 수정 등 여러 이벤트가 BottomSheet에서 주로 이루어졌습니다. 초기 개발 단계에서는 편리하게 관리하기 위해 최상단에서 MultiProvider로 모든 Provider를 주입하였지만
이는 사용하지 않는 Provider가 많은 경우에는 리소스를 낭비하고 있게됩니다.
- 이미 상단에 주입된 Provider는 dispose되지 않기 때문에 상태가 계속 유지된다.
사용자가 해당 UI에서 작업 중 빠져나와도 최상단에 주입된 ChangeNotifierProvider인 ViewModel은 자동으로 dispose되지 않기에 재진입시 기존 작업이 남아있는 현상이 발생했습니다.
이는 여러 예외처리과정이 필요하게 되었습니다.
위와 같은 메모리 낭비와 예외처리를 단순화하고자 UI에서 참조해야하는 Provider를 진입시 주입하는 방식으로 코드를 리팩토링하게 되었습니다.
이러한 과정 중 식재료 생성 UI에서 오늘 다뤄질 에러상황을 마주하게 되었습니다.
![]() |
![]() |
위는 식재료 생성 UI입니다. 현재 선택된 기본 재료가 없으면 탭하여 BottomSheet를 통해 식재료를 고를 수 있습니다. 생성 UI는 생성 ViewModel을 참조하고 있고, BottomSheet는 기본 식재료 ViewModel을 참조하고 있습니다. 여기서 중요한 사실은 생성 UI의 하위에 BottomSheet가 위치하지 않고, 형제 위젯트리에 위치하게 됩니다.
따라서, 기본 식재료 BottomSheet는 이전 UI의 ViewModel을 context를 통해서 참조할 수 없습니다.
void showIngredientAddBottomSheet() {
showModalBottomSheet(
backgroundColor: Colors.red,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32.0))),
context: context,
builder: (context) => const BasicIngredientBottomSheet());
이 context는 새롭게 생성된 context이기 때문입니다.
재료를 선택하는 로직은 아래와 같습니다.
...
onTap: () {
// 시트를 닫음.
Navigator.of(context).pop();
/// 해당 재료를 선택할 경우,
/// [RefreginatorIngredientViewModel] 에서 해당 재료를 기본 재료로
/// 선택하게 됨.
context
.read<NewRefreginatorIngredientViewModel>()
.onEvent(SelectNewIngredientEvent(selectIngredient: i));
},
여기서 i는 시트의 타일이 나타내는 식재료입니다.
이런 상황에서 context는 생성 ViewModel을 참조할 수 없기에 ProviderNotFound 에러가 발생하게 됩니다.
해결 방법은 두가지가 있습니다.
- 최상단에서 필요한 ViewModel을 주입한다.
하지만 이 방식은 결국 사용자가 해당 UI를 진입하지 않아도 메모리를 차지하기에 메모리가 낭비됩니다.- 생성 UI의 context에서 ViewModel을 찾아 전달하여 주입한다.
저는 이 방식을 선택했습니다. 이렇게 되면 Singleton 방식으로 생성 ViewModel에게 참조하도록 하게 됩니다.
void showIngredientAddBottomSheet() {
showModalBottomSheet(
backgroundColor: Colors.red,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32.0))),
context: context,
builder: (newContext) {
return MultiProvider(
providers: [
baiscIngredientViewModelProvider,
ListenableProvider(
create: (newContext) =>
context.read<NewRefreginatorIngredientViewModel>(),
)
],
builder: (newContext, child) =>
const BasicIngredientBottomSheet());
});
주목할 부분은 context를 다르게 참조하도록 한 것입니다.
ListenableProvider(
create: (newContext) =>
context.read<NewRefreginatorIngredientViewModel>(),
)
이를 통해서 BottomSheet의 newContext가 아닌 기존 context에서 해당 ViewModel을 읽어올 수 있고, ListenableProvider를 통해서 새롭게 주입할 수 있습니다.
처음에는 ChangeNotifierProvider로 주입할려고 했으나, ChangeNotifierProvider는 BottomSheet에서 pop()하게 되면 dispose되어 더이상 생성 ViewModel을 참조할 수 없게 되버립니다.
이제 에러 없이 정상적으로 동작하게 됩니다.