뭘 추가했는가?
기능적으로 필요한 부분을 추가했다.
인증이 필요하다 (Firebase Auth)
영상/사진 업로드가 필요하다 (Firebase Storage)
같은 구글이라 그런지 Firebase와 Flutter 연동은 엄청 쉽다. cli 설치하고 나서 거의 떠먹여주는 수준.

똰. 추가 완료.
앱 내 로그인 추가
개인적으로 OAuth를 좋아하긴 하지만 심사 통과나 베타 테스트 목적으로는 일반 로그인만 구현해도 끝날 것 같았다. 서버 호스팅 까지 비용 감당을 벌써 하는건…좀 그렇고 일단 MVP 차원에서는 Firebase Auth 쓰면 끝난다.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
final authStateChangesProvider = StreamProvider<User?>((ref) {
final auth = ref.watch(firebaseAuthProvider);
return auth.authStateChanges();
});
서비스를 추가해놓고, 초기 화면 라우트를 가드한다.
final goRouterProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateChangesProvider);
return GoRouter(
initialLocation: '/memories',
redirect: (context, state) {
final isLoggedIn = authState.asData?.value != null;
final isGoingToSignIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isGoingToSignIn) {
return '/login';
}
if (isLoggedIn && isGoingToSignIn) {
return '/memories';
}
return null;
},
...
로그인 관련해서 아직은 복잡하게 구현하지는 않았고, 테스트 계정을 임의로 등록해놓고 인증 되는지만 검증하였다. (어차피 validation 붙이면서 나중에 더 작업해야 한다)
class LoginScreen extends ConsumerWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final auth = ref.read(firebaseAuthProvider);
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
await auth.signInWithEmailAndPassword(
email: 'test@test.com',
password: 'testpassword',
);
},
child: const Text('Sign In'),
),
),
);
}
}

(사용 중지했으니 이 글 보고 마구마구 로그인 눌러보지 마시길… 어차피 안돼요.)
사진/영상 업로드
요건 제일 많이 쓰는 image_picker
패키지와, 선택한 파일은 Firebase Storage에 업로드하고 url을 반환하는 형태로 마무리지었다.
Future<void> _handleMediaUpload() async {
final ImagePicker picker = ImagePicker();
final List<XFile> medias = await picker.pickMultipleMedia();
if (medias.isEmpty) {
return;
}
final storageRef = FirebaseStorage.instance.ref();
final userId = FirebaseAuth.instance.currentUser?.uid;
for (final media in medias) {
final file = File(media.path);
final fileName = media.name;
final uploadRef = storageRef.child('memories/$userId/$fileName');
try {
final uploadTask = await uploadRef.putFile(file);
final downloadUrl = await uploadTask.ref.getDownloadURL();
debugPrint('Uploaded: $downloadUrl');
} catch (e) {
debugPrint('Upload failed: $e');
}
}
}

(이것도 CRUD 동작 전부 막았으니 이 글 보고 마구마구 업로드 하지 마시길… 어차피 안돼요.)
먼 훗날 AWS S3 Bucket으로 마이그레이션 해도 그리 복잡하지는 않을 것이다.
남은 것?
이제 중요한 기능은 거의 끝냈다. 남은 부분은
회원가입
번역
CRUD 동작 및 상태관리
iOS 앱스토어 업로드
Android APK 패키징 (심사가 빡빡해서 베타 테스트 방법은 고민 중)
남은 것 아마 손이 많이 가겠지만 주말에 빡개발 하면 마무리 지을 수 있을 듯.