이전 프로젝트에서 Riverpod과 Supabase를 쓰기로 했었는데, 그게 이루어지지 않았어서 많이 아쉬웠다.
그래서 Supabase + Riverpod를 기반으로 내 앱 포트폴리오를 만들어보기로했다.
나에게는 Next와 Notion API를 활용해서 만든 기존의 웹 포트폴리오가 있다. 심지어 mobile first로 만들었음🤔
그치만 Notion API를 내가 잘 활용하지 못한 것 같고, 관리도 힘들어서 플러터로 공부할겸 도전하고있다.
슈퍼베이스는 RDB라서 설계만 잘 하면 효율적으로 관리할 수 있다.
일단 가입한 후에 url과 anon키를 주는데, 나는 이 정보들은 .env폴더에서 관리하도록 했다.
우선 env파일을 플러터에서 인식시키고 사용하기 위해 필요한 작업들이 있다.
flutter_dotenv
패키지를 설치했다.그리고 env파일을 pubspec 파일에서 등록한다.
flutter:
assets:
- assets/config/.env
키를 따로 관리한다.
SUPABASE_URL=https://어쩌고저쩌고.supabase.co
SUPABASE_ANON_KEY=어쩌고저쩌고
깃허브에 env파일이 올라가지 않도록 gitignore에 꼭 아래 코드를 추가해야한다. 올라가도 수습할 수 있는 방법은 있지만 애초에 노출하지 않는 게 젤 좋겠지??
*.env
클라이언트가 빌드되기 전에 먼저 작업하기 위함이다.
env파일로부터 데이터를 읽어올 수 있도록 dotenv를 사용해 로드한다.
supabase 초기화작업을 시작한다.
이때 url, anonKey를 전달해야한다.
import 'package:supabase_flutter/supabase_flutter.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: "assets/config/.env");
await Supabase.initialize(
url: dotenv.env['SUPABASE_URL']!,
anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
);
runApp(
const MyApp(),
);
}
간단한 요청을 해서 확인해본다.
Supabase supabase = Supabase.instance;
Future<List<DataType>> getData() async {
try {
final res = await supabase.client.from('data').select();
return [
for (final data in res) Me.fromMap(data),
];
} catch (e) {
rethrow;
}
}
요청해보면 서버로부터 데이터가 잘 도착하는 것을 볼 수 있다.
riverpod은 provider를 좀더 보완해서 나온 상태관리 툴이다.
무슨 볼드모트처럼(ㅋㅋㅋㅋ) provider라는 글자를 재구성해서 riverpod이라는 이름을 지었다고 한다.
provider를 잠깐 공부해봤는데 확실히 유사하다.
다만 provider를 사용할 때 거의 필수적으로 추가했던 flutter_state_notifier
를 따로 다운받지 않아도 된다는 점.
기존에는 Widget
이었던 provider가 Object
로서 생성되기 때문에 main에서 빌드하지 않아도 된다는 점이 편리하다. 이전에는 프로바이더가 늘면서 main 파일의 코드도 점점 늘어날 수밖에 없었는데, 그런 보일러플레이트가 현저히 줄었다.
Future main() async {
... 생략
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
관심사 분리를 위해 슈베에서 데이터(리모트 소스)를 받아오는 함수를 따로 관리했다.
데이터를 가지고있는 레포이니 레포 폴더 내에서 작성할 것이다.
class AboutMeRepository {
AboutMeRepository();
Future<List<Me>> getAboutMe() async {
try {
final res = await Supabase.instance.client.from('data').select();
return [
for (final data in res) Me.fromMap(data),
];
} catch (e) {
throw e.toString();
}
}
}
final aboutMeRepositoryProvider = Provider<AboutMeRepository>(
(ref) => AboutMeRepository(),
);
상태관리를 위해 enum을 생성했다.
초기화할때, fetching할때, 요청에 대한 응답을 성공적으로 받았을때, 에러가 발생했을때를 상정한다.
enum DataStatus {
init,
fetching,
success,
error,
}
(1) 상태를 초기화하는 factory함수를 만들어 dataList에 빈 배열을 할당한다. 상태 또한 init으로 만든다.
(2) copyWith를 통해 데이터를 수정할 수 있도록 한다.
import 'package:equatable/equatable.dart';
class MeState extends Equatable {
final DataStatus meStatus;
final List<Me> meList;
const MeState({
required this.meStatus,
required this.meList,
});
factory MeState.init({ // 단계 (1)
List<Me>? meList,
}) {
return const MeState(
meList: [],
meStatus: DataStatus.init,
);
}
MeState copyWith({ // 단계 (2)
List<Me>? meList,
DataStatus? meStatus,
}) {
return MeState(
meList: meList ?? this.meList,
meStatus: meStatus ?? this.meStatus,
);
}
List<Object?> get props => [meList];
}
notifier가 생성될 때 위에서 만든 aboutMeRepository 객체를 전달한다.
그리고 이때 get함수를 실행하도록 한다.
StateNotifier는 상태를 구독하고 있다가 값이 변화하면 알려주는 역할을 한다.
class MeNotifier extends StateNotifier<MeState> {
final AboutMeRepository aboutMeRepository;
MeNotifier(this.aboutMeRepository) : super(MeState.init());
Future<void> getAboutMe() async {
try {
state = state.copyWith(meStatus: DataStatus.fetching);
List<Me> meList = await aboutMeRepository.getAboutMe();
state = state.copyWith(
meList: meList,
meStatus: DataStatus.success,
);
} catch (e) {
state = state.copyWith(meStatus: MeStatus.error);
rethrow;
}
}
}
final meProvider = StateNotifierProvider<MeNotifier, MeState>(
(ref) {
final meNotifier = MeNotifier(AboutMeRepository());
ref.onDispose(() {
meNotifier.dispose();
});
meNotifier.getAboutMe(); // 데이터를 받아오도록 한다.
return meNotifier;
},
);
일단 데이터를 get하는 과정은 이게 다다.
이후에는 RDB 세팅하는 방법을 좀 더 자세히 설명하고 적용해볼 것이다.
그리고 이미지, 영상을 업로드하고 사용하는 것도 적용해 볼 생각이다.