[flutter] Supabase + RiverPod으로 만드는 포트폴리오 앱

KoEunseo·2024년 1월 22일
0

flutter

목록 보기
41/45

이전 프로젝트에서 Riverpod과 Supabase를 쓰기로 했었는데, 그게 이루어지지 않았어서 많이 아쉬웠다.
그래서 Supabase + Riverpod를 기반으로 내 앱 포트폴리오를 만들어보기로했다.

나에게는 Next와 Notion API를 활용해서 만든 기존의 웹 포트폴리오가 있다. 심지어 mobile first로 만들었음🤔
그치만 Notion API를 내가 잘 활용하지 못한 것 같고, 관리도 힘들어서 플러터로 공부할겸 도전하고있다.

Supabase

슈퍼베이스는 RDB라서 설계만 잘 하면 효율적으로 관리할 수 있다.
일단 가입한 후에 url과 anon키를 주는데, 나는 이 정보들은 .env폴더에서 관리하도록 했다.

환경설정

우선 env파일을 플러터에서 인식시키고 사용하기 위해 필요한 작업들이 있다.

flutter_dotenv 패키지를 설치했다.

pubspec.yaml

그리고 env파일을 pubspec 파일에서 등록한다.

flutter:
  assets:
    - assets/config/.env

assets/config/.env

키를 따로 관리한다.

SUPABASE_URL=https://어쩌고저쩌고.supabase.co
SUPABASE_ANON_KEY=어쩌고저쩌고

가장 중요하고 까먹기 딱 좋은 .gitignore

깃허브에 env파일이 올라가지 않도록 gitignore에 꼭 아래 코드를 추가해야한다. 올라가도 수습할 수 있는 방법은 있지만 애초에 노출하지 않는 게 젤 좋겠지??

*.env

이제 supabase와 연동시킨다.

WidgetsFlutterBinding.ensureInitialized()

클라이언트가 빌드되기 전에 먼저 작업하기 위함이다.

dotenv.load(fileName: "assets/config/.env")

env파일로부터 데이터를 읽어올 수 있도록 dotenv를 사용해 로드한다.

Supabase.initialize()

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

riverpod은 provider를 좀더 보완해서 나온 상태관리 툴이다.
무슨 볼드모트처럼(ㅋㅋㅋㅋ) provider라는 글자를 재구성해서 riverpod이라는 이름을 지었다고 한다.

provider를 잠깐 공부해봤는데 확실히 유사하다.
다만 provider를 사용할 때 거의 필수적으로 추가했던 flutter_state_notifier를 따로 다운받지 않아도 된다는 점.
기존에는 Widget이었던 provider가 Object로서 생성되기 때문에 main에서 빌드하지 않아도 된다는 점이 편리하다. 이전에는 프로바이더가 늘면서 main 파일의 코드도 점점 늘어날 수밖에 없었는데, 그런 보일러플레이트가 현저히 줄었다.

1. ProviderScope로 앱의 최상단 위젯을 감싼다.

Future main() async {
  ... 생략
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

repository

aboutMeRepository.dart

관심사 분리를 위해 슈베에서 데이터(리모트 소스)를 받아오는 함수를 따로 관리했다.
데이터를 가지고있는 레포이니 레포 폴더 내에서 작성할 것이다.

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(),
);

viewmodel

state enum

상태관리를 위해 enum을 생성했다.
초기화할때, fetching할때, 요청에 대한 응답을 성공적으로 받았을때, 에러가 발생했을때를 상정한다.

enum DataStatus {
  init,
  fetching,
  success,
  error,
}

state

  • equatable은 인스턴스 관리를 손쉽게 하기 위해 사용했다.

    (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

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 세팅하는 방법을 좀 더 자세히 설명하고 적용해볼 것이다.
그리고 이미지, 영상을 업로드하고 사용하는 것도 적용해 볼 생각이다.

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글