Bloc/Cubit๊ณผ ์ํ ๊ด๋ฆฌ ์ ํ ๊ธฐ์ค
์ง๋ ๊ธ๊น์ง Provider์ Riverpod์ผ๋ก ์ํ๋ฅผ ๊ณต์ ํด๋ดค๋ค. ์ด๋ฒ์ ์ธ ๋ฒ์งธ ์ ํ์ง์ธ Bloc ๋ผ์ด๋ธ๋ฌ๋ฆฌ(flutter_bloc)๋ฅผ ๋ณด๊ณ , ๋ง์ง๋ง์ "๊ทธ๋์ ๋ญ ์จ์ผ ํ๋"๋ฅผ ์ ๋ฆฌํ๋ค. ๋จผ์ Bloc์ด ์ ๋์๋์ง๋ถํฐ ๋ณด์. ์๋ฒ์์ ์ฌ์ฉ์ ์ด๋ฆ์ ๋ถ๋ฌ์ค๋ ๋ชจ๋ธ์ ์ง๋ ๊ธ์ ChangeNotifier๋ก ๋ง๋ค์ด๋ณธ๋ค.
class UserModel extends ChangeNotifier {
bool isLoading = false;
String? error;
String? name;
Future<void> load() async {
isLoading = true;
notifyListeners();
try {
await Future.delayed(const Duration(seconds: 1)); // ์๋ฒ ์์ฒญ์ด๋ผ๊ณ ๊ฐ์
name = 'ํ๊ธธ๋';
isLoading = false;
} catch (e) {
error = '๋ถ๋ฌ์ค์ง ๋ชปํ์ด์';
isLoading = false;
}
notifyListeners();
}
}์๋ฌ๋ ๋์ง ์๋๋ค. ๊ทธ๋ฐ๋ฐ ์ฒ์ ์์ฒญ์ด ์คํจํด์ error์ ๋ฌธ๊ตฌ๊ฐ ๋ค์ด๊ฐ ๋ค, ๋ค์ ์๋ํด์ ์ฑ๊ณตํ๋ฉด ์ด์ํ ์ผ์ด ์๊ธด๋ค. name์๋ 'ํ๊ธธ๋'์ด ๋ค์ด๊ฐ๋๋ฐ error๋ ์ง์์ง์ง ์์์, ํ๋ฉด์ด "์๋ฌ ๋ฌธ๊ตฌ"์ "ํ๊ธธ๋"์ ๋์์ ๋ณด์ฌ์ค๋ค.
์์ธ: ์ํ๊ฐ ํ๋ ์ธ ๊ฐ๋ก ํฉ์ด์ ธ ์์ด์ ๋ง์ด ์ ๋๋ ์กฐํฉ์ด ๊ฐ๋ฅํ๋ค
isLoading, error, name ์ธ ํ๋๋ ๋ฐ๋ก ์์ง์ธ๋ค. ์กฐํฉ์ ์ฌ๋ ๊ฐ์ง์ธ๋ฐ ์๋ฏธ๊ฐ ์๋ ๊ฑด "๋ก๋ฉ ์ค", "์ฑ๊ณต", "์คํจ", "์์ ์ " ๋ค ๊ฐ์ง๋ฟ์ด๋ค. isLoading์ด true์ธ๋ฐ error๋ ์๋ ์ํ์ฒ๋ผ ๋ชจ์๋ ์กฐํฉ์ ์ฝ๋๊ฐ ๋ง์์ฃผ์ง ์๋๋ค. ๊ฐ์ ๋ฐ๊ฟ ๋๋ง๋ค ์ธ ํ๋๋ฅผ ๋ค ์ฑ๊ฒจ์ผ ํ๊ณ , ํ๋๋ง ๋นผ๋จน์ผ๋ฉด ์ด๋ฐ ๋ฒ๊ทธ๊ฐ ์๊ธด๋ค.
ํด๊ฒฐ: ์ํ๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ๋๋ค
Dart 3์ sealed class๋ก ๊ฐ๋ฅํ ์ํ๋ฅผ ๋ค ๊ฐ์ง๋ก ๋ชป๋ฐ๋๋ค.
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final String name;
UserLoaded(this.name);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}์ด์ ์ํ๋ ๋ค ๊ฐ์ง ์ค ์ ํํ ํ๋๋ค. UserLoaded์๋ error๊ฐ ์์ ์๊ณ UserError์๋ name์ด ์์ด์, ์๋ฌ์ ์ด๋ฆ์ด ๋์์ ์กด์ฌํ ์ ์๋ค. ์ด ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ Cubit์ด๋ค.
Cubit: ์ํ ํ๋๋ฅผ ๋ค๊ณ emit์ผ๋ก ๋ฐ๊พผ๋ค
์ค์น๋ถํฐ ํ๋ค.
flutter pub add flutter_blocimport 'package:flutter_bloc/flutter_bloc.dart';
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserInitial()); // ์ฒ์ ์ํ
Future<void> load() async {
emit(UserLoading());
try {
await Future.delayed(const Duration(seconds: 1)); // ์๋ฒ ์์ฒญ์ด๋ผ๊ณ ๊ฐ์
emit(UserLoaded('ํ๊ธธ๋'));
} catch (e) {
emit(UserError('๋ถ๋ฌ์ค์ง ๋ชปํ์ด์'));
}
}
}Cubit<UserState>: ์ํ์ ํ์ ์ดUserState์ธ ๊ด๋ฆฌ์๋ค: super(UserInitial()): ์์ฑ์ ๊ธ์์ ๋ณธ initializer list๋ก ์ฒ์ ์ํ๋ฅผ ๋ถ๋ชจ์๊ฒ ๋๊ธด๋คemit(์ ์ํ): ์ํ๋ฅผ ํต์งธ๋ก ๊ฐ์๋ผ์ฐ๊ณ ๊ตฌ๋ ์์๊ฒ ์๋ฆฐ๋ค.notifyListeners()๊ฐ ํ์ ์๋คstate: ์ง๊ธ ์ํ๋ค. ๋ฐ์์๋ ์ฝ๊ธฐ๋ง ๋๊ณ ๋ฐ๊ฟ ์ ์๋ค. ๋ฐ๊พธ๋ ๊ธธ์emit๋ฟ์ด๋ค
ChangeNotifier๋ ํ๋๋ฅผ ํ๋์ฉ ๊ณ ์น๋ค ๋ง์ง๋ง์ ์๋ฆผ์ ๋ณด๋๋ค. Cubit์ ์์ฑ๋ ์ํ๋ฅผ ํ ๋ฒ์ emitํ๋ค. ์ค๊ฐ์ ์ด์ ์ฉกํ ์ํ๊ฐ ํ๋ฉด์ ๋ณด์ผ ์ผ์ด ์๋ค.
ํ๋ฉด์ ์ฐ๊ฒฐํ๊ธฐ
void main() {
runApp(
BlocProvider(
create: (context) => UserCubit()..load(), // ๋ง๋ค๋ฉด์ ๋ฐ๋ก ๋ถ๋ฌ์ค๊ธฐ
child: const MyApp(),
),
);
}
class UserView extends StatelessWidget {
const UserView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
return switch (state) {
UserInitial() => const Text('์์ ์ '),
UserLoading() => const CircularProgressIndicator(),
UserLoaded(:final name) => Text('์๋
, $name'),
UserError(:final message) => Text('์๋ฌ: $message'),
};
},
);
}
}BlocProvider: Cubit์ ํธ๋ฆฌ์ ๊ฑธ์ด๋๋ค. Provider์ChangeNotifierProvider์ ๊ฐ์ ์๋ฆฌ๋คBlocBuilder<UserCubit, UserState>: ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋คbuilder๋ฅผ ๋ค์ ์คํํ๋ค.watch์ ๋น์ทํ ์ญํ ์ด๋คUserCubit()..load():..๋ ์์ ๊ฐ์ฒด๋ฅผ ๋๋ ค์ฃผ๋ฉด์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ ๋ฌธ๋ฒ์ด๋ค. Cubit์ ๋ง๋ค์๋ง์load()๋ฅผ ๋ถ๋ฅธ๋คswitch์sealed class๋ฅผ ๋ฃ์ผ๋ฉด ๋ค ๊ฐ์ง๋ฅผ ๋น ์ง์์ด ์ฒ๋ฆฌํ๋์ง ์ปดํ์ผ๋ฌ๊ฐ ํ์ธํ๋ค. ์ํ๋ฅผ ํ๋ ์ถ๊ฐํ๊ณswitch์์ ๋นผ๋จน์ผ๋ฉด ๋นจ๊ฐ ์ค์ด ๋ฌ๋ค
๋ฒํผ์์ Cubit์ ํจ์๋ฅผ ๋ถ๋ฅผ ๋ ์ง๋ ๊ธ๋ค๊ณผ ๋๊ฐ์ด context.read๋ฅผ ์ด๋ค.
ElevatedButton(
onPressed: () => context.read<UserCubit>().load(),
child: const Text('๋ค์ ๋ถ๋ฌ์ค๊ธฐ'),
)์ฝ์ง ํฌ์ธํธ 1: ๋ฆฌ์คํธ๋ฅผ ๊ณ ์ณ์ emitํ๋ฉด ํ๋ฉด์ด ์ ๋ฐ๋๋ค
class TodoCubit extends Cubit<List<String>> {
TodoCubit() : super([]);
void add(String item) {
state.add(item); // ๊ธฐ์กด ๋ฆฌ์คํธ์ ์ง์ ์ถ๊ฐ
emit(state); // ๊ฐ์ ๋ฆฌ์คํธ๋ฅผ ๋ค์ emit
}
}ํญ๋ชฉ์ ์ถ๊ฐ๋๋๋ฐ ํ๋ฉด์ ๊ทธ๋๋ก๋ค. Cubit์ ์ ์ํ๊ฐ ํ์ฌ ์ํ์ ๊ฐ์ผ๋ฉด ๋ฌด์ํ๋ค. ๊ฐ์ ๋ฆฌ์คํธ ๊ฐ์ฒด๋ฅผ emitํ๋ฉด "๋ฐ๋ ๊ฒ ์๋ค"๊ณ ํ๋จํ๋ค. ์ ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ ๋๊ฒจ์ผ ํ๋ค.
void add(String item) {
emit([...state, item]); // ์ ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ emit
}[...state, item]์ ๊ธฐ์กด ํญ๋ชฉ์ ํผ์น๊ณ ํ๋๋ฅผ ๋ํ ์ ๋ฆฌ์คํธ๋ค. ์ปฌ๋ ์
๊ธ์์ ๋ณธ .toList() ์ต๊ด๊ณผ ์ด์ด์ง๋ค. Cubit์์๋ ์ํ๋ฅผ ์ง์ ๊ณ ์น์ง ์๊ณ ์๋ก ๋ง๋ ๋ค๊ฐ ๊ท์น์ด๋ค.
์ฝ์ง ํฌ์ธํธ 2: BlocProvider๋ฅผ ๊ฑธ์ด๋ ๊ณณ์์ ๋ฐ๋ก ์ฝ์ผ๋ฉด ๋ชป ์ฐพ๋๋ค
BlocProvider๋ฅผ ๋ง๋๋ ๊ฐ์ build์์ context.read<UserCubit>()๋ฅผ ๋ถ๋ฅด๋ฉด BlocProvider๋ฅผ ์ฐพ์ง ๋ชปํ๋ค๋ ์๋ฌ๊ฐ ๋๋ค. InheritedWidget, Provider์์ ๋ณธ ๊ฒ๊ณผ ๊ฐ์ ์ด์ ๋ค. context๊ฐ BlocProvider๋ณด๋ค ์์ ์์ด์ ์๋ก ์ฌ๋ผ๊ฐ๋ ์๋ค. ์ฝ๋ ์ชฝ์ ๋ณ๋ ์์ ฏ์ด๋ BlocBuilder, Builder๋ก ๋ถ๋ฆฌํ๋ค.
Cubit๊ณผ Bloc์ ์ฐจ์ด
flutter_bloc์๋ ๋ ๊ฐ์ง๊ฐ ์๋ค. ์ง๊ธ๊น์ง ์ด ๊ฑด Cubit์ด๋ค. Bloc์ ํ ๋จ๊ณ ๋ ๋์๊ฐ์, ํจ์๋ฅผ ์ง์ ๋ถ๋ฅด๋ ๋์ ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ๋ณด๋ธ๋ค.
sealed class CounterEvent {}
class Incremented extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Incremented>((event, emit) => emit(state + 1));
}
}
// ์ฌ์ฉ
context.read<CounterBloc>().add(Incremented());Cubit:increment()๊ฐ์ ํจ์๋ฅผ ํธ์ถํ๋ฉด ์ํ๊ฐ ๋ฐ๋๋ค. ์ฝ๋๊ฐ ์งง๋คBloc:add(์ด๋ฒคํธ)๋ฅผ ๋ณด๋ด๋ฉดon<์ด๋ฒคํธ>๊ฐ ๋ฐ์์ ์ํ๋ฅผ ๋ฐ๊พผ๋ค. ์ฝ๋๊ฐ ๊ธธ์ง๋ง, ์ด๋ค ์ด๋ฒคํธ ๋๋ฌธ์ ์ํ๊ฐ ๋ฐ๋์๋์ง ๊ธฐ๋ก์ผ๋ก ๋จ๋๋ค
์ฒ์์๋ Cubit์ผ๋ก ์์ํ๊ณ , ์ด๋ฒคํธ ๊ธฐ๋ก์ด๋ ์ด๋ฒคํธ ๋ณํ(๊ฒ์์ด ์
๋ ฅ ์ง์ฐ ์ฒ๋ฆฌ ๋ฑ)์ด ํ์ํด์ง ๋ Bloc์ผ๋ก ์ฌ๋ฆฌ๋ ๊ฒ ๋ณดํต์ด๋ค.
๊ทธ๋์ ๋ญ ์จ์ผ ํ๋: ์ ํ ๊ธฐ์ค
์ ๋ต์ ์๋ค. ์ง๊ธ๊น์ง ๋ณธ ๋ค ๊ฐ์ง๋ฅผ ๊ธฐ์ค๋ณ๋ก ๋๊ณ ๋ณด๋ฉด ์ด๋ ๋ค.
setState: ํ ์์ ฏ ์์์๋ง ์ฐ๋ ๊ฐ(์ ๋ ฅ ์ค์ธ ๊ธ์, ํผ์นจ ์ฌ๋ถ). ๋ณ๋ ํจํค์ง๊ฐ ํ์ ์๋ค- Provider: ๊ตฌ์กฐ๊ฐ ๋จ์ํด์ ๊ฐ์ฅ ๋นจ๋ฆฌ ์ตํ๋ค. ์์ ์ฑ์ด๋ ํ์ต์ฉ์ ์๋ง๋ค. ๋ค๋ง ๊ฐ์ ํ์ ์ผ๋ก ์ฐพ๊ธฐ ๋๋ฌธ์ ์คํ ์ค์์ผ ์๋ฌ๊ฐ ๋๋ฌ๋๋ค
- Riverpod: ์ปดํ์ผ ๋ ์๋ฌ๋ฅผ ์ก๊ณ
context์์ด ์ฝ๋๋ค. ๊ฐ๋ผ๋ฆฌ ์๋ก ์์กดํ๋ ๊ตฌ์กฐ์ ํ ์คํธ์ ์ ๋ฆฌํด์, ์๋ก ์์ํ๋ ํ๋ก์ ํธ์์ ๋ง์ด ๊ณ ๋ฅธ๋ค - Bloc/Cubit: ์ํ์ ์ด๋ฒคํธ๋ฅผ ์๊ฒฉํ ํ์ ๊ฐ๋๋ค. ์ฝ๋๋ ๋์ด๋์ง๋ง ์ฌ๋ฌ ์ฌ๋์ด ๊ฐ์ ๊ตฌ์กฐ๋ก ์ฐ๊ฒ ๊ฐ์ ํ๊ธฐ ์ข์์, ํ์ด ํฌ๊ณ ๊ท์น์ด ์ค์ํ ํ๋ก์ ํธ์์ ๋ง์ด ์ด๋ค
๊ณ ๋ฅผ ๋๋ ์ธ ๊ฐ์ง๋ฅผ ๋ณธ๋ค.
- ํ๋ก์ ํธ ๊ท๋ชจ: ํ๋ฉด ๋์ธ ๊ฐ๋ฉด Provider๋ก ์ถฉ๋ถํ๊ณ , ์ปค์ง์๋ก Riverpod์ด๋ Bloc์ ๊ท์น์ด ๋์์ด ๋๋ค
- ํ ๊ฒฝํ: ์ด๋ฏธ ์ต์ํ ์ฌ๋์ด ๋ง์ ์ชฝ์ด ๋ซ๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฑ๋ฅ ์ฐจ์ด๋ณด๋ค ํ์ด ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฐ๋ ๊ฒ ๋ ํฌ๋ค
- ํ ์คํธ ์ฉ์ด์ฑ: ์ํ๋ฅผ ๋ง๋๋ ๊ณณ์ด ์์ ฏ๊ณผ ๋ถ๋ฆฌ๋ผ ์์์๋ก ํ ์คํธ๊ฐ ์ฝ๋ค. ์ธ ๊ฐ์ง ๋ชจ๋ ๊ฐ๋ฅํ์ง๋ง Riverpod๊ณผ Bloc์ด ํ์ด ๋ ๋ถ๋ช ํ๋ค
ํผ์ ์ฑ์ ๋ง๋๋ ์ ์ฅ์ด๋ผ๋ฉด Riverpod ํ๋๋ก ์์ํ๋ ๊ฒ ๋ฌด๋ํ๋ค. ๋ค๋ง ์ด๋ ์ชฝ์ ๊ณจ๋ผ๋ ์ค๋ ๋ณธ "์ํ๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ๋๋ค"๋ ์์น์ ๊ทธ๋๋ก ์ธ ์ ์๋ค.
Compose, SwiftUI์ ๋น๊ตํ๋ฉด
// Compose - ViewModel + sealed interface UiState
sealed interface UserUiState {
data object Loading : UserUiState
data class Loaded(val name: String) : UserUiState
data class Error(val message: String) : UserUiState
}
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<UserUiState>(UserUiState.Loading)
val state = _state.asStateFlow()
}
// ํ๋ฉด
when (val s = state) {
is UserUiState.Loading -> CircularProgressIndicator()
is UserUiState.Loaded -> Text("์๋
, ${s.name}")
is UserUiState.Error -> Text(s.message)
}// SwiftUI - enum์ผ๋ก ์ํ ๋ฌถ๊ธฐ
enum UserState {
case loading
case loaded(name: String)
case error(message: String)
}
switch state {
case .loading: ProgressView()
case .loaded(let name): Text("์๋
, \(name)")
case .error(let message): Text(message)
}- ์ํ๋ฅผ ๋ฌถ๋ ๋ฐฉ๋ฒ: Flutter
sealed class/ Composesealed interface UiState/ SwiftUIenum(์ฐ๊ด๊ฐ) - ์ํ๋ฅผ ๋ค๊ณ ์๋ ๊ณณ: Flutter
Cubit/ ComposeViewModel+StateFlow/ SwiftUIObservableObject๋@Observable - ์ํ ๋ฐ๊พธ๊ธฐ: Flutter
emit(์ ์ํ)/ Compose_state.value = ์ ์ํ/ SwiftUI ํ๋กํผํฐ ๋์ - ํ๋ฉด์์ ๋ถ๊ธฐ: ์ธ ์ชฝ ๋ชจ๋
switch/when์ผ๋ก ๋น ์ง์์ด ์ฒ๋ฆฌ
Compose์ ViewModel + sealed interface UiState ๊ตฌ์กฐ์ ๊ฑฐ์ ๊ฐ๋ค. Cubit์ด ViewModel, BlocBuilder๊ฐ collectAsState ๋ค์ when ๋ถ๊ธฐ์ ํด๋นํ๋ค.
์ ๋ฆฌ
- ์ํ๋ฅผ
isLoading,error,name๊ฐ์ ํ๋๋ก ํฉ์ด๋๋ฉด ๋ชจ์๋ ์กฐํฉ์ด ๊ฐ๋ฅํ๊ณ , ํ๋๋ง ๋นผ๋จน์ด๋ ๋ฒ๊ทธ๊ฐ ์๊ธด๋ค sealed class๋ก ๊ฐ๋ฅํ ์ํ๋ฅผ ์ ํด๋๊ณCubit<์ํ>๊ฐemit์ผ๋ก ํต์งธ๋ก ๊ฐ์๋ผ์ฐ๋ฉด, ๋ง์ด ๋๋ ์ํ๋ง ์กด์ฌํ๋ค- ํ๋ฉด์์๋
BlocProvider๋ก ๊ฑธ์ด๋๊ณBlocBuilder๋ก ์ํ๋ณ ํ๋ฉด์switch๋ก ๋๋๋ค.sealed class๋ผ์ ๋น ๋จ๋ฆฐ ์ํ๋ ์ปดํ์ผ๋ฌ๊ฐ ์๋ ค์ค๋ค - Cubit์ ์ ์ํ๊ฐ ํ์ฌ ์ํ์ ๊ฐ์ผ๋ฉด ๋ฌด์ํ๋ฏ๋ก, ๋ฆฌ์คํธ ๊ฐ์ ๊ฐ์ ์ง์ ๊ณ ์น์ง ๋ง๊ณ ์๋ก ๋ง๋ค์ด์
emitํ๋ค - ํจ์๋ฅผ ๋ถ๋ฅด๋
Cubit์ผ๋ก ์์ํ๊ณ , ์ด๋ฒคํธ ๊ธฐ๋ก์ด ํ์ํ๋ฉดBloc์ ์ด๋ค - ํ๋ก์ ํธ ๊ท๋ชจ, ํ ๊ฒฝํ, ํ ์คํธ ์ฉ์ด์ฑ์ ๋ณด๊ณ ๊ณ ๋ฅด๋, ํผ์ ์์ํ๋ค๋ฉด Riverpod์ด ๋ฌด๋ํ๋ค
๋ค์ ๊ธ๋ถํฐ๋ 4๋จ๊ณ๋ก ๋์ด๊ฐ์, ์ฌ๋ฌ ํ๋ฉด์ ์ค๊ฐ๋ ํ๋ฉด ์ด๋(Navigator์ go_router)์ ๋ค๋ค๋ณด๊ฒ ๋ค.
